UNPKG

md-editor-v3

Version:

Markdown editor for vue3, developed in jsx and typescript, dark theme、beautify content by prettier、render articles directly、paste or clip the picture and upload it...

1,614 lines 67.8 kB
"use strict"; const vue = require("vue"); const config = require("./config.cjs"); const eventName = require("./event-name.cjs"); const util = require("@vavt/util"); const dom = require("./dom.cjs"); const mediumZoom = require("medium-zoom"); const copy2clipboard = require("@vavt/copy2clipboard"); const mdit = require("markdown-it"); const ImageFiguresPlugin = require("markdown-it-image-figures"); const SubPlugin = require("markdown-it-sub"); const SupPlugin = require("markdown-it-sup"); const index = require("./index5.cjs"); const lruCache = require("lru-cache"); const CDN_IDS = { hljs: `${config.prefix}-hljs`, hlcss: `${config.prefix}-hlCss`, prettier: `${config.prefix}-prettier`, prettierMD: `${config.prefix}-prettierMD`, cropperjs: `${config.prefix}-cropper`, croppercss: `${config.prefix}-cropperCss`, screenfull: `${config.prefix}-screenfull`, mermaidM: `${config.prefix}-mermaid-m`, mermaid: `${config.prefix}-mermaid`, katexjs: `${config.prefix}-katex`, katexcss: `${config.prefix}-katexCss` }; const userZoom = (props, html) => { const editorId = vue.inject("editorId"); const { noImgZoomIn } = props; const zoomHander = util.debounce(() => { const imgs = document.querySelectorAll( `#${editorId}-preview img:not(.not-zoom):not(.medium-zoom-image)` ); if (imgs.length === 0) { return; } mediumZoom(imgs, { background: "#00000073" }); }); vue.onMounted(() => { if (!noImgZoomIn && props.setting.preview) { zoomHander(); } }); vue.watch([html, vue.toRef(props.setting, "preview")], () => { if (!noImgZoomIn && props.setting.preview) { zoomHander(); } }); }; const useCopyCode = (props, html, key) => { const editorId = vue.inject("editorId"); const rootRef = vue.inject("rootRef"); const ult = vue.inject("usedLanguageText"); const initCopyEntry = () => { rootRef.value.querySelectorAll(`#${editorId} .${config.prefix}-preview .${config.prefix}-code`).forEach((codeBlock) => { let clearTimer = -1; const copyButton = codeBlock.querySelector( `.${config.prefix}-copy-button` ); if (copyButton) copyButton.onclick = (e) => { e.preventDefault(); clearTimeout(clearTimer); const activeCode = codeBlock.querySelector("input:checked + pre code") || codeBlock.querySelector("pre code"); const codeText = activeCode.textContent; const { text, successTips, failTips } = ult.value.copyCode; let msg = successTips; copy2clipboard(props.formatCopiedText(codeText)).catch(() => { msg = failTips; }).finally(() => { if (copyButton.dataset.isIcon) { copyButton.dataset.tips = msg; } else { copyButton.innerHTML = msg; } clearTimer = window.setTimeout(() => { if (copyButton.dataset.isIcon) { copyButton.dataset.tips = text; } else { copyButton.innerHTML = text; } }, 1500); }); }; }); }; const htmlChanged = () => { vue.nextTick(initCopyEntry); }; const settingPreviewChanged = (nVal) => { if (nVal) { vue.nextTick(initCopyEntry); } }; vue.watch([html, key], htmlChanged); vue.watch(() => props.setting.preview, settingPreviewChanged); vue.watch(() => props.setting.htmlPreview, settingPreviewChanged); vue.onMounted(initCopyEntry); }; const useHighlight = (props) => { const highlight = vue.inject("highlight"); const hljsRef = vue.shallowRef(config.globalConfig.editorExtensions.highlight.instance); vue.onMounted(() => { if (props.noHighlight || hljsRef.value) { return; } dom.appendHandler("link", { ...highlight.value.css, rel: "stylesheet", id: CDN_IDS.hlcss }); dom.appendHandler( "script", { ...highlight.value.js, id: CDN_IDS.hljs, onload() { hljsRef.value = window.hljs; } }, "hljs" ); }); vue.watch( () => highlight.value.css, () => { if (props.noHighlight || config.globalConfig.editorExtensions.highlight.instance) { return; } dom.updateHandler("link", { ...highlight.value.css, rel: "stylesheet", id: CDN_IDS.hlcss }); } ); return hljsRef; }; const mermaidCache = new lruCache.LRUCache({ max: 1e3, // 缓存10分钟 ttl: 6e5 }); const useMermaid = (props) => { const editorId = vue.inject("editorId"); const theme = vue.inject("theme"); const rootRef = vue.inject("rootRef"); const { editorExtensions, editorExtensionsAttrs, mermaidConfig } = config.globalConfig; let mermaid = editorExtensions.mermaid.instance; const reRenderRef = vue.shallowRef(-1); const configMermaid = () => { if (!props.noMermaid && mermaid) { mermaid.initialize( mermaidConfig({ startOnLoad: false, theme: theme.value === "dark" ? "dark" : "default" }) ); reRenderRef.value = reRenderRef.value + 1; } }; vue.watch( () => theme.value, () => { mermaidCache.clear(); configMermaid(); } ); vue.onMounted(() => { var _a, _b; if (props.noMermaid || mermaid) { return; } const jsSrc = editorExtensions.mermaid.js; if (/\.mjs/.test(jsSrc)) { dom.appendHandler("link", { ...(_a = editorExtensionsAttrs.mermaid) == null ? void 0 : _a.js, rel: "modulepreload", href: jsSrc, id: CDN_IDS.mermaidM }); import( /* @vite-ignore */ /* webpackIgnore: true */ jsSrc ).then((module2) => { mermaid = module2.default; configMermaid(); }); } else { dom.appendHandler( "script", { ...(_b = editorExtensionsAttrs.mermaid) == null ? void 0 : _b.js, src: jsSrc, id: CDN_IDS.mermaid, onload() { mermaid = window.mermaid; configMermaid(); } }, "mermaid" ); } }); const replaceMermaid = async () => { if (!props.noMermaid && mermaid) { const mermaidSourceEles = rootRef.value.querySelectorAll( `div.${config.prefix}-mermaid` ); const svgContainingElement = document.createElement("div"); const sceWidth = document.body.offsetWidth > 1366 ? document.body.offsetWidth : 1366; const sceHeight = document.body.offsetHeight > 768 ? document.body.offsetHeight : 768; svgContainingElement.style.width = sceWidth + "px"; svgContainingElement.style.height = sceHeight + "px"; svgContainingElement.style.position = "fixed"; svgContainingElement.style.zIndex = "-10000"; svgContainingElement.style.top = "-10000"; let count = mermaidSourceEles.length; if (count > 0) { document.body.appendChild(svgContainingElement); } await Promise.allSettled( Array.from(mermaidSourceEles).map((ele) => { const handler = async (item) => { var _a; if (item.dataset.closed === "false") { return false; } const code = item.innerText; let mermaidHtml = mermaidCache.get(code); if (!mermaidHtml) { const idRand = util.randomId(); let result = { svg: "" }; try { result = await mermaid.render(idRand, code, svgContainingElement); mermaidHtml = await props.sanitizeMermaid(result.svg); const p = document.createElement("p"); p.className = `${config.prefix}-mermaid`; p.setAttribute("data-processed", ""); p.innerHTML = mermaidHtml; (_a = p.children[0]) == null ? void 0 : _a.removeAttribute("height"); mermaidCache.set(code, p.innerHTML); if (item.dataset.line !== void 0) { p.dataset.line = item.dataset.line; } item.replaceWith(p); } catch (error) { eventName.bus.emit(editorId, eventName.ERROR_CATCHER, { name: "mermaid", message: error.message, error }); } if (--count === 0) { svgContainingElement.remove(); } } }; return handler(ele); }) ); } }; return { reRenderRef, replaceMermaid }; }; const useKatex = (props) => { const katex = vue.shallowRef(config.globalConfig.editorExtensions.katex.instance); vue.onMounted(() => { var _a, _b; if (props.noKatex || katex.value) { return; } const { editorExtensions, editorExtensionsAttrs } = config.globalConfig; dom.appendHandler( "script", { ...(_a = editorExtensionsAttrs.katex) == null ? void 0 : _a.js, src: editorExtensions.katex.js, id: CDN_IDS.katexjs, onload() { katex.value = window.katex; } }, "katex" ); dom.appendHandler("link", { ...(_b = editorExtensionsAttrs.katex) == null ? void 0 : _b.css, rel: "stylesheet", href: editorExtensions.katex.css, id: CDN_IDS.katexcss }); }); return katex; }; const MermaidPlugin = (md, options) => { const temp = md.renderer.rules.fence.bind(md.renderer.rules); md.renderer.rules.fence = (tokens, idx, ops, env, slf) => { var _a; const token = tokens[idx]; const code = token.content.trim(); if (token.info === "mermaid") { token.attrSet("class", `${config.prefix}-mermaid`); token.attrSet("data-mermaid-theme", options.themeRef.value); if (token.map && token.level === 0) { const closeLine = token.map[1] - 1; const closeLineText = (_a = env.srcLines[closeLine]) == null ? void 0 : _a.trim(); const isClosingFence = closeLineText == null ? void 0 : closeLineText.startsWith("```"); token.attrSet("data-closed", isClosingFence); token.attrSet("data-line", String(token.map[0])); } const mermaidHtml = mermaidCache.get(code); if (mermaidHtml) { token.attrSet("data-processed", ""); return `<p ${slf.renderAttrs(token)}>${mermaidHtml}</p>`; } return `<div ${slf.renderAttrs(token)}>${md.utils.escapeHtml(code)}</div>`; } return temp(tokens, idx, ops, env, slf); }; }; const mergeAttrs = (token, addAttrs) => { const tmpAttrs = token.attrs ? token.attrs.slice() : []; addAttrs.forEach((addAttr) => { const i = token.attrIndex(addAttr[0]); if (i < 0) { tmpAttrs.push(addAttr); } else { tmpAttrs[i] = tmpAttrs[i].slice(); tmpAttrs[i][1] += ` ${addAttr[1]}`; } }); return tmpAttrs; }; const delimiters = { block: [ { open: "$$", close: "$$" }, { open: "\\[", close: "\\]" } ], inline: [ { open: "$$", close: "$$" }, { open: "$", close: "$" }, { open: "\\[", close: "\\]" }, { open: "\\(", close: "\\)" } ] }; const create_math_inline = (options) => (state, silent) => { const delimiters2 = options.delimiters; let match, token, pos; for (const delim of delimiters2) { if (state.src.startsWith(delim.open, state.pos)) { const start = state.pos + delim.open.length; match = start; while ((match = state.src.indexOf(delim.close, match)) !== -1) { pos = match - 1; while (state.src[pos] === "\\") { pos -= 1; } if ((match - pos) % 2 === 1) { break; } match += delim.close.length; } if (match === -1) { if (!silent) { state.pending += delim.open; } state.pos = start; return true; } if (match - start === 0) { if (!silent) { state.pending += delim.open + delim.close; } state.pos = start + delim.close.length; return true; } if (!silent) { const inlineContent = state.src.slice(start, match); token = state.push("math_inline", "math", 0); token.markup = delim.open; token.content = inlineContent; } state.pos = match + delim.close.length; return true; } } return false; }; const create_math_block = (options) => (state, start, end, silent) => { const delimiters2 = options.delimiters; let firstLine, lastLine, next, lastPos, found = false; let pos = state.bMarks[start] + state.tShift[start]; let max = state.eMarks[start]; for (const delim of delimiters2) { if (state.src.slice(pos, pos + delim.open.length) === delim.open && state.src.slice(max - delim.close.length, max) === delim.close) { pos += delim.open.length; firstLine = state.src.slice(pos, max); if (silent) { return true; } if (firstLine.trim().slice(-delim.close.length) === delim.close) { firstLine = firstLine.trim().slice(0, -delim.close.length); found = true; } for (next = start; !found; ) { next++; if (next >= end) { break; } pos = state.bMarks[next] + state.tShift[next]; max = state.eMarks[next]; if (pos < max && state.tShift[next] < state.blkIndent) { break; } if (state.src.slice(pos, max).trim().slice(-delim.close.length) === delim.close) { lastPos = state.src.slice(0, max).lastIndexOf(delim.close); lastLine = state.src.slice(pos, lastPos); found = true; } } state.line = next + 1; const token = state.push("math_block", "math", 0); token.block = true; token.content = (firstLine && firstLine.trim() ? firstLine + "\n" : "") + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine && lastLine.trim() ? lastLine : ""); token.map = [start, state.line]; token.markup = delim.open; return true; } } return false; }; const KatexPlugin = (md, { katexRef, inlineDelimiters, blockDelimiters }) => { const katexInline = (tokens, idx, options, env, slf) => { const token = tokens[idx]; const tmpToken = { attrs: mergeAttrs(token, [["class", `${config.prefix}-katex-inline`]]) }; if (katexRef.value) { const html = katexRef.value.renderToString( token.content, config.globalConfig.katexConfig({ throwOnError: false }) ); return `<span ${slf.renderAttrs(tmpToken)} data-processed>${html}</span>`; } else { return `<span ${slf.renderAttrs(tmpToken)}>${token.content}</span>`; } }; const katexBlock = (tokens, idx, options, env, slf) => { const token = tokens[idx]; const tmpToken = { attrs: mergeAttrs(token, [["class", `${config.prefix}-katex-block`]]) }; if (katexRef.value) { const html = katexRef.value.renderToString( token.content, config.globalConfig.katexConfig({ throwOnError: false, displayMode: true }) ); return `<p ${slf.renderAttrs(tmpToken)} data-processed>${html}</p>`; } else { return `<p ${slf.renderAttrs(tmpToken)}>${token.content}</p>`; } }; md.inline.ruler.before( "escape", "math_inline", create_math_inline({ delimiters: inlineDelimiters || delimiters.inline }) ); md.block.ruler.after( "blockquote", "math_block", create_math_block({ delimiters: blockDelimiters || delimiters.block }), { alt: ["paragraph", "reference", "blockquote", "list"] } ); md.renderer.rules.math_inline = katexInline; md.renderer.rules.math_block = katexBlock; }; const AdmonitionPlugin = (md, options) => { options = options || {}; const markers = 3, markerStr = options.marker || "!", markerChar = markerStr.charCodeAt(0), markerLen = markerStr.length; let type = "", title = ""; const render = (tokens, idx, _options, _env, self) => { const token = tokens[idx]; if (token.type === "admonition_open") { tokens[idx].attrPush([ "class", `${config.prefix}-admonition ${config.prefix}-admonition-${token.info}` ]); } else if (token.type === "admonition_title_open") { tokens[idx].attrPush(["class", `${config.prefix}-admonition-title`]); } return self.renderToken(tokens, idx, _options); }; const validate = (params) => { const array = params.trim().split(" ", 2); title = ""; type = array[0]; if (array.length > 1) { title = params.substring(type.length + 2); } }; md.block.ruler.before( "code", "admonition", (state, startLine, endLine, silent) => { let pos, nextLine, token, autoClosed = false, start = state.bMarks[startLine] + state.tShift[startLine], max = state.eMarks[startLine]; if (markerChar !== state.src.charCodeAt(start)) { return false; } for (pos = start + 1; pos <= max; pos++) { if (markerStr[(pos - start) % markerLen] !== state.src[pos]) { break; } } const markerCount = Math.floor((pos - start) / markerLen); if (markerCount !== markers) { return false; } pos -= (pos - start) % markerLen; const markup = state.src.slice(start, pos); const params = state.src.slice(pos, max); validate(params); if (silent) { return true; } nextLine = startLine; for (; ; ) { nextLine++; if (nextLine >= endLine) { break; } start = state.bMarks[nextLine] + state.tShift[nextLine]; max = state.eMarks[nextLine]; if (start < max && state.sCount[nextLine] < state.blkIndent) { break; } if (markerChar !== state.src.charCodeAt(start)) { continue; } if (state.sCount[nextLine] - state.blkIndent >= 4) { continue; } for (pos = start + 1; pos <= max; pos++) { if (markerStr[(pos - start) % markerLen] !== state.src[pos]) { break; } } if (Math.floor((pos - start) / markerLen) < markerCount) { continue; } pos -= (pos - start) % markerLen; pos = state.skipSpaces(pos); if (pos < max) { continue; } autoClosed = true; break; } const oldParent = state.parentType; const oldLineMax = state.lineMax; state.parentType = "root"; state.lineMax = nextLine; token = state.push("admonition_open", "div", 1); token.markup = markup; token.block = true; token.info = type; token.map = [startLine, nextLine]; if (title) { token = state.push("admonition_title_open", "p", 1); token.markup = markup + " " + type; token.map = [startLine, nextLine]; token = state.push("inline", "", 0); token.content = title; token.map = [startLine, state.line - 1]; token.children = []; token = state.push("admonition_title_close", "p", -1); token.markup = markup + " " + type; } state.md.block.tokenize(state, startLine + 1, nextLine); token = state.push("admonition_close", "div", -1); token.markup = state.src.slice(start, pos); token.block = true; state.parentType = oldParent; state.lineMax = oldLineMax; state.line = nextLine + (autoClosed ? 1 : 0); return true; }, { alt: ["paragraph", "reference", "blockquote", "list"] } ); md.renderer.rules["admonition_open"] = render; md.renderer.rules["admonition_title_open"] = render; md.renderer.rules["admonition_title_close"] = render; md.renderer.rules["admonition_close"] = render; }; const HeadingPlugin = (md, options) => { md.renderer.rules.heading_open = (tokens, idx) => { var _a; const token = tokens[idx]; const text = ((_a = tokens[idx + 1].children) == null ? void 0 : _a.reduce((p, c) => { return p + (["text", "code_inline", "math_inline"].includes(c.type) ? c.content || "" : ""); }, "")) || ""; const level = token.markup.length; options.headsRef.value.push({ text, level, line: token.map[0] }); if (token.map && token.level === 0) { token.attrSet( "id", options.mdHeadingId(text, level, options.headsRef.value.length) ); } return md.renderer.renderToken(tokens, idx, options); }; md.renderer.rules.heading_close = (tokens, idx, opts, _env, self) => { return self.renderToken(tokens, idx, opts); }; }; const codetabs = (md, _opts) => { const defaultRender = md.renderer.rules.fence, unescapeAll = md.utils.unescapeAll, re = /\[(\w*)(?::([\w ]*))?\]/, mandatoryRe = /::(open|close)/; const getInfo = (token) => { return token.info ? unescapeAll(token.info).trim() : ""; }; const getGroupAndTab = (token) => { const info = getInfo(token), [group = null, tab = ""] = (re.exec(info) || []).slice(1); return [group, tab]; }; const getLangName = (token) => { const info = getInfo(token); return info ? info.split(/(\s+)/g)[0] : ""; }; const getTagType = (token) => { const mandatory = token.info.match(mandatoryRe) || []; const open = mandatory[1] === "open" || mandatory[1] !== "close" && _opts.codeFoldable && token.content.trim().split("\n").length < _opts.autoFoldThreshold; const tagContainer = mandatory[1] || _opts.codeFoldable ? "details" : "div", tagHeader = mandatory[1] || _opts.codeFoldable ? "summary" : "div"; return { open, tagContainer, tagHeader }; }; const fenceGroup = (tokens, idx, options, env, slf) => { var _a; if (tokens[idx].hidden) { return ""; } const codeCodeText = (_a = _opts.usedLanguageTextRef.value) == null ? void 0 : _a.copyCode.text; const copyBtnHtml = _opts.customIconRef.value.copy || codeCodeText; const isIcon = !!_opts.customIconRef.value.copy; const collapseTips = `<span class="${config.prefix}-collapse-tips">${dom.StrIcon("collapse-tips", _opts.customIconRef.value)}</span>`; const [GROUP] = getGroupAndTab(tokens[idx]); if (GROUP === null) { const { open: open2, tagContainer: tagContainer2, tagHeader: tagHeader2 } = getTagType(tokens[idx]); const addAttrs2 = [["class", `${config.prefix}-code`]]; if (open2) addAttrs2.push(["open", ""]); const tmpToken2 = { attrs: mergeAttrs(tokens[idx], addAttrs2) }; tokens[idx].info = tokens[idx].info.replace(mandatoryRe, ""); const codeRendered = defaultRender(tokens, idx, options, env, slf); return ` <${tagContainer2} ${slf.renderAttrs(tmpToken2)}> <${tagHeader2} class="${config.prefix}-code-head"> <div class="${config.prefix}-code-flag"><span></span><span></span><span></span></div> <div class="${config.prefix}-code-action"> <span class="${config.prefix}-code-lang">${md.utils.escapeHtml(tokens[idx].info.trim())}</span> <span class="${config.prefix}-copy-button" data-tips="${codeCodeText}"${isIcon ? " data-is-icon=true" : ""}>${copyBtnHtml}</span> ${_opts.extraTools instanceof Function ? _opts.extraTools({ lang: tokens[idx].info.trim() }) : _opts.extraTools || ""} ${tagContainer2 === "details" ? collapseTips : ""} </div> </${tagHeader2}> ${codeRendered} </${tagContainer2}> `; } let token, group, tab, checked, labels = "", pres = "", langs = ""; const { open, tagContainer, tagHeader } = getTagType(tokens[idx]); const addAttrs = [["class", `${config.prefix}-code`]]; if (open) addAttrs.push(["open", ""]); const tmpToken = { attrs: mergeAttrs(tokens[idx], addAttrs) }; for (let i = idx; i < tokens.length; i++) { token = tokens[i]; [group, tab] = getGroupAndTab(token); if (group !== GROUP) { break; } token.info = token.info.replace(re, "").replace(mandatoryRe, ""); token.hidden = true; const className = `${config.prefix}-codetab-${_opts.editorId}-${idx}-${i - idx}`; checked = i - idx > 0 ? "" : "checked"; labels += ` <li> <input type="radio" id="label-${config.prefix}-codetab-label-1-${_opts.editorId}-${idx}-${i - idx}" name="${config.prefix}-codetab-label-${_opts.editorId}-${idx}" class="${className}" ${checked} > <label for="label-${config.prefix}-codetab-label-1-${_opts.editorId}-${idx}-${i - idx}" onclick="this.getRootNode().querySelectorAll('.${className}').forEach(e => e.click())" > ${md.utils.escapeHtml(tab || getLangName(token))} </label> </li>`; pres += ` <div role="tabpanel"> <input type="radio" name="${config.prefix}-codetab-pre-${_opts.editorId}-${idx}" class="${className}" ${checked} role="presentation"> ${defaultRender(tokens, i, options, env, slf)} </div>`; langs += ` <input type="radio" name="${config.prefix}-codetab-lang-${_opts.editorId}-${idx}" class="${className}" ${checked} role="presentation"> <span class=${config.prefix}-code-lang role="note">${md.utils.escapeHtml(getLangName(token))}</span>`; } return ` <${tagContainer} ${slf.renderAttrs(tmpToken)}> <${tagHeader} class="${config.prefix}-code-head"> <div class="${config.prefix}-code-flag"> <ul class="${config.prefix}-codetab-label" role="tablist">${labels}</ul> </div> <div class="${config.prefix}-code-action"> <span class="${config.prefix}-codetab-lang">${langs}</span> <span class="${config.prefix}-copy-button" data-tips="${codeCodeText}"${isIcon ? " data-is-icon=true" : ""}>${copyBtnHtml}</span> ${_opts.extraTools instanceof Function ? _opts.extraTools({ lang: tokens[idx].info.trim() }) : _opts.extraTools || ""} ${tagContainer === "details" ? collapseTips : ""} </div> </${tagHeader}> ${pres} </${tagContainer}> `; }; md.renderer.rules.fence = fenceGroup; md.renderer.rules.code_block = fenceGroup; }; const attrSet = (token, name, value) => { const index2 = token.attrIndex(name); const attr = [name, value]; if (index2 < 0) { token.attrPush(attr); } else { token.attrs = token.attrs || []; token.attrs[index2] = attr; } }; const isInline = (token) => { return token.type === "inline"; }; const isParagraph = (token) => { return token.type === "paragraph_open"; }; const isListItem = (token) => { return token.type === "list_item_open"; }; const startsWithTodoMarkdown = (token) => { return token.content.indexOf("[ ] ") === 0 || token.content.indexOf("[x] ") === 0 || token.content.indexOf("[X] ") === 0; }; const isTodoItem = (tokens, index2) => { return isInline(tokens[index2]) && isParagraph(tokens[index2 - 1]) && isListItem(tokens[index2 - 2]) && startsWithTodoMarkdown(tokens[index2]); }; const parentToken = (tokens, index2) => { const targetLevel = tokens[index2].level - 1; for (let i = index2 - 1; i >= 0; i--) { if (tokens[i].level === targetLevel) { return i; } } return -1; }; const beginLabel = (TokenConstructor) => { const token = new TokenConstructor("html_inline", "", 0); token.content = "<label>"; return token; }; const endLabel = (TokenConstructor) => { const token = new TokenConstructor("html_inline", "", 0); token.content = "</label>"; return token; }; const afterLabel = (content, id, TokenConstructor) => { const token = new TokenConstructor("html_inline", "", 0); token.content = '<label class="task-list-item-label" for="' + id + '">' + content + "</label>"; token.attrs = [{ for: id }]; return token; }; const makeCheckbox = (token, TokenConstructor, options) => { const checkbox = new TokenConstructor("html_inline", "", 0); const disabledAttr = !options.enabled ? ' disabled="" ' : " "; if (token.content.indexOf("[ ] ") === 0) { checkbox.content = '<input class="task-list-item-checkbox"' + disabledAttr + 'type="checkbox">'; } else if (token.content.indexOf("[x] ") === 0 || token.content.indexOf("[X] ") === 0) { checkbox.content = '<input class="task-list-item-checkbox" checked=""' + disabledAttr + 'type="checkbox">'; } return checkbox; }; const todoify = (token, TokenConstructor, options) => { token.children = token.children || []; token.children.unshift(makeCheckbox(token, TokenConstructor, options)); token.children[1].content = token.children[1].content.slice(3); token.content = token.content.slice(3); if (options.label) { if (options.labelAfter) { token.children.pop(); const id = "task-item-" + Math.ceil(Math.random() * (1e4 * 1e3) - 1e3); token.children[0].content = token.children[0].content.slice(0, -1) + ' id="' + id + '">'; token.children.push(afterLabel(token.content, id, TokenConstructor)); } else { token.children.unshift(beginLabel(TokenConstructor)); token.children.push(endLabel(TokenConstructor)); } } }; const githubTaskLists = (md, options = {}) => { md.core.ruler.after("inline", "github-task-lists", (state) => { const tokens = state.tokens; for (let i = 2; i < tokens.length; i++) { if (isTodoItem(tokens, i)) { todoify(tokens[i], state.Token, options); attrSet( tokens[i - 2], "class", "task-list-item" + (options.enabled ? " enabled" : " ") ); attrSet(tokens[parentToken(tokens, i - 2)], "class", "contains-task-list"); } } }); }; const initLineNumber = (md) => { md.core.ruler.push("init-line-number", (state) => { state.tokens.forEach((token) => { if (token.map) { if (!token.attrs) { token.attrs = []; } token.attrs.push(["data-line", token.map[0].toString()]); } }); return true; }); }; const useMarkdownIt = (props, previewOnly) => { const { editorConfig, markdownItConfig, markdownItPlugins, editorExtensions } = config.globalConfig; const editorId = vue.inject("editorId"); const languageRef = vue.inject("language"); const usedLanguageTextRef = vue.inject( "usedLanguageText" ); const showCodeRowNumber = vue.inject("showCodeRowNumber"); const themeRef = vue.inject("theme"); const customIconRef = vue.inject("customIcon"); const rootRef = vue.inject("rootRef"); const headsRef = vue.ref([]); let clearMermaidEvents = () => { }; const hljsRef = useHighlight(props); const katexRef = useKatex(props); const { reRenderRef, replaceMermaid } = useMermaid(props); const md = mdit({ html: true, breaks: true, linkify: true }); markdownItConfig(md, { editorId }); const plugins = [ { type: "image", plugin: ImageFiguresPlugin, options: { figcaption: true, classes: "md-zoom" } }, { type: "admonition", plugin: AdmonitionPlugin, options: {} }, { type: "taskList", plugin: githubTaskLists, options: {} }, { type: "heading", plugin: HeadingPlugin, options: { mdHeadingId: props.mdHeadingId, headsRef } }, { type: "code", plugin: codetabs, options: { editorId, usedLanguageTextRef, // showCodeRowNumber, codeFoldable: props.codeFoldable, autoFoldThreshold: props.autoFoldThreshold, customIconRef } }, { type: "sub", plugin: SubPlugin, options: {} }, { type: "sup", plugin: SupPlugin, options: {} } ]; if (!props.noKatex) { plugins.push({ type: "katex", plugin: KatexPlugin, options: { katexRef } }); } if (!props.noMermaid) { plugins.push({ type: "mermaid", plugin: MermaidPlugin, options: { themeRef } }); } markdownItPlugins(plugins, { editorId }).forEach((item) => { md.use(item.plugin, item.options); }); const userDefHighlight = md.options.highlight; md.set({ highlight: (str, language, attrs) => { if (userDefHighlight) { const result = userDefHighlight(str, language, attrs); if (result) { return result; } } let codeHtml; if (!props.noHighlight && hljsRef.value) { const hljsLang = hljsRef.value.getLanguage(language); if (hljsLang) { codeHtml = hljsRef.value.highlight(str, { language, ignoreIllegals: true }).value; } else { codeHtml = hljsRef.value.highlightAuto(str).value; } } else { codeHtml = md.utils.escapeHtml(str); } const codeSpan = showCodeRowNumber ? index.generateCodeRowNumber( codeHtml.replace(/^\n+|\n+$/g, ""), str.replace(/^\n+|\n+$/g, "") ) : `<span class="${config.prefix}-code-block">${codeHtml.replace(/^\n+|\n+$/g, "")}</span>`; return `<pre><code class="language-${language}" language=${language}>${codeSpan}</code></pre>`; } }); initLineNumber(md); const key = vue.ref(`_article-key_${util.randomId()}`); const html = vue.ref( props.sanitize( md.render(props.modelValue, { srcLines: props.modelValue.split("\n") }) ) ); const updatedTodo = () => { eventName.bus.emit(editorId, eventName.BUILD_FINISHED, html.value); props.onHtmlChanged(html.value); props.onGetCatalog(headsRef.value); eventName.bus.emit(editorId, eventName.CATALOG_CHANGED, headsRef.value); vue.nextTick(() => { replaceMermaid().then(() => { var _a, _b; if ((_a = editorExtensions.mermaid) == null ? void 0 : _a.enableZoom) { clearMermaidEvents(); clearMermaidEvents = dom.zoomMermaid( (_b = rootRef.value) == null ? void 0 : _b.querySelectorAll( `#${editorId} p.${config.prefix}-mermaid:not([data-closed=false])` ), { customIcon: customIconRef.value } ); } }); }); }; const markHtml = () => { headsRef.value = []; html.value = props.sanitize( md.render(props.modelValue, { srcLines: props.modelValue.split("\n") }) ); updatedTodo(); }; const needReRender = vue.computed(() => { return (props.noKatex || katexRef.value) && (props.noHighlight || hljsRef.value); }); let timer = -1; vue.watch([vue.toRef(props, "modelValue"), needReRender, reRenderRef, languageRef], () => { timer = window.setTimeout( () => { markHtml(); }, previewOnly ? 0 : editorConfig.renderDelay ); }); vue.watch( () => props.setting.preview, () => { if (props.setting.preview) { vue.nextTick(() => { replaceMermaid().then(() => { var _a, _b; if ((_a = editorExtensions.mermaid) == null ? void 0 : _a.enableZoom) { clearMermaidEvents(); clearMermaidEvents = dom.zoomMermaid( (_b = rootRef.value) == null ? void 0 : _b.querySelectorAll( `#${editorId} p.${config.prefix}-mermaid:not([data-closed=false])` ), { customIcon: customIconRef.value } ); } }); eventName.bus.emit(editorId, eventName.CATALOG_CHANGED, headsRef.value); }); } } ); vue.onMounted(updatedTodo); vue.onMounted(() => { eventName.bus.on(editorId, { name: eventName.PUSH_CATALOG, callback() { eventName.bus.emit(editorId, eventName.CATALOG_CHANGED, headsRef.value); } }); eventName.bus.on(editorId, { name: eventName.RERENDER, callback: () => { key.value = `_article-key_${util.randomId()}`; markHtml(); } }); }); vue.onBeforeUnmount(() => { clearMermaidEvents(); clearTimeout(timer); }); return { html, key }; }; const template = { checked: { regexp: /- \[x\]/, value: "- [ ]" }, unChecked: { regexp: /- \[\s\]/, value: "- [x]" } }; const useTaskState = (props, html) => { const editorId = vue.inject("editorId"); const rootRef = vue.inject("rootRef"); let removeListener = () => { }; const addListener = () => { if (!rootRef.value) { return false; } const tasks = rootRef.value.querySelectorAll(".task-list-item.enabled"); const listener = (e) => { var _a; e.preventDefault(); const nextValue = e.target.checked ? "unChecked" : "checked"; const line = (_a = e.target.parentElement) == null ? void 0 : _a.dataset.line; if (!line) { return; } const lineNumber = Number(line); const lines = props.modelValue.split("\n"); const targetValue = lines[Number(lineNumber)].replace( template[nextValue].regexp, template[nextValue].value ); if (props.previewOnly) { lines[Number(lineNumber)] = targetValue; props.onChange(lines.join("\n")); } else { eventName.bus.emit(editorId, eventName.TASK_STATE_CHANGED, lineNumber + 1, targetValue); } }; tasks.forEach((item) => { item.addEventListener("click", listener); }); removeListener = () => { tasks.forEach((item) => { item.removeEventListener("click", listener); }); }; }; vue.onBeforeUnmount(() => { removeListener(); }); vue.watch( [html], () => { removeListener(); vue.nextTick(addListener); }, { immediate: true } ); }; const useRemount = (props, html, key) => { const handler = () => { vue.nextTick(() => { var _a; (_a = props.onRemount) == null ? void 0 : _a.call(props); }); }; const settingPreviewChanged = (nVal) => { if (nVal) { handler(); } }; vue.watch([html, key], handler); vue.watch(() => props.setting.preview, settingPreviewChanged); vue.watch(() => props.setting.htmlPreview, settingPreviewChanged); vue.onMounted(handler); }; const contentPreviewProps = { modelValue: { type: String, default: "" }, onChange: { type: Function, default: () => { } }, setting: { type: Object, default: () => ({ preview: true }) }, onHtmlChanged: { type: Function, default: () => { } }, onGetCatalog: { type: Function, default: () => { } }, mdHeadingId: { type: Function, default: () => "" }, noMermaid: { type: Boolean, default: false }, sanitize: { type: Function, default: (html) => html }, // 不使用该函数功能 noKatex: { type: Boolean, default: false }, formatCopiedText: { type: Function, default: (text) => text }, noHighlight: { type: Boolean, default: false }, previewOnly: { type: Boolean, default: false }, noImgZoomIn: { type: Boolean }, sanitizeMermaid: { type: Function }, codeFoldable: { type: Boolean }, autoFoldThreshold: { type: Number }, onRemount: { type: Function } }; const contentProps = { ...contentPreviewProps, updateModelValue: { type: Function, default: () => { } }, placeholder: { type: String, default: "" }, scrollAuto: { type: Boolean }, autofocus: { type: Boolean }, disabled: { type: Boolean }, readonly: { type: Boolean }, maxlength: { type: Number }, autoDetectCode: { type: Boolean }, /** * 输入框失去焦点时触发事件 */ onBlur: { type: Function, default: () => { } }, /** * 输入框获得焦点时触发事件 */ onFocus: { type: Function, default: () => { } }, noPrettier: { type: Boolean }, completions: { type: Array }, catalogVisible: { type: Boolean }, theme: { type: String, default: "light" }, onInput: { type: Function }, onDrop: { type: Function, default: () => { } }, inputBoxWidth: { type: String }, oninputBoxWidthChange: { type: Function }, transformImgUrl: { type: Function, default: (t) => t }, catalogLayout: { type: String }, catalogMaxDepth: { type: Number } }; const splitNodes = (html) => { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); return Array.from(doc.body.childNodes); }; const compareHtml = (newNodes, currentNodes) => { const updates = []; const deletes = []; newNodes.forEach((newNode, index2) => { const currentNode = currentNodes[index2]; if (!currentNode) { updates.push({ index: index2, newNode }); return; } if (newNode.nodeType !== currentNode.nodeType || newNode.textContent !== currentNode.textContent || newNode.nodeType === 1 && newNode.outerHTML !== currentNode.outerHTML) { updates.push({ index: index2, newNode }); } }); if (currentNodes.length > newNodes.length) { for (let i = newNodes.length; i < currentNodes.length; i++) { deletes.push(currentNodes[i]); } } return { updates, deletes }; }; const UpdateOnDemand = /* @__PURE__ */ vue.defineComponent({ name: "UpdateOnDemand", props: { html: { type: String, required: true } }, setup(props) { const editorId = vue.inject("editorId"); const previewTheme = vue.inject("previewTheme"); const showCodeRowNumber = vue.inject("showCodeRowNumber"); const htmlContainer = vue.ref(); const firstHtml = props.html; const updateHtmlContent = (updates, deletes) => { if (!htmlContainer.value) return; deletes.forEach((node) => { node.remove(); }); updates.forEach(({ index: index2, newNode }) => { var _a, _b, _c; const targetNode = (_a = htmlContainer.value) == null ? void 0 : _a.childNodes[index2]; if (!targetNode) { (_b = htmlContainer.value) == null ? void 0 : _b.appendChild(newNode.cloneNode(true)); } else { (_c = htmlContainer.value) == null ? void 0 : _c.replaceChild(newNode.cloneNode(true), targetNode); } }); }; vue.watch(() => props.html, (newHtml) => { var _a; const newNodes = splitNodes(newHtml); const currentNodes = Array.from(((_a = htmlContainer.value) == null ? void 0 : _a.childNodes) || []); const { updates, deletes } = compareHtml(newNodes, currentNodes); updateHtmlContent(updates, deletes); }); return () => vue.createVNode("div", { "id": `${editorId}-preview`, "class": [`${config.prefix}-preview`, `${previewTheme == null ? void 0 : previewTheme.value}-theme`, showCodeRowNumber && `${config.prefix}-scrn`], "innerHTML": firstHtml, "ref": htmlContainer }, null); } }); const ContentPreview = /* @__PURE__ */ vue.defineComponent({ name: "ContentPreview", props: contentPreviewProps, setup(props) { const editorId = vue.inject("editorId"); const { html, key } = useMarkdownIt(props, props.previewOnly); useCopyCode(props, html, key); userZoom(props, html); useTaskState(props, html); useRemount(props, html, key); return () => { return vue.createVNode(vue.Fragment, null, [props.setting.preview && vue.createVNode("div", { "id": `${editorId}-preview-wrapper`, "class": `${config.prefix}-preview-wrapper`, "key": "content-preview-wrapper" }, [vue.createVNode(UpdateOnDemand, { "key": key.value, "html": html.value }, null)]), !props.previewOnly && props.setting.htmlPreview && vue.createVNode("div", { "id": `${editorId}-html-wrapper`, "class": `${config.prefix}-preview-wrapper`, "key": "html-preview-wrapper" }, [vue.createVNode("div", { "class": `${config.prefix}-html` }, [html.value])])]); }; } }); const useOnSave = (props, context, options) => { const { editorId } = options; const state = vue.reactive({ // 是否已编译成html buildFinished: false, // 存储当前最新的html html: "" }); vue.watch( () => props.modelValue, () => { state.buildFinished = false; } ); vue.onMounted(() => { eventName.bus.on(editorId, { name: eventName.BUILD_FINISHED, callback(html) { state.buildFinished = true; state.html = html; } }); eventName.bus.on(editorId, { name: eventName.ON_SAVE, callback() { const htmlPromise = new Promise((rev) => { if (state.buildFinished) { rev(state.html); } else { const buildFinishedCallback = (html) => { rev(html); eventName.bus.remove(editorId, eventName.BUILD_FINISHED, buildFinishedCallback); }; eventName.bus.on(editorId, { name: eventName.BUILD_FINISHED, callback: buildFinishedCallback }); } }); if (props.onSave) { props.onSave(props.modelValue, htmlPromise); } else { context.emit("onSave", props.modelValue, htmlPromise); } } }); }); }; const useProvidePreview = (props, rootRef) => { const hljsUrls = config.globalConfig.editorExtensions.highlight; const hljsAttrs = config.globalConfig.editorExtensionsAttrs.highlight; const editorId = useEditorId(props); vue.provide("editorId", editorId); vue.provide("rootRef", rootRef); vue.provide( "theme", vue.computed(() => props.theme) ); vue.provide( "language", vue.computed(() => props.language) ); vue.provide( "highlight", vue.computed(() => { const { js: jsUrl } = hljsUrls; const cssList = { ...config.codeCss, ...hljsUrls.css }; const { js: jsAttrs, css: cssAttrs = {} } = hljsAttrs || {}; const _theme = props.codeStyleReverse && props.codeStyleReverseList.includes(props.previewTheme) ? "dark" : props.theme; const codeCssHref = cssList[props.codeTheme] ? cssList[props.codeTheme][_theme] : config.codeCss.atom[_theme]; const codeCssAttrs = cssList[props.codeTheme] && cssAttrs[props.codeTheme] ? cssAttrs[props.codeTheme][_theme] : cssAttrs["atom"] ? cssAttrs["atom"][_theme] : {}; return { js: { src: jsUrl, ...jsAttrs }, css: { href: codeCssHref, ...codeCssAttrs } }; }) ); vue.provide("showCodeRowNumber", props.showCodeRowNumber); const usedLanguageText = vue.computed(() => { const allText = { ...config.staticTextDefault, ...config.globalConfig.editorConfig.languageUserDefined }; return util.deepMerge( util.deepClone(config.staticTextDefault["en-US"]), allText[props.language] || {} ); }); vue.provide("usedLanguageText", usedLanguageText); vue.provide( "previewTheme", vue.computed(() => props.previewTheme) ); vue.provide( "customIcon", vue.computed(() => props.customIcon) ); return { editorId }; }; const useProvide = (props, rootRef) => { vue.provide("tabWidth", props.tabWidth); vue.provide( "disabled", vue.computed(() => props.disabled) ); return useProvidePreview(props, rootRef); }; const useExpansion = (props) => { const { noPrettier, noUploadImg } = props; const { editorExtensions, editorExtensionsAttrs } = config.globalConfig; const noPrettierScript = noPrettier || editorExtensions.prettier.prettierInstance; const noParserMarkdownScript = noPrettier || editorExtensions.prettier.parserMarkdownInstance; const noCropperScript = noUploadImg || editorExtensions.cropper.instance; vue.onMounted(() => { if (!noCropperScript) { const { js = {}, css = {} } = editorExtensionsAttrs.cropper || {}; dom.appendHandler("link", { ...css, rel: "stylesheet", href: editorExtensions.cropper.css, id: CDN_IDS.croppercss }); dom.appendHandler("script", { ...js, src: editorExtensions.cropper.js, id: CDN_IDS.cropperjs }); } if (!noPrettierScript) { const { standaloneJs = {} } = editorExtensionsAttrs.prettier || {}; dom.appendHandler("script", { ...standaloneJs, src: editorExtensions.prettier.standaloneJs, id: CDN_IDS.prettier }); } if (!noParserMarkdownScript) { const { parserMarkdownJs = {} } = editorExtensionsAttrs.prettier || {}; dom.appendHandler("script", { ...parserMarkdownJs, src: editorExtensions.prettier.parserMarkdownJs, id: CDN_IDS.prettierMD }); } }); }; const useErrorCatcher = (props, context, options) => { const { editorId } = options; vue.onMounted(() => { eventName.bus.on(editorId, { name: eventName.ERROR_CATCHER, callback: (err) => { var _a; (_a = props.onError) == null ? void 0 : _a.call(props, err); context.emit("onError", err); } }); }); }; const useConfig = (props, context, options) => { const { editorId } = options; const setting = vue.reactive({ pageFullscreen: props.pageFullscreen, fullscreen: false, preview: props.preview, htmlPreview: props.preview ? false : props.htmlPreview, previewOnly: false }); const cacheSetting = vue.reactive({ ...setting }); const updateSetting = (k, v) => { const realValue = v === void 0 ? !setting[k] : v; switch (k) { case "preview": { setting.htmlPreview = false; setting.previewOnly = f