mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 15:21:33 +08:00
231 lines
7.4 KiB
JavaScript
231 lines
7.4 KiB
JavaScript
import mermaid from 'mermaid'
|
|
import flowchart from 'flowchart.js'
|
|
import Diagram from './sequence'
|
|
import vegaEmbed from 'vega-embed'
|
|
import { CLASS_OR_ID } from '../../config'
|
|
import { conflict, mixins, camelToSnake } from '../../utils'
|
|
import { patch, toVNode, toHTML, h } from './snabbdom'
|
|
import { beginRules } from '../rules'
|
|
import renderInlines from './renderInlines'
|
|
import renderBlock from './renderBlock'
|
|
|
|
class StateRender {
|
|
constructor (muya) {
|
|
this.muya = muya
|
|
this.eventCenter = muya.eventCenter
|
|
this.codeCache = new Map()
|
|
this.loadImageMap = new Map()
|
|
this.loadMathMap = new Map()
|
|
this.mermaidCache = new Set()
|
|
this.diagramCache = new Map()
|
|
this.tokenCache = new Map()
|
|
this.labels = new Map()
|
|
this.urlMap = new Map()
|
|
this.container = null
|
|
}
|
|
|
|
setContainer (container) {
|
|
this.container = container
|
|
}
|
|
|
|
// collect link reference definition
|
|
collectLabels (blocks) {
|
|
this.labels.clear()
|
|
|
|
const travel = block => {
|
|
const { text, children } = block
|
|
if (children && children.length) {
|
|
children.forEach(c => travel(c))
|
|
} else if (text) {
|
|
const tokens = beginRules['reference_definition'].exec(text)
|
|
if (tokens) {
|
|
const key = (tokens[2] + tokens[3]).toLowerCase()
|
|
if (!this.labels.has(key)) {
|
|
this.labels.set(key, {
|
|
href: tokens[6],
|
|
title: tokens[10] || ''
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
blocks.forEach(b => travel(b))
|
|
}
|
|
|
|
checkConflicted (block, token, cursor) {
|
|
const { start, end } = cursor
|
|
const key = block.key
|
|
const { start: tokenStart, end: tokenEnd } = token.range
|
|
|
|
if (key !== start.key && key !== end.key) {
|
|
return false
|
|
} else if (key === start.key && key !== end.key) {
|
|
return conflict([tokenStart, tokenEnd], [start.offset, start.offset])
|
|
} else if (key !== start.key && key === end.key) {
|
|
return conflict([tokenStart, tokenEnd], [end.offset, end.offset])
|
|
} else {
|
|
return conflict([tokenStart, tokenEnd], [start.offset, start.offset]) ||
|
|
conflict([tokenStart, tokenEnd], [end.offset, end.offset])
|
|
}
|
|
}
|
|
|
|
getClassName (outerClass, block, token, cursor) {
|
|
return outerClass || (this.checkConflicted(block, token, cursor) ? CLASS_OR_ID['AG_GRAY'] : CLASS_OR_ID['AG_HIDE'])
|
|
}
|
|
|
|
getHighlightClassName (active) {
|
|
return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION']
|
|
}
|
|
|
|
getSelector (block, activeBlocks) {
|
|
const { cursor, selectedBlock } = this.muya.contentState
|
|
const type = block.type === 'hr' ? 'p' : block.type
|
|
const isActive = activeBlocks.some(b => b.key === block.key) || block.key === cursor.start.key
|
|
|
|
let selector = `${type}#${block.key}.${CLASS_OR_ID['AG_PARAGRAPH']}`
|
|
if (isActive) {
|
|
selector += `.${CLASS_OR_ID['AG_ACTIVE']}`
|
|
}
|
|
if (type === 'span') {
|
|
selector += `.ag-${camelToSnake(block.functionType)}`
|
|
}
|
|
if (!block.parent && selectedBlock && block.key === selectedBlock.key) {
|
|
selector += `.${CLASS_OR_ID['AG_SELECTED']}`
|
|
}
|
|
return selector
|
|
}
|
|
|
|
renderMermaid () {
|
|
if (this.mermaidCache.size) {
|
|
mermaid.initialize({
|
|
theme: this.muya.options.mermaidTheme
|
|
})
|
|
mermaid.init(undefined, document.querySelectorAll(Array.from(this.mermaidCache).join(', ')))
|
|
this.mermaidCache.clear()
|
|
}
|
|
}
|
|
|
|
async renderDiagram () {
|
|
const cache = this.diagramCache
|
|
const RENDER_MAP = {
|
|
flowchart: flowchart,
|
|
sequence: Diagram,
|
|
'vega-lite': vegaEmbed
|
|
}
|
|
if (cache.size) {
|
|
for (const [key, value] of cache.entries()) {
|
|
const target = document.querySelector(key)
|
|
const { code, functionType } = value
|
|
const render = RENDER_MAP[functionType]
|
|
const options = {}
|
|
if (functionType === 'sequence') {
|
|
Object.assign(options, { theme: this.muya.options.sequenceTheme })
|
|
} else if (functionType === 'vega-lite') {
|
|
Object.assign(options, {
|
|
actions: false,
|
|
tooltip: false,
|
|
renderer: 'svg',
|
|
theme: this.muya.options.vegaTheme
|
|
})
|
|
}
|
|
try {
|
|
if (functionType === 'flowchart' || functionType === 'sequence') {
|
|
const diagram = render.parse(code)
|
|
target.innerHTML = ''
|
|
diagram.drawSVG(target, options)
|
|
} else if (functionType === 'vega-lite') {
|
|
await render(key, JSON.parse(code), options)
|
|
}
|
|
} catch (err) {
|
|
target.innerHTML = `< Invalid ${functionType === 'flowchart' ? 'Flow Chart' : 'Sequence'} Codes >`
|
|
target.classList.add(CLASS_OR_ID['AG_MATH_ERROR'])
|
|
}
|
|
}
|
|
this.diagramCache.clear()
|
|
}
|
|
}
|
|
|
|
render (blocks, activeBlocks, matches) {
|
|
const selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
|
const children = blocks.map(block => {
|
|
return this.renderBlock(block, activeBlocks, matches, true)
|
|
})
|
|
|
|
const newVdom = h(selector, children)
|
|
const rootDom = document.querySelector(selector) || this.container
|
|
const oldVdom = toVNode(rootDom)
|
|
|
|
patch(oldVdom, newVdom)
|
|
this.renderMermaid()
|
|
this.renderDiagram()
|
|
this.codeCache.clear()
|
|
}
|
|
|
|
// Only render the blocks which you updated
|
|
partialRender (blocks, activeBlocks, matches, startKey, endKey) {
|
|
const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
|
|
// If cursor is not in render blocks, need to render cursor block independently
|
|
const needRenderCursorBlock = blocks.indexOf(cursorOutMostBlock) === -1
|
|
const newVnode = h('section', blocks.map(block => this.renderBlock(block, activeBlocks, matches)))
|
|
const html = toHTML(newVnode).replace(/^<section>([\s\S]+?)<\/section>$/, '$1')
|
|
|
|
const needToRemoved = []
|
|
const firstOldDom = startKey
|
|
? document.querySelector(`#${startKey}`)
|
|
: document.querySelector(`div#${CLASS_OR_ID['AG_EDITOR_ID']}`).firstElementChild
|
|
if (!firstOldDom) {
|
|
// TODO@Jocs Just for fix #541, Because I'll rewrite block and render method, it will nolonger have this issue.
|
|
return
|
|
}
|
|
needToRemoved.push(firstOldDom)
|
|
let nextSibling = firstOldDom.nextElementSibling
|
|
while (nextSibling && nextSibling.id !== endKey) {
|
|
needToRemoved.push(nextSibling)
|
|
nextSibling = nextSibling.nextElementSibling
|
|
}
|
|
nextSibling && needToRemoved.push(nextSibling)
|
|
|
|
firstOldDom.insertAdjacentHTML('beforebegin', html)
|
|
|
|
Array.from(needToRemoved).forEach(dom => dom.remove())
|
|
|
|
// Render cursor block independently
|
|
if (needRenderCursorBlock) {
|
|
const { key } = cursorOutMostBlock
|
|
const cursorDom = document.querySelector(`#${key}`)
|
|
if (cursorDom) {
|
|
const oldCursorVnode = toVNode(cursorDom)
|
|
const newCursorVnode = this.renderBlock(cursorOutMostBlock, activeBlocks, matches)
|
|
patch(oldCursorVnode, newCursorVnode)
|
|
}
|
|
}
|
|
|
|
this.renderMermaid()
|
|
this.renderDiagram()
|
|
this.codeCache.clear()
|
|
}
|
|
|
|
/**
|
|
* Only render one block.
|
|
*
|
|
* @param {object} block
|
|
* @param {array} activeBlocks
|
|
* @param {array} matches
|
|
*/
|
|
singleRender (block, activeBlocks, matches) {
|
|
const selector = `#${block.key}`
|
|
const newVdom = this.renderBlock(block, activeBlocks, matches, true)
|
|
const rootDom = document.querySelector(selector)
|
|
const oldVdom = toVNode(rootDom)
|
|
patch(oldVdom, newVdom)
|
|
this.renderMermaid()
|
|
this.renderDiagram()
|
|
this.codeCache.clear()
|
|
}
|
|
}
|
|
|
|
mixins(StateRender, renderInlines, renderBlock)
|
|
|
|
export default StateRender
|