返回 DeepSeek-Reasonix
remarkMathPolicy.ts
根目录 / desktop / frontend / src / components / remarkMathPolicy.ts
1 import type { Root, Text } from "mdast";
2 import type { InlineMath } from "mdast-util-math";
3 import { visit } from "unist-util-visit";
4 import { classifyInlineMath } from "./mathClassify";
5 import { latexNormalizeForKatex } from "./latexNormalize";
6 import {
7 resolveProtectedInlineMathSource,
8 restoreProtectedInlineMathSource,
9 } from "./mathNormalize";
10
11 type HastLikeNode = {
12 type: string;
13 value?: string;
14 children?: HastLikeNode[];
15 };
16
17 type MathNodeData = {
18 hChildren?: HastLikeNode[];
19 hProperties?: Record<string, unknown>;
20 };
21
22 type ParentWithChildren = {
23 children: Array<{ type: string; value?: unknown; position?: unknown }>;
24 };
25
26 type VFileLike = {
27 value: unknown;
28 };
29
30 function originalSource(node: InlineMath, file: VFileLike): string {
31 const source = String(file.value ?? "");
32 const start = node.position?.start.offset;
33 const end = node.position?.end.offset;
34 if (typeof start === "number" && typeof end === "number") {
35 const span = source.slice(start, end);
36 if (span.startsWith("$") && span.endsWith("$")) {
37 return `$${restoreProtectedInlineMathSource(span.slice(1, -1))}$`;
38 }
39 return restoreProtectedInlineMathSource(span);
40 }
41 return `$${restoreProtectedInlineMathSource(node.value)}$`;
42 }
43
44 function inlineMathSources(node: InlineMath, file: VFileLike): {
45 source: string;
46 rendered: string;
47 } {
48 const parsed = resolveProtectedInlineMathSource(node.value);
49 const fileSource = String(file.value ?? "");
50 const start = node.position?.start.offset;
51 const end = node.position?.end.offset;
52 if (typeof start !== "number" || typeof end !== "number") return parsed;
53
54 const span = fileSource.slice(start, end);
55 if (!span.startsWith("$") || !span.endsWith("$")) return parsed;
56 return {
57 source: resolveProtectedInlineMathSource(span.slice(1, -1)).source,
58 rendered: parsed.rendered,
59 };
60 }
61
62 function setMathValue(
63 node: { value: string; data?: unknown },
64 source: string,
65 value: string,
66 ): void {
67 node.value = value;
68
69 // mdast-util-math caches hast children while parsing. Keep that rendering
70 // payload in sync with node.value when a later remark plugin normalises it,
71 // while carrying the unnormalised TeX through to the rehype stage.
72 const data = node.data as MathNodeData | undefined;
73 if (data) {
74 data.hProperties ??= {};
75 data.hProperties.dataLatexSource = source;
76 }
77 const hChildren = data?.hChildren;
78 const updateFirstText = (children: HastLikeNode[] | undefined): boolean => {
79 if (!children) return false;
80 for (const child of children) {
81 if (child.type === "text") {
82 child.value = value;
83 return true;
84 }
85 if (updateFirstText(child.children)) return true;
86 }
87 return false;
88 };
89 updateFirstText(hChildren);
90 }
91
92 /**
93 * Semantic policy layered after remark-math has parsed Markdown boundaries.
94 * Code spans/fences never become math nodes, so this plugin only decides how
95 * already-parsed inline math should render.
96 */
97 export function remarkMathPolicy() {
98 return (tree: Root, file: VFileLike) => {
99 visit(tree, "inlineMath", (node, index, parent) => {
100 if (typeof index !== "number" || !parent) return;
101
102 const typedNode = node as InlineMath;
103 const typedParent = parent as ParentWithChildren;
104 const { source, rendered } = inlineMathSources(typedNode, file);
105 const decision = classifyInlineMath(source.trim());
106
107 if (decision === "math") {
108 setMathValue(typedNode, source, latexNormalizeForKatex(rendered));
109 return;
110 }
111
112 const value = originalSource(typedNode, file);
113 typedParent.children[index] = { type: "text", value } satisfies Text;
114 });
115
116 visit(tree, "math", (node) => {
117 const { source, rendered } = resolveProtectedInlineMathSource(node.value);
118 setMathValue(node, source, latexNormalizeForKatex(rendered));
119 });
120 };
121 }
122
122 lines TYPESCRIPT