Spaces:
Build error
Build error
| const DELIMITER_LIST = [ | |
| { left: '$$', right: '$$', display: true }, | |
| { left: '$', right: '$', display: false }, | |
| { left: '\\pu{', right: '}', display: false }, | |
| { left: '\\ce{', right: '}', display: false }, | |
| { left: '\\(', right: '\\)', display: false }, | |
| { left: '\\[', right: '\\]', display: true }, | |
| { left: '\\begin{equation}', right: '\\end{equation}', display: true } | |
| ]; | |
| // Defines characters that are allowed to immediately precede or follow a math delimiter. | |
| const ALLOWED_SURROUNDING_CHARS = | |
| '\\s。,、、;;„“‘’“”()「」『』[]《》【】‹›«»…⋯::?!~⇒?!-\\/:-@\\[-`{-~\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}'; | |
| // Modified to fit more formats in different languages. Originally: '\\s?。,、;!-\\/:-@\\[-`{-~\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}'; | |
| // Pre-compile the surrounding character regex once at module load time. | |
| // This regex uses Unicode property escapes (\p{Script=Han}, etc.) which are | |
| // extremely expensive to compile - doing so on every call caused ~87% of | |
| // markdown rendering time to be spent in KaTeX regex compilation. | |
| const ALLOWED_SURROUNDING_CHARS_REGEX = new RegExp(`[${ALLOWED_SURROUNDING_CHARS}]`, 'u'); | |
| // const DELIMITER_LIST = [ | |
| // { left: '$$', right: '$$', display: false }, | |
| // { left: '$', right: '$', display: false }, | |
| // ]; | |
| // const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/; | |
| // const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/; | |
| const inlinePatterns = []; | |
| const blockPatterns = []; | |
| function escapeRegex(string) { | |
| return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); | |
| } | |
| function generateRegexRules(delimiters) { | |
| delimiters.forEach((delimiter) => { | |
| const { left, right, display } = delimiter; | |
| // Ensure regex-safe delimiters | |
| const escapedLeft = escapeRegex(left); | |
| const escapedRight = escapeRegex(right); | |
| if (!display) { | |
| // For inline delimiters, we match everything | |
| inlinePatterns.push(`${escapedLeft}((?:\\\\[^]|[^\\\\])+?)${escapedRight}`); | |
| } else { | |
| // Block delimiters doubles as inline delimiters when not followed by a newline | |
| inlinePatterns.push(`${escapedLeft}(?!\\n)((?:\\\\[^]|[^\\\\])+?)(?!\\n)${escapedRight}`); | |
| blockPatterns.push(`${escapedLeft}\\n((?:\\\\[^]|[^\\\\])+?)\\n${escapedRight}`); | |
| } | |
| }); | |
| // Math formulas can end in special characters | |
| const inlineRule = new RegExp( | |
| `^(${inlinePatterns.join('|')})(?=[${ALLOWED_SURROUNDING_CHARS}]|$)`, | |
| 'u' | |
| ); | |
| const blockRule = new RegExp( | |
| `^(${blockPatterns.join('|')})(?=[${ALLOWED_SURROUNDING_CHARS}]|$)`, | |
| 'u' | |
| ); | |
| return { inlineRule, blockRule }; | |
| } | |
| const { inlineRule, blockRule } = generateRegexRules(DELIMITER_LIST); | |
| const isAllowedTrailing = (src: string, i: number): boolean => | |
| i >= src.length || ALLOWED_SURROUNDING_CHARS_REGEX.test(src.charAt(i)); | |
| const isBlockBoundary = (src: string, i: number): boolean => | |
| /^(?:[ \t]*\r?\n|$)/.test(src.slice(i)); | |
| const findClosingDelimiter = (src: string, i: number): number => | |
| i >= src.length - 1 | |
| ? -1 | |
| : src[i] === '\\' | |
| ? findClosingDelimiter(src, i + 2) | |
| : src[i] === '$' && src[i + 1] === '$' | |
| ? i | |
| : findClosingDelimiter(src, i + 1); | |
| export const tokenizeDisplayMath = ( | |
| src: string, | |
| type: 'inlineKatex' | 'blockKatex', | |
| requireBlockBoundary = false | |
| ) => { | |
| if (!src.startsWith('$$')) return; | |
| const endIndex = findClosingDelimiter(src, 2); | |
| if (endIndex === -1) return; | |
| const raw = src.slice(0, endIndex + 2); | |
| const text = raw.slice(2, -2); | |
| const afterClose = endIndex + 2; | |
| const validators: Array<() => boolean> = [ | |
| () => text.trim().length > 0, | |
| () => isAllowedTrailing(src, afterClose), | |
| () => !requireBlockBoundary || isBlockBoundary(src, afterClose) | |
| ]; | |
| return validators.every((v) => v()) ? { type, raw, text, displayMode: true } : undefined; | |
| }; | |
| export default function (options = {}) { | |
| return { | |
| extensions: [inlineKatex(options), blockKatex(options)] | |
| }; | |
| } | |
| function katexStart(src, displayMode: boolean) { | |
| for (let i = 0; i < src.length; i++) { | |
| const ch = src.charCodeAt(i); | |
| if (ch === 36 /* $ */) { | |
| // Display mode requires $$, skip single $ for display | |
| if (displayMode && src.charAt(i + 1) !== '$') { | |
| continue; | |
| } | |
| if (i === 0 || ALLOWED_SURROUNDING_CHARS_REGEX.test(src.charAt(i - 1))) { | |
| return i; | |
| } | |
| } else if (ch === 92 /* \ */) { | |
| const next = src.charAt(i + 1); | |
| // Only consider \ if followed by a valid math delimiter start | |
| if (displayMode) { | |
| // Display: \[ or \begin{equation} | |
| if (next !== '[' && next !== 'b') continue; | |
| } else { | |
| // Inline: \( or \ce{ or \pu{ | |
| if (next !== '(' && next !== 'c' && next !== 'p') continue; | |
| } | |
| if (i === 0 || ALLOWED_SURROUNDING_CHARS_REGEX.test(src.charAt(i - 1))) { | |
| return i; | |
| } | |
| } | |
| } | |
| } | |
| function katexTokenizer(src, tokens, displayMode: boolean) { | |
| if (src.startsWith('$$')) { | |
| const displayToken = tokenizeDisplayMath( | |
| src, | |
| displayMode ? 'blockKatex' : 'inlineKatex', | |
| displayMode | |
| ); | |
| if (displayToken) { | |
| return displayToken; | |
| } | |
| } | |
| const ruleReg = displayMode ? blockRule : inlineRule; | |
| const type = displayMode ? 'blockKatex' : 'inlineKatex'; | |
| const match = src.match(ruleReg); | |
| if (match) { | |
| const text = match | |
| .slice(2) | |
| .filter((item) => item) | |
| .find((item) => item.trim()); | |
| return { | |
| type, | |
| raw: match[0], | |
| text: text, | |
| displayMode | |
| }; | |
| } | |
| } | |
| function inlineKatex(options) { | |
| return { | |
| name: 'inlineKatex', | |
| level: 'inline', | |
| start(src) { | |
| return katexStart(src, false); | |
| }, | |
| tokenizer(src, tokens) { | |
| return katexTokenizer(src, tokens, false); | |
| }, | |
| renderer(token) { | |
| return `${token?.text ?? ''}`; | |
| } | |
| }; | |
| } | |
| function blockKatex(options) { | |
| return { | |
| name: 'blockKatex', | |
| level: 'block', | |
| start(src) { | |
| return katexStart(src, true); | |
| }, | |
| tokenizer(src, tokens) { | |
| return katexTokenizer(src, tokens, true); | |
| }, | |
| renderer(token) { | |
| return `${token?.text ?? ''}`; | |
| } | |
| }; | |
| } | |