返回 DeepSeek-Reasonix
rehypeReasonixKatex.ts
根目录 / desktop / frontend / src / components / rehypeReasonixKatex.ts
1 import rehypeKatex from "rehype-katex";
2
3 type HastNode = {
4 type: string;
5 tagName?: string;
6 value?: string;
7 properties?: Record<string, unknown>;
8 children?: HastNode[];
9 };
10
11 const SOURCE_MARKER = "reasonix-latex-source:";
12
13 function markLatexSources(node: HastNode, sources: string[]): void {
14 const children = node.children;
15 if (!children) return;
16
17 for (let index = 0; index < children.length; index += 1) {
18 const child = children[index];
19 const source = child.properties?.dataLatexSource;
20 if (child.type === "element" && typeof source === "string") {
21 const marker = `${SOURCE_MARKER}${sources.push(source) - 1}`;
22 children.splice(index, 0, { type: "comment", value: marker });
23 index += 1;
24 continue;
25 }
26 markLatexSources(child, sources);
27 }
28 }
29
30 function classNames(node: HastNode): string[] {
31 const value = node.properties?.className;
32 return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
33 }
34
35 function findKatexRoot(node: HastNode): HastNode | null {
36 if (
37 node.type === "element"
38 && (classNames(node).includes("katex-display") || classNames(node).includes("katex"))
39 ) {
40 return node;
41 }
42 for (const child of node.children ?? []) {
43 const root = findKatexRoot(child);
44 if (root) return root;
45 }
46 return null;
47 }
48
49 function findTexAnnotation(node: HastNode): HastNode | null {
50 if (
51 node.type === "element"
52 && node.tagName === "annotation"
53 && node.properties?.encoding === "application/x-tex"
54 ) {
55 return node;
56 }
57 for (const child of node.children ?? []) {
58 const annotation = findTexAnnotation(child);
59 if (annotation) return annotation;
60 }
61 return null;
62 }
63
64 function restoreLatexSources(node: HastNode, sources: string[]): void {
65 const children = node.children;
66 if (!children) return;
67
68 for (let index = 0; index < children.length;) {
69 const child = children[index];
70 const markerIndex = child.type === "comment" && child.value?.startsWith(SOURCE_MARKER)
71 ? Number(child.value.slice(SOURCE_MARKER.length))
72 : Number.NaN;
73 if (Number.isInteger(markerIndex)) {
74 const source = sources[markerIndex];
75 const rendered = children[index + 1];
76 const root = rendered ? findKatexRoot(rendered) : null;
77 const annotation = root ? findTexAnnotation(root) : null;
78 if (root && source !== undefined) {
79 root.properties ??= {};
80 root.properties.dataLatexSource = source;
81 }
82 if (annotation && source !== undefined) {
83 annotation.children = [{ type: "text", value: source }];
84 }
85 children.splice(index, 1);
86 continue;
87 }
88 restoreLatexSources(child, sources);
89 index += 1;
90 }
91 }
92
93 /**
94 * Render the normalised TeX with KaTeX, then restore the original source into
95 * the generated MathML annotation so selection-copy can round-trip user input.
96 */
97 export function rehypeReasonixKatex() {
98 const renderKatex = rehypeKatex();
99 return (
100 tree: Parameters<typeof renderKatex>[0],
101 file: Parameters<typeof renderKatex>[1],
102 ): undefined => {
103 const sources: string[] = [];
104 markLatexSources(tree as HastNode, sources);
105 renderKatex(tree, file);
106 restoreLatexSources(tree as HastNode, sources);
107 };
108 }
109
110 export const reasonixRehypePlugins = [rehypeReasonixKatex];
111
111 lines TYPESCRIPT