mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
177 lines
5.3 KiB
TypeScript
177 lines
5.3 KiB
TypeScript
import type { Options as ReactMarkdownOptions } from "react-markdown";
|
|
import rehypeKatex from "rehype-katex";
|
|
import rehypeRaw from "rehype-raw";
|
|
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
import remarkGfm from "remark-gfm";
|
|
import remarkMath from "remark-math";
|
|
|
|
const markdownSanitizeSchema = {
|
|
...defaultSchema,
|
|
attributes: {
|
|
...defaultSchema.attributes,
|
|
code: [["className", /^language-./, "math-inline", "math-display"]],
|
|
},
|
|
strip: [...(defaultSchema.strip || []), "iframe", "object", "style", "form"],
|
|
};
|
|
|
|
export function normalizeDisplayMath(markdown: string): string {
|
|
const lineBreak = markdown.includes("\r\n") ? "\r\n" : "\n";
|
|
const lines = markdown.split(/\r?\n/);
|
|
const normalized: string[] = [];
|
|
let fence: { marker: string; size: number } | null = null;
|
|
let inlineCodeMarkerSize = 0;
|
|
let rawCodeTag: string | null = null;
|
|
|
|
for (let index = 0; index < lines.length; index++) {
|
|
const line = lines[index];
|
|
|
|
if (rawCodeTag) {
|
|
normalized.push(line);
|
|
if (new RegExp(`</${rawCodeTag}\\s*>`, "i").test(line)) rawCodeTag = null;
|
|
continue;
|
|
}
|
|
|
|
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
if (fenceMatch) {
|
|
const marker = fenceMatch[1][0];
|
|
const size = fenceMatch[1].length;
|
|
if (!fence) fence = { marker, size };
|
|
else if (marker === fence.marker && size >= fence.size) fence = null;
|
|
inlineCodeMarkerSize = 0;
|
|
normalized.push(line);
|
|
continue;
|
|
}
|
|
|
|
if (fence) {
|
|
normalized.push(line);
|
|
continue;
|
|
}
|
|
|
|
const rawCodeOpen = line.match(/<(code|pre|script|style)\b/i);
|
|
if (rawCodeOpen) {
|
|
const tag = rawCodeOpen[1].toLowerCase();
|
|
const remainder = line.slice((rawCodeOpen.index ?? 0) + rawCodeOpen[0].length);
|
|
if (!new RegExp(`</${tag}\\s*>`, "i").test(remainder)) rawCodeTag = tag;
|
|
inlineCodeMarkerSize = 0;
|
|
normalized.push(line);
|
|
continue;
|
|
}
|
|
|
|
if (/^(?: {4}|\t)/.test(line) || line.trim() === "") {
|
|
inlineCodeMarkerSize = 0;
|
|
normalized.push(line);
|
|
continue;
|
|
}
|
|
|
|
if (inlineCodeMarkerSize || line.includes("`")) {
|
|
inlineCodeMarkerSize = updateInlineCodeMarker(line, inlineCodeMarkerSize);
|
|
normalized.push(line);
|
|
continue;
|
|
}
|
|
|
|
const bracketDisplayOneLine = line.match(/^([ ]{0,3})\\\[[ \t]*(.+?)[ \t]*\\\][ \t]*$/);
|
|
if (bracketDisplayOneLine) {
|
|
const math = bracketDisplayOneLine[2].trim();
|
|
if (math) {
|
|
normalized.push(`${bracketDisplayOneLine[1]}$$`, math, `${bracketDisplayOneLine[1]}$$`);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const bracketDisplayStart = line.match(/^([ ]{0,3})\\\[[ \t]*$/);
|
|
if (bracketDisplayStart) {
|
|
const closingIndex = findBracketDisplayClose(lines, index + 1);
|
|
if (closingIndex !== -1) {
|
|
normalized.push(
|
|
`${bracketDisplayStart[1]}$$`,
|
|
...lines.slice(index + 1, closingIndex),
|
|
`${bracketDisplayStart[1]}$$`,
|
|
);
|
|
index = closingIndex;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const displayMathMatch = line.match(/^([ \t]{0,3})\$\$(.+)\$\$[ \t]*$/);
|
|
if (displayMathMatch) {
|
|
const math = displayMathMatch[2].trim();
|
|
if (math) {
|
|
normalized.push(`${displayMathMatch[1]}$$`, math, `${displayMathMatch[1]}$$`);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
normalized.push(normalizeInlineLatexMath(line));
|
|
}
|
|
|
|
return normalized.join(lineBreak);
|
|
}
|
|
|
|
function findBracketDisplayClose(lines: string[], startIndex: number): number {
|
|
for (let index = startIndex; index < lines.length; index++) {
|
|
const line = lines[index];
|
|
if (/^ {0,3}\\\][ \t]*$/.test(line)) return index;
|
|
|
|
// Do not pair delimiters across another Markdown block boundary.
|
|
if (
|
|
/^ {0,3}(`{3,}|~{3,})/.test(line) ||
|
|
/^ {0,3}\\\[[ \t]*$/.test(line) ||
|
|
/<(code|pre|script|style)\b/i.test(line)
|
|
) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
function updateInlineCodeMarker(line: string, initialMarkerSize: number): number {
|
|
let markerSize = initialMarkerSize;
|
|
for (let cursor = 0; cursor < line.length;) {
|
|
if (line[cursor] !== "`") {
|
|
cursor++;
|
|
continue;
|
|
}
|
|
|
|
let end = cursor + 1;
|
|
while (line[end] === "`") end++;
|
|
const runSize = end - cursor;
|
|
if (markerSize === 0) markerSize = runSize;
|
|
else if (runSize === markerSize) markerSize = 0;
|
|
cursor = end;
|
|
}
|
|
return markerSize;
|
|
}
|
|
|
|
function normalizeInlineLatexMath(line: string): string {
|
|
if (
|
|
/^\s{0,3}\[[^\]]+\]:/.test(line) ||
|
|
/]\s*\(/.test(line) ||
|
|
/<(?:!--|\/?[A-Za-z][^>]*>)/.test(line) ||
|
|
/\b(?:https?|file|mailto):/i.test(line) ||
|
|
/\b[A-Za-z]:\\/.test(line)
|
|
) {
|
|
return line;
|
|
}
|
|
|
|
return line.replace(
|
|
/(?<!\\)\\\(([^`\r\n$]+?)(?<!\\)\\\)/g,
|
|
(match, math: string) => (math.trim() ? `$${math}$` : match),
|
|
);
|
|
}
|
|
|
|
export const markdownRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [remarkGfm, remarkMath];
|
|
export const markdownPreviewRemarkPlugins: ReactMarkdownOptions["remarkPlugins"] = [remarkGfm, remarkMath];
|
|
|
|
export const markdownRehypePlugins: ReactMarkdownOptions["rehypePlugins"] = [
|
|
rehypeRaw,
|
|
[rehypeSanitize, markdownSanitizeSchema],
|
|
[rehypeKatex, { throwOnError: false, strict: false }],
|
|
];
|
|
|
|
export const markdownPreviewRehypePlugins: ReactMarkdownOptions["rehypePlugins"] = [
|
|
rehypeRaw,
|
|
[rehypeSanitize, markdownSanitizeSchema],
|
|
[rehypeKatex, { throwOnError: false, strict: false }],
|
|
];
|