marktext/src/muya/lib/parser/render/index.js
2019-07-24 10:57:21 +08:00

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