源码
packages/ui/src/components/markdown-math.ts
// Dollar-delimiter normalization adapted from ZCode; see licenses/third-party-notices.txt.
import { type MarkdownFence, markdownFence } from "../markdown-fence.ts";
const likelyMathSyntaxPattern = /[\\{}^_=+\-*/<>|()[\]∇∂∫∑√∞≈≠≤≥±×÷πΠα-ωΑ-Ω]/u;
const texCommandPattern = /\\[A-Za-z]+/;
const simpleMathIdentifierPattern = /^(?:[A-Za-z]|[a-z][A-Za-z0-9]{1,2}|\d+(?:\.\d+)?)$/;
const compactCurrencyRangePrefixPattern = /^(?:\d[\d,]*(?:\.\d+)?|\.\d+)[+\-*/]$/;
const compactCurrencyAmountStartPattern = /^(?:\d|\.\d)/;
function isEscapedMarkdownCharacter(text: string, index: number): boolean {
let slashCount = 0;
for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor--) {
slashCount++;
}
return slashCount % 2 === 1;
}
function isSingleDollarDelimiter(text: string, index: number): boolean {
return (
text[index] === "$" &&
text[index - 1] !== "$" &&
text[index + 1] !== "$" &&
!isEscapedMarkdownCharacter(text, index)
);
}
function findClosingSingleDollarDelimiter(text: string, startIndex: number): number {
for (let index = startIndex; index < text.length; index++) {
if (isSingleDollarDelimiter(text, index)) {
return index;
}
}
return -1;
}
function isLikelySingleDollarMath(content: string): boolean {
if (!content || content !== content.trim() || /[\r\n]/.test(content)) {
return false;
}
if (texCommandPattern.test(content) || likelyMathSyntaxPattern.test(content)) {
return true;
}
if (!/\s/.test(content) && simpleMathIdentifierPattern.test(content)) {
return true;
}
return false;
}
function isLikelyCompactCurrencyRangeText(text: string, closingIndex: number, content: string): boolean {
if (!compactCurrencyRangePrefixPattern.test(content)) {
return false;
}
return compactCurrencyAmountStartPattern.test(text.slice(closingIndex + 1));
}
function normalizeSingleDollarMathInText(text: string): string {
if (!text.includes("$")) {
return text;
}
let output = "";
for (let index = 0; index < text.length; index++) {
if (!isSingleDollarDelimiter(text, index)) {
output += text[index];
continue;
}
const closingIndex = findClosingSingleDollarDelimiter(text, index + 1);
if (closingIndex === -1) {
output += text[index];
continue;
}
const content = text.slice(index + 1, closingIndex);
if (isLikelyCompactCurrencyRangeText(text, closingIndex, content)) {
// `$5-$10` 这类紧凑价格区间的第二个 `$` 会被误当成公式闭合符。
// 只转义当前 `$`,让整段继续按普通文本渲染并保留美元符号。
output += "\\$";
continue;
}
if (isLikelySingleDollarMath(content)) {
output += text.slice(index, closingIndex + 1);
index = closingIndex;
continue;
}
// 开启 singleDollarTextMath 后,`$5 ... $10` / `$HOME ... $PATH`
// 这类普通文本会被误当成公式。只转义当前 `$`,让后续 `$` 继续按原文本扫描。
output += "\\$";
}
return output;
}
function normalizeSingleDollarMathOutsideInlineCode(line: string): string {
let output = "";
let cursor = 0;
while (cursor < line.length) {
const codeStart = line.indexOf("`", cursor);
if (codeStart === -1) {
output += normalizeSingleDollarMathInText(line.slice(cursor));
break;
}
output += normalizeSingleDollarMathInText(line.slice(cursor, codeStart));
let codeFenceEnd = codeStart + 1;
while (line[codeFenceEnd] === "`") {
codeFenceEnd++;
}
const codeMarker = line.slice(codeStart, codeFenceEnd);
const codeEnd = line.indexOf(codeMarker, codeFenceEnd);
if (codeEnd === -1) {
output += normalizeSingleDollarMathInText(line.slice(codeStart));
break;
}
output += line.slice(codeStart, codeEnd + codeMarker.length);
cursor = codeEnd + codeMarker.length;
}
return output;
}
export function normalizeMath(markdown: string): string {
if (!markdown.includes("$")) {
return markdown;
}
let output = "";
let cursor = 0;
let activeFence: MarkdownFence | null = null;
while (cursor < markdown.length) {
const newlineIndex = markdown.indexOf("\n", cursor);
const lineEnd = newlineIndex === -1 ? markdown.length : newlineIndex;
const line = markdown.slice(cursor, lineEnd);
const newline = newlineIndex === -1 ? "" : "\n";
const fence = markdownFence(line, activeFence);
output +=
activeFence || fence || /^(?: {4}|\t)/.test(line)
? line + newline
: normalizeSingleDollarMathOutsideInlineCode(line) + newline;
if (fence) activeFence = activeFence ? null : fence;
cursor = lineEnd + newline.length;
}
return output;
}