UNPKG

@rtdui/editor

Version:

React rich text editor based on tiptap

344 lines (342 loc) 10.6 kB
//#region packages/editor/src/RichTextEditor/tiptap_extensions/extension-markdown-paste/markdown-it-katex.ts function isWhitespace(char) { return /^\s$/u.test(char); } function isWordCharacterOrNumber(char) { return /^[\w\d]$/u.test(char); } /** * Test if potential opening or closing delimiter */ function isValidInlineDelim(state, pos) { const prevChar = state.src[pos - 1]; const char = state.src[pos]; const nextChar = state.src[pos + 1]; if (char !== "$") return { can_open: false, can_close: false }; let canOpen = false; let canClose = false; if (prevChar !== "$" && prevChar !== "\\" && (prevChar === void 0 || isWhitespace(prevChar) || !isWordCharacterOrNumber(prevChar))) canOpen = true; if (nextChar !== "$" && (nextChar === void 0 || isWhitespace(nextChar) || !isWordCharacterOrNumber(nextChar))) canClose = true; return { can_open: canOpen, can_close: canClose }; } function isValidBlockDelim(state, pos) { const prevChar = state.src[pos - 1]; const char = state.src[pos]; const nextChar = state.src[pos + 1]; const nextCharPlus1 = state.src[pos + 2]; if (char === "$" && prevChar !== "$" && prevChar !== "\\" && nextChar === "$" && nextCharPlus1 !== "$") return { can_open: true, can_close: true }; return { can_open: false, can_close: false }; } /** * $...$ 包裹的内联Tetex数学公式 * @param state * @param silent * @returns */ function inlineMath(state, silent) { if (state.src[state.pos] !== "$") return false; const lastToken = state.tokens.at(-1); if (lastToken?.type === "html_inline") { if (/^<\w+.+[^/]>$/.test(lastToken.content)) return false; } let res = isValidInlineDelim(state, state.pos); if (!res.can_open) { if (!silent) state.pending += "$"; state.pos += 1; return true; } const start = state.pos + 1; let match = start; let pos; while ((match = state.src.indexOf("$", match)) !== -1) { pos = match - 1; while (state.src[pos] === "\\") pos -= 1; if ((match - pos) % 2 === 1) break; match += 1; } if (match === -1) { if (!silent) state.pending += "$"; state.pos = start; return true; } if (match - start === 0) { if (!silent) state.pending += "$$"; state.pos = start + 1; return true; } res = isValidInlineDelim(state, match); if (!res.can_close) { if (!silent) state.pending += "$"; state.pos = start; return true; } if (!silent) { const token = state.push("math_inline", "math", 0); token.markup = "$"; token.content = state.src.slice(start, match); } state.pos = match + 1; return true; } function blockMath(state, start, end, silent) { let lastLine; let next; let lastPos; let found = false; let pos = state.bMarks[start] + state.tShift[start]; let max = state.eMarks[start]; if (pos + 2 > max) return false; if (state.src.slice(pos, pos + 2) !== "$$") return false; pos += 2; let firstLine = state.src.slice(pos, max); if (silent) return true; if (firstLine.trim().slice(-2) === "$$") { firstLine = firstLine.trim().slice(0, -2); found = true; } for (next = start; !found;) { next += 1; 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(-2) === "$$") { lastPos = state.src.slice(0, max).lastIndexOf("$$"); lastLine = state.src.slice(pos, lastPos); found = true; } else if (state.src.slice(pos, max).trim().includes("$$")) { lastPos = state.src.slice(0, max).trim().indexOf("$$"); 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?.trim() ? `${firstLine}\n` : "") + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine?.trim() ? lastLine : ""); token.map = [start, state.line]; token.markup = "$$"; return true; } function blockBareMath(state, start, end, silent) { let lastLine; let found = false; let pos = state.bMarks[start] + state.tShift[start]; let max = state.eMarks[start]; const firstLine = state.src.slice(pos, max); if (!/^\\begin/.test(firstLine)) return false; if (start > 0) { const previousStart = state.bMarks[start - 1] + state.tShift[start - 1]; const previousEnd = state.eMarks[start - 1]; const previousLine = state.src.slice(previousStart, previousEnd); if (!/^\s*$/.test(previousLine)) return false; } if (silent) return true; let next = start; if (!/\\end[{}\w]*\s*$/.test(firstLine)) { let nestingCount = 0; while (!found) { next += 1; if (next >= end) break; pos = state.bMarks[next] + state.tShift[next]; max = state.eMarks[next]; if (pos < max && state.tShift[next] < state.blkIndent) break; const line = state.src.slice(pos, max); if (/\\begin/.test(line)) nestingCount += 1; else if (/\\end/.test(line)) { nestingCount -= 1; if (nestingCount < 0) { const lastPos = max; 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?.trim() ? `${firstLine}\n` : "") + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine?.trim() ? lastLine : ""); token.map = [start, state.line]; token.markup = "$$"; return true; } /** * $$...$$ 包裹的块级Tetex数学公式规则 * @param state * @param silent * @returns */ function inlineMathBlock(state, silent) { let match; let token; let res; let pos; if (state.src.slice(state.pos, state.pos + 2) !== "$$") return false; res = isValidBlockDelim(state, state.pos); if (!res.can_open) { if (!silent) state.pending += "$$"; state.pos += 2; return true; } const start = state.pos + 2; match = start; while ((match = state.src.indexOf("$$", match)) !== -1) { pos = match - 1; while (state.src[pos] === "\\") pos -= 1; if ((match - pos) % 2 === 1) break; match += 2; } if (match === -1) { if (!silent) state.pending += "$$"; state.pos = start; return true; } if (match - start === 0) { if (!silent) state.pending += "$$$$"; state.pos = start + 2; return true; } res = isValidBlockDelim(state, match); if (!res.can_close) { if (!silent) state.pending += "$$"; state.pos = start; return true; } if (!silent) { token = state.push("math_block", "math", 0); token.block = true; token.markup = "$$"; token.content = state.src.slice(start, match); } state.pos = match + 2; return true; } /** * \begin{Env} ... \end{Env}包裹的Tatex数学公式规则 * @param state * @param silent * @returns */ function inlineBareBlock(state, silent) { const text = state.src.slice(state.pos); if (!/^\n\\begin/.test(text)) return false; state.pos += 1; if (silent) return true; const lines = text.split(/\n/g).slice(1); const beginRe = /^\\begin/; const endRe = /^\\end/; let nestingCount = 0; let foundLine; for (let i = 0; i < lines.length; ++i) { const line = lines[i]; if (beginRe.test(line)) nestingCount += 1; else if (endRe.test(line)) { nestingCount -= 1; if (nestingCount <= 0) { foundLine = i; break; } } } if (typeof foundLine === "undefined") return false; const endIndex = lines.slice(0, foundLine + 1).reduce((p, c) => p + c.length, 0) + foundLine + 1; if (!silent) { const token = state.push("math_inline_bare_block", "math", 0); token.block = true; token.markup = "$$"; token.content = text.slice(1, endIndex); } state.pos += endIndex; return true; } function handleMathInHtml(state, mathType, mathMarkup, mathRegex) { const { tokens } = state; for (let index = tokens.length - 1; index >= 0; index--) { const currentToken = tokens[index]; const newTokens = []; if (currentToken.type !== "html_block") continue; const { content } = currentToken; for (const match of content.matchAll(mathRegex)) { if (!match.groups) continue; const { html_before_math, math, html_after_math } = match.groups; if (html_before_math) newTokens.push({ ...currentToken, type: "html_block", map: null, content: html_before_math }); if (math) newTokens.push({ ...currentToken, type: mathType, map: null, content: math, markup: mathMarkup, block: true, tag: "math" }); if (html_after_math) newTokens.push({ ...currentToken, type: "html_block", map: null, content: html_after_math }); } if (newTokens.length > 0) tokens.splice(index, 1, ...newTokens); } return true; } function markdownItKatex(md, options) { options = options || {}; const { enableBareBlocks, enableMathBlockInHtml, enableMathInlineInHtml } = options; const katexInline = (markup, latex) => { return String.raw`${markup}${latex}${markup}`; }; const inlineRenderer = (tokens, idx) => { return katexInline(tokens[idx].markup, tokens[idx].content); }; const katexBlockRenderer = (markup, latex) => { return String.raw`<div data-type="mathKatexBlock">${latex}</div>`; }; const blockRenderer = (tokens, idx) => { return `${katexBlockRenderer(tokens[idx].markup, tokens[idx].content)}\n`; }; md.inline.ruler.after("escape", "math_inline", inlineMath); md.inline.ruler.after("escape", "math_inline_block", inlineMathBlock); if (enableBareBlocks) md.inline.ruler.before("text", "math_inline_bare_block", inlineBareBlock); md.block.ruler.after("blockquote", "math_block", (state, start, end, silent) => { if (enableBareBlocks && blockBareMath(state, start, end, silent)) return true; return blockMath(state, start, end, silent); }, { alt: [ "paragraph", "reference", "blockquote", "list" ] }); const math_block_within_html_regex = /(?<html_before_math>[\s\S]*?)\$\$(?<math>[\s\S]+?)\$\$(?<html_after_math>(?:(?!\$\$[\s\S]+?\$\$)[\s\S])*)/gm; const math_inline_within_html_regex = /(?<html_before_math>[\s\S]*?)\$(?<math>.*?)\$(?<html_after_math>(?:(?!\$.*?\$)[\s\S])*)/gm; if (enableMathBlockInHtml) md.core.ruler.push("math_block_in_html_block", (state) => { return handleMathInHtml(state, "math_block", "$$", math_block_within_html_regex); }); if (enableMathInlineInHtml) md.core.ruler.push("math_inline_in_html_block", (state) => { return handleMathInHtml(state, "math_inline", "$", math_inline_within_html_regex); }); md.renderer.rules.math_inline = inlineRenderer; md.renderer.rules.math_inline_block = blockRenderer; md.renderer.rules.math_inline_bare_block = blockRenderer; md.renderer.rules.math_block = blockRenderer; } //#endregion export { markdownItKatex as default };