@rtdui/editor
Version:
React rich text editor based on tiptap
344 lines (342 loc) • 10.6 kB
JavaScript
//#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 };