返回 DeepSeek-Reasonix
math-young-diagrams.test.ts
根目录 / desktop / frontend / src / __tests__ / math-young-diagrams.test.ts
1 // Golden cases for the Young diagram / tableau macros in the math pipeline.
2 //
3 // Run: tsx src/__tests__/math-young-diagrams.test.ts
4
5 import { createElement } from "react";
6 import { renderToStaticMarkup } from "react-dom/server";
7 import ReactMarkdown from "react-markdown";
8 import { reasonixRehypePlugins, reasonixRemarkPlugins } from "../components/markdownRemarkPlugins";
9 import { normalizeMath, resolveProtectedInlineMathSource } from "../components/mathNormalize";
10 import { expandYoungDiagrams } from "../components/youngDiagrams";
11
12 let passed = 0;
13 let failed = 0;
14
15 function check(label: string, fn: () => boolean) {
16 try {
17 if (fn()) { process.stdout.write(` PASS ${label}
18 `); passed += 1; }
19 else { process.stdout.write(` FAIL ${label}
20 `); failed += 1; }
21 } catch (e) {
22 process.stdout.write(` ERROR ${label}: ${(e as Error).message}
23 `); failed += 1;
24 }
25 }
26
27 function renderHtml(src: string): string {
28 return renderToStaticMarkup(
29 createElement(ReactMarkdown, {
30 remarkPlugins: reasonixRemarkPlugins,
31 rehypePlugins: reasonixRehypePlugins,
32 children: normalizeMath(src),
33 }),
34 );
35 }
36
37 // ── Young diagram / tableau macros ─────────────────────────────────────────────
38 // `\yng` (ytableau) and `\young` (youngtab) are common in physics —
39 // SU(N) irreps, tensor decompositions, character tables — but KaTeX
40 // doesn't bundle either macro package. The pre-pass translates them
41 // to KaTeX-compatible `\boxed{array}` forms inside the math body so
42 // the diagram renders as a grid of boxes.
43
44 console.log("\nnormalizeMath — Young diagram macros");
45
46 check("\\yng(2,1) renders as (2,1) Young diagram", () => {
47 const html = renderHtml("$$\\yng(2,1)$$");
48 return html.includes("katex-display") && !html.includes("katex-error");
49 });
50 check("\\yng(2,1) in prose (no $ delimiters) gets wrapped and rendered", () => {
51 // A model that writes "the partition \\yng(2,1) corresponds to the
52 // (2,1) irrep" doesn't usually put $$ around the macro. The
53 // translator wraps bare \\yng in `$…$` so remark-math sees it as
54 // inline math and katex renders the diagram.
55 const html = renderHtml("The partition \\yng(2,1) is symmetric.");
56 return html.includes("katex")
57 && !html.includes("katex-error")
58 && !html.includes("reasonixInternal")
59 && html.includes('data-latex-source="\\yng(2,1)"');
60 });
61 check("\\yng inside \\(...\\) does not get double-wrapped", () => {
62 const out = resolveProtectedInlineMathSource(normalizeMath("\\(\\yng(2,1)\\)"));
63 return out.source === "$\\yng(2,1)$"
64 && out.rendered === "$\\begin{array}{l}\\square \\! \\square \\\\[-0.525em] \\square\\end{array}$";
65 });
66 check("\\yng inside \\[...\\] stays display math without triple dollars", () => {
67 const out = resolveProtectedInlineMathSource(normalizeMath("\\[\\yng(2,1)\\]"));
68 return out.source === "$$\n\\yng(2,1)\n$$"
69 && out.rendered.startsWith("$$\n\\begin{array}{l}")
70 && out.rendered.endsWith("$$")
71 && !out.rendered.includes("$$$");
72 });
73 check("escaped dollar before bare \\yng does not suppress wrapping", () => {
74 const src = String.raw`Price is \$5; shape \yng(2,1)`;
75 const expected = String.raw`Price is \$5; shape $\begin{array}{l}\square \! \square \\[-0.525em] \square\end{array}$`;
76 const out = resolveProtectedInlineMathSource(normalizeMath(src));
77 return out.source === String.raw`Price is \$5; shape $\yng(2,1)$`
78 && out.rendered === expected;
79 });
80 check("digit-starting inline math with \\yng does not get nested wrappers", () => {
81 const out = resolveProtectedInlineMathSource(normalizeMath("$3\\,\\yng(2,1)$"));
82 return out.source === "$3\\,\\yng(2,1)$"
83 && out.rendered === "$3\\,\\begin{array}{l}\\square \\! \\square \\\\[-0.525em] \\square\\end{array}$";
84 });
85 check("digit-starting inline math with \\young does not get nested wrappers", () => {
86 const out = resolveProtectedInlineMathSource(normalizeMath("$2 + \\young(ab,c)$"));
87 return out.source === "$2 + \\young(ab,c)$"
88 && out.rendered === "$2 + \\begin{array}{l}\\boxed{a} \\! \\boxed{b} \\\\[-0.525em] \\boxed{c}\\end{array}$";
89 });
90 check("display math ending in digit closes before following bare \\yng", () => {
91 const out = resolveProtectedInlineMathSource(normalizeMath("$$x^2$$ \\yng(1)"));
92 return out.source === "$$\nx^2\n$$\n $\\yng(1)$"
93 && out.rendered === "$$\nx^2\n$$\n $\\begin{array}{l}\\square\\end{array}$";
94 });
95 check("bare \\yng after inline math is separated from adjacent dollars", () => {
96 const out = resolveProtectedInlineMathSource(normalizeMath("$x$\\yng(1)"));
97 return out.source === "$x$ $\\yng(1)$"
98 && out.rendered === "$x$ $\\begin{array}{l}\\square\\end{array}$";
99 });
100 check("bare \\yng before inline math is separated from adjacent dollars", () => {
101 const out = resolveProtectedInlineMathSource(normalizeMath("\\yng(1)$x$"));
102 return out.source === "$\\yng(1)$ $x$"
103 && out.rendered === "$\\begin{array}{l}\\square\\end{array}$ $x$";
104 });
105 check("\\yng (2,1) with a space before parens gets wrapped and rendered", () => {
106 const html = renderHtml("The partition \\yng (2,1) is symmetric.");
107 return html.includes("katex")
108 && !html.includes("katex-error")
109 && !html.includes("reasonixInternal")
110 && html.includes('data-latex-source="\\yng (2,1)"');
111 });
112 check("\\yng(3,2,1) renders as (3,2,1) Young diagram", () => {
113 const html = renderHtml("$$\\yng(3,2,1)$$");
114 return html.includes("katex-display") && !html.includes("katex-error");
115 });
116 check("\\yng(2,1){a&b\\\\c\\\\d&e} renders filled Young tableau", () => {
117 const html = renderHtml("$$\\yng(2,1){a&b\\\\c\\\\d&e}$$");
118 return html.includes("katex-display") && !html.includes("katex-error");
119 });
120 check("\\young(2 1) compatibility shorthand renders as (2,1) diagram", () => {
121 const html = renderHtml("$$\\young(2 1)$$");
122 return html.includes("katex-display") && !html.includes("katex-error");
123 });
124 check("\\young(ab,c) labelled youngtab syntax renders labels", () => {
125 const html = renderHtml("$$\\young(ab,c)$$");
126 return html.includes("katex-display")
127 && !html.includes("katex-error")
128 && !html.includes("reasonixInternal")
129 && html.includes('data-latex-source="\\young(ab,c)"')
130 && ["a", "b", "c"].every((label) => html.includes(label));
131 });
132 check("\\young(ab,c) labelled cells keep boxes", () => {
133 const out = expandYoungDiagrams("\\young(ab,c)");
134 return out.includes("\\boxed{a}")
135 && out.includes("\\boxed{b}")
136 && out.includes("\\boxed{c}");
137 });
138 check("\\young(abcd,:cd,:c) skew placeholders are invisible offsets", () => {
139 const out = expandYoungDiagrams("\\young(abcd,:cd,:c)");
140 return out.includes("\\hphantom{\\boxed{x}}")
141 && !out.includes("\\boxed{:}");
142 });
143 check("\\yng(4,3,2,1) renders as (4,3,2,1) Young diagram", () => {
144 const html = renderHtml("$$\\yng(4,3,2,1)$$");
145 return html.includes("katex-display") && !html.includes("katex-error");
146 });
147 check("\\yng(3,2,1) uses left-aligned array (rows start at same x)", () => {
148 // A Young diagram's shorter rows must start at the same x-position
149 // as the longest row's first cell — `{l}` (left) instead of `{c}`
150 // (centered) gives that layout. Without this, the diagram looks
151 // like each row is independently centred, which isn't a Young
152 // diagram.
153 const out = expandYoungDiagrams("\\yng(3,2,1)");
154 return out.includes("\\begin{array}{l}")
155 && !out.includes("\\begin{array}{c}");
156 });
157 check("expandYoungDiagrams uses flush cells (\\! cancels \\,) ", () => {
158 // Adjacent \square boxes should be flush — the convention for Young
159 // diagrams. The translator uses `\!` (negative thin space, -0.1667em)
160 // which exactly cancels `\,` so cells touch without visible gap.
161 // `\,` (positive thin space) would leave a gap.
162 const out = expandYoungDiagrams("\\yng(3)");
163 return out.includes("\\!") && !out.includes("\\, ");
164 });
165 check("expandYoungDiagrams uses flush rows (\\[-0.525em] closes the math-axis gap)", () => {
166 // The math axis positions a \square glyph centred on the row
167 // baseline, which leaves a visible ~0.4em gap between the bottom of
168 // one row's box and the top of the next row's box when the default
169 // 1.2em baseline-to-baseline spacing is used. Using `\\[-0.4em]`
170 // between rows pulls each subsequent row up by the math-axis offset,
171 // so consecutive rows touch. (Earlier versions tried wrapping each
172 // cell in `\raisebox{-0.35em}` which does NOT close the gap —
173 // uniform translation can't change the relative distance between
174 // row baselines.)
175 const out = expandYoungDiagrams("\\yng(2,1)");
176 return out.includes("\\\\[-0.525em]");
177 });
178 check("expandYoungDiagrams substitutes correct array form", () => {
179 // Direct unit test on the translator — no need to go through the
180 // full pipeline for this assertion.
181 const out = expandYoungDiagrams("\\yng(2,1)");
182 return out.includes("\\begin{array}{l}")
183 && out.includes("\\square")
184 && out.includes(" \\\\[-0.525em] ");
185 });
186 check("expandYoungDiagrams handles \\yng with content", () => {
187 // Bare \yng in prose gets wrapped in `$…$` so remark-math sees it as
188 // math; macros already inside a `$…$` block just substitute the inner
189 // form (the surrounding delimiters are preserved).
190 // Cells are joined with `\!` (negative thin space) so adjacent
191 // boxes are flush. Row separators use `\\[-0.525em]` (per-row
192 // negative spacing) so consecutive rows touch — the visible
193 // glyph height of `\square` is 0.675em (measured from katex's
194 // single-glyph strut), and the default 1.2em baseline spacing
195 // leaves 0.525em of gap. `\\[-0.525em]` subtracts exactly that.
196 const out = expandYoungDiagrams("\\yng(2,1){a&b\\\\c}");
197 return out === "$\\begin{array}{l}\\boxed{a} \\! \\boxed{b} \\\\[-0.525em] \\boxed{c}\\end{array}$";
198 });
199 check("expandYoungDiagrams handles labelled \\young rows", () => {
200 const out = expandYoungDiagrams("\\young(ab,c)");
201 return out === "$\\begin{array}{l}\\boxed{a} \\! \\boxed{b} \\\\[-0.525em] \\boxed{c}\\end{array}$";
202 });
203 check("expandYoungDiagrams treats comma-separated numeric \\young as labels, not a 12-cell row", () => {
204 const out = expandYoungDiagrams("\\young(12,3)");
205 return out === "$\\begin{array}{l}\\boxed{1} \\! \\boxed{2} \\\\[-0.525em] \\boxed{3}\\end{array}$";
206 });
207 check("expandYoungDiagrams leaves invalid negative \\yng shape alone", () => {
208 const out = expandYoungDiagrams("\\yng(-1)");
209 return out === "\\yng(-1)";
210 });
211 check("expandYoungDiagrams leaves oversized \\yng shape alone", () => {
212 const out = expandYoungDiagrams("\\yng(513)");
213 return out === "\\yng(513)";
214 });
215 check("expandYoungDiagrams leaves non-Young macros alone", () => {
216 const out = expandYoungDiagrams("\\frac{a}{b}");
217 return out === "\\frac{a}{b}";
218 });
219
220 // ── Summary ───────────────────────────────────────────────────────────────────
221
222 console.log(`
223 ${passed} passed, ${failed} failed, ${passed + failed} total`);
224 if (failed > 0) process.exit(1);
225
225 lines TYPESCRIPT