From 2c05994f8069a1b0f4d051da42184978ef76c63d Mon Sep 17 00:00:00 2001 From: suxin Date: Sat, 1 Apr 2023 23:31:57 +0800 Subject: [PATCH] fix: mermaid will render a plain text on DOM target when secondary render happens, reference: https://github.com/mermaid-js/mermaid/issues/311#issuecomment-332557344 --- README.md | 4 +- commitlint.config.js | 30 +++ lib/contentState/index.js | 4 + lib/eventHandler/clickEvent.js | 2 +- lib/parser/render/index.js | 57 ++-- lib/parser/render/snabbdom.js | 7 +- lib/renderers/index.js | 2 +- lib/selection/README.md | 26 ++ lib/selection/index.js | 117 ++++++--- package.json | 2 +- yarn.lock | 458 +++++++++++---------------------- 11 files changed, 325 insertions(+), 384 deletions(-) create mode 100644 commitlint.config.js create mode 100644 lib/selection/README.md diff --git a/README.md b/README.md index ecdfc57..f6d4c0f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -### Meidt +

Medit

-A browser based Markdown editor. \ No newline at end of file +

This libray is extracted from Marktext. Thanks to their hard work.

diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..a1e84d5 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,30 @@ +module.exports = { + parserPreset: { + parserOpts: { + headerPattern: /^(\w*)(?:\((.*)\))?:[ ]?(.*)$/, + headerCorrespondence: ['type', 'scope', 'subject'] + } + }, + rules: { + 'type-empty': [2, 'never'], + 'type-case': [2, 'always', 'lower-case'], + 'subject-empty': [2, 'never'], + 'type-enum': [ + 2, + 'always', + [ + 'build', + 'ci', + 'chore', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test' + ] + ] + } +} \ No newline at end of file diff --git a/lib/contentState/index.js b/lib/contentState/index.js index 250b257..1f3a346 100644 --- a/lib/contentState/index.js +++ b/lib/contentState/index.js @@ -232,8 +232,11 @@ class ContentState { }) this.setNextRenderRange() this.stateRender.collectLabels(blocks) + console.log("[ContentState::render]: blocks", blocks); + this.stateRender.render(blocks, activeBlocks, matches) if (isRenderCursor) { + console.log("[ContentState::render] setCursor") this.setCursor() } else { this.muya.blur() @@ -269,6 +272,7 @@ class ContentState { this.stateRender.collectLabels(blocks) this.stateRender.partialRender(blocksToRender, activeBlocks, matches, startKey, endKey) if (isRenderCursor) { + console.log("[ContentState:partialRender] setCursor") this.setCursor() } else { this.muya.blur() diff --git a/lib/eventHandler/clickEvent.js b/lib/eventHandler/clickEvent.js index 7a8c96b..54c2368 100644 --- a/lib/eventHandler/clickEvent.js +++ b/lib/eventHandler/clickEvent.js @@ -55,7 +55,7 @@ class ClickEvent { end } } - + console.log("[contextClickBingding]:Context clicked, set curser to: ", contentState.cursor, " and dispatch 'contextmenu' event.") const sectionChanges = contentState.selectionChange(contentState.cursor) eventCenter.dispatch('contextmenu', event, sectionChanges) } diff --git a/lib/parser/render/index.js b/lib/parser/render/index.js index 6340198..2162b20 100644 --- a/lib/parser/render/index.js +++ b/lib/parser/render/index.js @@ -1,13 +1,13 @@ import loadRenderer from '../../renderers' -import { CLASS_OR_ID, PREVIEW_DOMPURIFY_CONFIG } from '../../config' -import { conflict, mixins, camelToSnake, sanitize } from '../../utils' -import { patch, toVNode, toHTML, h } from './snabbdom' -import { beginRules } from '../rules' +import {CLASS_OR_ID, PREVIEW_DOMPURIFY_CONFIG} from '../../config' +import {conflict, mixins, camelToSnake, sanitize} 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) { + constructor(muya) { this.muya = muya this.eventCenter = muya.eventCenter this.codeCache = new Map() @@ -23,16 +23,16 @@ class StateRender { this.container = null } - setContainer (container) { + setContainer(container) { this.container = container } // collect link reference definition - collectLabels (blocks) { + collectLabels(blocks) { this.labels.clear() const travel = block => { - const { text, children } = block + const {text, children} = block if (children && children.length) { children.forEach(c => travel(c)) } else if (text) { @@ -52,10 +52,10 @@ class StateRender { blocks.forEach(b => travel(b)) } - checkConflicted (block, token, cursor) { - const { start, end } = cursor + checkConflicted(block, token, cursor) { + const {start, end} = cursor const key = block.key - const { start: tokenStart, end: tokenEnd } = token.range + const {start: tokenStart, end: tokenEnd} = token.range if (key !== start.key && key !== end.key) { return false @@ -69,16 +69,16 @@ class StateRender { } } - getClassName (outerClass, block, token, cursor) { + getClassName(outerClass, block, token, cursor) { return outerClass || (this.checkConflicted(block, token, cursor) ? CLASS_OR_ID.AG_GRAY : CLASS_OR_ID.AG_HIDE) } - getHighlightClassName (active) { + getHighlightClassName(active) { return active ? CLASS_OR_ID.AG_HIGHLIGHT : CLASS_OR_ID.AG_SELECTION } - getSelector (block, activeBlocks) { - const { cursor, selectedBlock } = this.muya.contentState + 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 @@ -95,15 +95,17 @@ class StateRender { return selector } - async renderMermaid () { + async renderMermaid() { if (this.mermaidCache.size) { + console.log("[StateRender:renderMermaid]: start"); + const mermaid = await loadRenderer('mermaid') mermaid.initialize({ securityLevel: 'strict', theme: this.muya.options.mermaidTheme }) for (const [key, value] of this.mermaidCache.entries()) { - const { code } = value + const {code} = value const target = document.querySelector(key) if (!target) { continue @@ -111,6 +113,11 @@ class StateRender { try { mermaid.parse(code) target.innerHTML = sanitize(code, PREVIEW_DOMPURIFY_CONFIG, true) + // mermaid will not render if DOM target has data-processed attribute set to true, + // so we need to remove it here. + if (target.getAttribute('data-processed') === 'true') { + target.removeAttribute("data-processed"); + } mermaid.init(undefined, target) } catch (err) { target.innerHTML = '< Invalid Mermaid Codes >' @@ -122,7 +129,7 @@ class StateRender { } } - async renderDiagram () { + async renderDiagram() { const cache = this.diagramCache if (cache.size) { const RENDER_MAP = { @@ -137,11 +144,11 @@ class StateRender { if (!target) { continue } - const { code, functionType } = value + const {code, functionType} = value const render = RENDER_MAP[functionType] const options = {} if (functionType === 'sequence') { - Object.assign(options, { theme: this.muya.options.sequenceTheme }) + Object.assign(options, {theme: this.muya.options.sequenceTheme}) } else if (functionType === 'vega-lite') { Object.assign(options, { actions: false, @@ -171,7 +178,7 @@ class StateRender { } } - render (blocks, activeBlocks, matches) { + render(blocks, activeBlocks, matches) { const selector = `div#${CLASS_OR_ID.AG_EDITOR_ID}` const children = blocks.map(block => { return this.renderBlock(null, block, activeBlocks, matches, true) @@ -187,7 +194,7 @@ class StateRender { } // Only render the blocks which you updated - partialRender (blocks, activeBlocks, matches, startKey, endKey) { + 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 @@ -216,7 +223,7 @@ class StateRender { // Render cursor block independently if (needRenderCursorBlock) { - const { key } = cursorOutMostBlock + const {key} = cursorOutMostBlock const cursorDom = document.querySelector(`#${key}`) if (cursorDom) { const oldCursorVnode = toVNode(cursorDom) @@ -237,7 +244,7 @@ class StateRender { * @param {array} activeBlocks * @param {array} matches */ - singleRender (block, activeBlocks, matches) { + singleRender(block, activeBlocks, matches) { const selector = `#${block.key}` const newVdom = this.renderBlock(null, block, activeBlocks, matches, true) const rootDom = document.querySelector(selector) @@ -248,7 +255,7 @@ class StateRender { this.codeCache.clear() } - invalidateImageCache () { + invalidateImageCache() { this.loadImageMap.forEach((imageInfo, key) => { imageInfo.touchMsec = Date.now() this.loadImageMap.set(key, imageInfo) diff --git a/lib/parser/render/snabbdom.js b/lib/parser/render/snabbdom.js index 2f6cc0b..92129bf 100644 --- a/lib/parser/render/snabbdom.js +++ b/lib/parser/render/snabbdom.js @@ -19,10 +19,13 @@ export const patch = init([ eventListenersModule ]) -export const h = sh -export const toVNode = sToVNode + +export const h = sh // for create virtual dom + +export const toVNode = sToVNode // converting a DOM element to a virtual node export const toHTML = require('snabbdom-to-html') // helper function for convert vnode to HTML string + export const htmlToVNode = html => { // helper function for convert html to vnode const wrapper = document.createElement('div') wrapper.innerHTML = html diff --git a/lib/renderers/index.js b/lib/renderers/index.js index 7235667..89ba157 100644 --- a/lib/renderers/index.js +++ b/lib/renderers/index.js @@ -20,7 +20,7 @@ const loadRenderer = async (name) => { rendererCache.set(name, m.default) break case 'mermaid': - m = await import('mermaid/dist/mermaid.core.js') + m = await import('mermaid/dist/mermaid.core.mjs') rendererCache.set(name, m.default) break case 'vega-lite': diff --git a/lib/selection/README.md b/lib/selection/README.md new file mode 100644 index 0000000..33888a9 --- /dev/null +++ b/lib/selection/README.md @@ -0,0 +1,26 @@ +## Some Knowledge to know before you start + +### Node Type +- 1: Element node +- 2: Attribute node +- 3: Text node +- 4: CDATA section node +- 5: Entity Reference node +- 6: Entity node +- 7: Processing Instruction node +- 8: Comment node +- 9: Document node +- 10: Document Type node +- 11: Document Fragment node +- 12: Notation node + +### Range.setStart() and Range.setEnd() +If the `node` argument passed to `setStart()` is a text node, the `startOffset` value is the index of the first +character in the text node that should be included in the range. For example, if `node` is a text node containing +the string "Hello, world", and `startOffset` is 3, then the range would start with the fourth character in the +text node, which is the letter "l" + +If the `node` argument is an element node, the `startOffset` value is the index of the child node within the +element that should be the start of the range. For example, if `node` is an unordered list (`