返回 DeepSeek-Reasonix
largeMarkdownTable.ts
根目录 / desktop / frontend / src / lib / largeMarkdownTable.ts
1 export type MarkdownTableAlignment = "left" | "center" | "right" | null;
2
3 export interface VirtualMarkdownTableData {
4 align: MarkdownTableAlignment[];
5 header: string[];
6 rows: string[][];
7 }
8
9 export interface ExtractedMarkdownTables {
10 text: string;
11 markerPrefix: string;
12 tables: VirtualMarkdownTableData[];
13 }
14
15 const LARGE_TABLE_MIN_ROWS = 50;
16 const UNSAFE_INLINE_MARKDOWN_RE = /[\\`*_{}\[\]<>!&$~^]/;
17
18 function stripOuterPipe(line: string): string {
19 let value = line.trim();
20 if (value.startsWith("|")) value = value.slice(1);
21 if (value.endsWith("|")) value = value.slice(0, -1);
22 return value;
23 }
24
25 function splitPlainRow(line: string): string[] | null {
26 if (!line.includes("|") || /^\s{4}/.test(line)) return null;
27 const cells = stripOuterPipe(line).split("|").map((cell) => cell.trim());
28 if (cells.length < 2 || cells.some((cell) => UNSAFE_INLINE_MARKDOWN_RE.test(cell))) return null;
29 return cells;
30 }
31
32 function delimiterAlignment(line: string): MarkdownTableAlignment[] | null {
33 if (!line.includes("|") || /^\s{4}/.test(line)) return null;
34 const cells = stripOuterPipe(line).split("|").map((cell) => cell.trim());
35 if (cells.length < 2 || cells.some((cell) => !/^:?-+:?$/.test(cell))) return null;
36 return cells.map((cell) => {
37 const left = cell.startsWith(":");
38 const right = cell.endsWith(":");
39 if (left && right) return "center";
40 if (right) return "right";
41 if (left) return "left";
42 return null;
43 });
44 }
45
46 function normalizeRow(cells: string[], columns: number): string[] {
47 if (cells.length >= columns) return cells.slice(0, columns);
48 return [...cells, ...Array<string>(columns - cells.length).fill("")];
49 }
50
51 function fenceStart(line: string): { marker: string; length: number } | null {
52 const match = /^ {0,3}(`{3,}|~{3,})/.exec(line);
53 return match ? { marker: match[1][0], length: match[1].length } : null;
54 }
55
56 function fenceEnd(line: string, fence: { marker: string; length: number }): boolean {
57 const escaped = fence.marker === "`" ? "`" : "~";
58 return new RegExp(`^ {0,3}${escaped}{${fence.length},}[ \\t]*$`).test(line);
59 }
60
61 function unusedMarkerPrefix(text: string): string {
62 let suffix = 0;
63 let prefix = "REASONIXLARGETABLE";
64 while (text.includes(prefix)) {
65 suffix += 1;
66 prefix = `REASONIXLARGETABLE${suffix}`;
67 }
68 return prefix;
69 }
70
71 /**
72 * Extract only large, top-level GFM tables whose cells are provably plain
73 * text. Complex tables keep flowing through remark-gfm unchanged. This avoids
74 * micromark's super-linear giant-table path without reimplementing inline
75 * Markdown, references, footnotes, math, escapes, entities, or raw HTML.
76 */
77 export function extractLargePlainMarkdownTables(text: string): ExtractedMarkdownTables {
78 const lines = text.split("\n");
79 const output: string[] = [];
80 const tables: VirtualMarkdownTableData[] = [];
81 const markerPrefix = unusedMarkerPrefix(text);
82 let fence: { marker: string; length: number } | null = null;
83
84 for (let index = 0; index < lines.length;) {
85 const line = lines[index];
86 if (fence) {
87 output.push(line);
88 if (fenceEnd(line, fence)) fence = null;
89 index += 1;
90 continue;
91 }
92 const openingFence = fenceStart(line);
93 if (openingFence) {
94 fence = openingFence;
95 output.push(line);
96 index += 1;
97 continue;
98 }
99
100 const header = splitPlainRow(line);
101 const align = index + 1 < lines.length ? delimiterAlignment(lines[index + 1]) : null;
102 if (!header || !align || header.length !== align.length) {
103 output.push(line);
104 index += 1;
105 continue;
106 }
107
108 const rows: string[][] = [];
109 let rowIndex = index + 2;
110 while (rowIndex < lines.length) {
111 const cells = splitPlainRow(lines[rowIndex]);
112 if (!cells) break;
113 rows.push(normalizeRow(cells, header.length));
114 rowIndex += 1;
115 }
116 // Fail closed when a complex/non-plain row might still belong to the same
117 // GFM table. Only a blank line or EOF is an unambiguous fast-path end.
118 const cleanEnd = rowIndex === lines.length || lines[rowIndex].trim() === "";
119 if (rows.length <= LARGE_TABLE_MIN_ROWS || !cleanEnd) {
120 output.push(line);
121 index += 1;
122 continue;
123 }
124
125 const tableIndex = tables.length;
126 tables.push({ align, header, rows });
127 output.push(`${markerPrefix}${tableIndex}`);
128 index = rowIndex;
129 }
130
131 return { text: output.join("\n"), markerPrefix, tables };
132 }
133
133 lines TYPESCRIPT