display-math newline preservation (Option A ruling 2026-09-27): inline spans restore all six debris chars, display spans keep real newlines — multi-line equations never corrupted; \right/\rho CR-debris still restores; sloppy \neq-in-display accepted as the rarest slice
This commit is contained in:
+17
-1
@@ -229,6 +229,16 @@ export function transformMathDelimiters(markdown: string): string {
|
|||||||
* balanced `\(…\)` / `\[…\]` spans are touched — the same scanner rules as
|
* balanced `\(…\)` / `\[…\]` spans are touched — the same scanner rules as
|
||||||
* transformMathDelimiters (escaped delimiters are literal, unbalanced
|
* transformMathDelimiters (escaped delimiters are literal, unbalanced
|
||||||
* delimiters pass through untouched).
|
* delimiters pass through untouched).
|
||||||
|
*
|
||||||
|
* ASYMMETRIC NEWLINE RULE (owner ruling 2026-09-27, Option A): inline spans
|
||||||
|
* restore all six debris chars — real newlines/tabs inside INLINE math
|
||||||
|
* essentially never occur (options and inline math are single-line by
|
||||||
|
* nature). Display spans restore five and KEEP real newlines — multi-line
|
||||||
|
* display math (aligned environments, equation systems) is common, and a
|
||||||
|
* real line break is byte-identical on the wire to sloppy `\neq`/`\nu`/
|
||||||
|
* `\neg`/`\nabla` (JSON `\n` resolves to a newline either way). Corrupting
|
||||||
|
* legitimate content is strictly worse than failing to repair that rarest
|
||||||
|
* slice, so display-span n-command sloppiness stays broken by design.
|
||||||
*/
|
*/
|
||||||
const ESCAPE_DEBRIS: Record<string, string> = {
|
const ESCAPE_DEBRIS: Record<string, string> = {
|
||||||
"\b": "\\b",
|
"\b": "\\b",
|
||||||
@@ -241,6 +251,11 @@ const ESCAPE_DEBRIS: Record<string, string> = {
|
|||||||
|
|
||||||
const DEBRIS_RE = /[\b\t\n\v\f\r]/;
|
const DEBRIS_RE = /[\b\t\n\v\f\r]/;
|
||||||
|
|
||||||
|
/** Display spans keep real newlines (multi-line equations) — debris there is
|
||||||
|
* everything except \n (a lone real \r never occurs in authored text, and
|
||||||
|
* `\right`/`\rho`/`\rangle` corruption is common in display math). */
|
||||||
|
const DISPLAY_DEBRIS_RE = /[\b\t\v\f\r]/;
|
||||||
|
|
||||||
export function restoreMathEscapes(text: string): string {
|
export function restoreMathEscapes(text: string): string {
|
||||||
if (!DEBRIS_RE.test(text)) return text;
|
if (!DEBRIS_RE.test(text)) return text;
|
||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
@@ -256,7 +271,8 @@ export function restoreMathEscapes(text: string): string {
|
|||||||
const closeAt = findClose(text, i + open.length, close);
|
const closeAt = findClose(text, i + open.length, close);
|
||||||
if (closeAt !== -1) {
|
if (closeAt !== -1) {
|
||||||
const inner = text.slice(i + open.length, closeAt);
|
const inner = text.slice(i + open.length, closeAt);
|
||||||
out.push(open + inner.replace(DEBRIS_RE, (c) => ESCAPE_DEBRIS[c]!) + close);
|
const debris = isDisplay ? DISPLAY_DEBRIS_RE : DEBRIS_RE;
|
||||||
|
out.push(open + inner.replace(debris, (c) => ESCAPE_DEBRIS[c]!) + close);
|
||||||
i = closeAt + close.length;
|
i = closeAt + close.length;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,3 +216,53 @@ describe("LaTeX-escape restoration (the JSON-debris fix, owner ruling 2026-09-26
|
|||||||
assert.equal(parsed.question, "\\(\\x0corall");
|
assert.equal(parsed.question, "\\(\\x0corall");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("display-math newline rule (Option A, owner ruling 2026-09-27)", () => {
|
||||||
|
// TS literal below: \\[ is \[, \\forall is intact \forall, \n is a REAL newline, \\\\ is the LaTeX \\ line break.
|
||||||
|
const MULTILINE = "Compare:\n\\[\n\\forall x, & f(x) = x^2 \\\\\n& g(x) = x^3\n\\]";
|
||||||
|
|
||||||
|
it("real newlines inside display math are kept — multi-line equations pass through untouched", () => {
|
||||||
|
const parsed = parseQuizInput({
|
||||||
|
question: MULTILINE,
|
||||||
|
options: [{ label: "f grows slower" }, { label: "g grows slower" }],
|
||||||
|
correctAnswer: "f grows slower",
|
||||||
|
explanation: "",
|
||||||
|
});
|
||||||
|
assert.ok(parsed);
|
||||||
|
assert.equal(parsed.question, MULTILINE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("non-newline debris inside display math still restores (the \\right CR case)", () => {
|
||||||
|
const parsed = parseQuizInput({
|
||||||
|
question: "Evaluate \\[\\left( a \right)\\] carefully.",
|
||||||
|
options: [{ label: "a" }, { label: "not a" }],
|
||||||
|
correctAnswer: "a",
|
||||||
|
explanation: "",
|
||||||
|
});
|
||||||
|
assert.ok(parsed);
|
||||||
|
assert.equal(parsed.question, "Evaluate \\[\\left( a \\right)\\] carefully.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sloppy \\neq in display math stays broken — the accepted cost (newline kept, byte-identical to a real line break)", () => {
|
||||||
|
const debris = "Is \\[p \neq q\\] valid?";
|
||||||
|
const parsed = parseQuizInput({
|
||||||
|
question: debris,
|
||||||
|
options: [{ label: "yes" }, { label: "no" }],
|
||||||
|
correctAnswer: "no",
|
||||||
|
explanation: "",
|
||||||
|
});
|
||||||
|
assert.ok(parsed);
|
||||||
|
assert.equal(parsed.question, debris);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inline spans still restore newline debris (inline math is single-line by nature)", () => {
|
||||||
|
const parsed = parseQuizInput({
|
||||||
|
question: "Is \\(p \neq q\\) valid?",
|
||||||
|
options: [{ label: "yes" }, { label: "no" }],
|
||||||
|
correctAnswer: "no",
|
||||||
|
explanation: "",
|
||||||
|
});
|
||||||
|
assert.ok(parsed);
|
||||||
|
assert.equal(parsed.question, "Is \\(p \\neq q\\) valid?");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user