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(/^
([\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