import { describe, expect, it } from "vitest"; import { normalizeMath } from "./math"; describe("normalizeMath", () => { describe("LaTeX bracket delimiters", () => { it("converts inline \\( ... \\) to single dollars", () => { expect(normalizeMath("Energy is \\(E = mc^2\\) exactly.")).toBe( "Energy is $E = mc^2$ exactly.", ); }); // The `$$` fences must land on their own lines: remark-math only emits a // *block* formula when `$$` opens a line. Written inline as `$$ x $$` it // silently degrades to inline math — verified against the real pipeline. it("converts display \\[ ... \\] to block double dollars", () => { expect(normalizeMath("\\[ \\sum_{i=1}^{n} x_i \\]")).toBe("\n$$\n\\sum_{i=1}^{n} x_i\n$$\n"); }); it("converts several occurrences in one string", () => { expect(normalizeMath("\\(a\\) and \\(b\\) then \\[c\\]")).toBe( "$a$ and $b$ then \n$$\nc\n$$\n", ); }); it("converts display math spanning multiple lines", () => { const input = "Before\n\\[\n x = y\n\\]\nAfter"; expect(normalizeMath(input)).toBe("Before\n\n$$\nx = y\n$$\n\nAfter"); }); it("keeps backslash commands inside the formula intact", () => { expect(normalizeMath("\\(\\alpha \\times \\beta\\)")).toBe("$\\alpha \\times \\beta$"); }); }); describe("existing dollar math", () => { it("leaves inline $...$ untouched", () => { expect(normalizeMath("The value $x^2$ grows.")).toBe("The value $x^2$ grows."); }); it("leaves display $$...$$ untouched", () => { expect(normalizeMath("$$\n\\frac{a}{b}\n$$")).toBe("$$\n\\frac{a}{b}\n$$"); }); }); describe("code is never rewritten", () => { it("leaves \\( inside a fenced code block alone", () => { const input = "```python\nprint('\\(not math\\)')\n```"; expect(normalizeMath(input)).toBe(input); }); it("leaves \\[ inside a fenced code block alone", () => { const input = "```\narr\\[0\\]\n```"; expect(normalizeMath(input)).toBe(input); }); it("leaves \\( inside an inline code span alone", () => { const input = "Call `f\\(x\\)` to run it."; expect(normalizeMath(input)).toBe(input); }); it("still converts math outside a fenced block", () => { const input = "\\(a\\)\n```\n\\(b\\)\n```\n\\(c\\)"; expect(normalizeMath(input)).toBe("$a$\n```\n\\(b\\)\n```\n$c$"); }); }); describe("streaming safety", () => { it("leaves an unterminated \\( alone", () => { expect(normalizeMath("Energy is \\(E = mc")).toBe("Energy is \\(E = mc"); }); it("leaves an unterminated \\[ alone", () => { expect(normalizeMath("\\[ \\sum_{i=1}")).toBe("\\[ \\sum_{i=1}"); }); it("converts the closed pair and leaves a trailing open one", () => { expect(normalizeMath("\\(a\\) then \\(b")).toBe("$a$ then \\(b"); }); it("leaves an unterminated fenced block's contents alone", () => { const input = "text\n```\n\\(a\\)"; expect(normalizeMath(input)).toBe(input); }); }); // A fence is only a fence at the start of a line. A stray ``` written // mid-sentence — an answer *about* markdown, say — used to be treated as an // unterminated block, so every formula after it silently stopped rendering. describe("a mid-sentence ``` is not a fence", () => { it("still converts math after an inline triple backtick", () => { expect(normalizeMath("Use ``` for fences. Then \\(a=b\\).")).toBe( "Use ``` for fences. Then $a=b$.", ); }); it("does not swallow prose between a stray ``` and a later real fence", () => { const input = "Use ``` inline. Also \\(a\\).\n```\ncode\n```"; expect(normalizeMath(input)).toBe("Use ``` inline. Also $a$.\n```\ncode\n```"); }); it("still treats an indented fence (up to 3 spaces) as a fence", () => { const input = " ```\n\\(a\\)\n ```"; expect(normalizeMath(input)).toBe(input); }); }); describe("passthrough", () => { it("returns an empty string unchanged", () => { expect(normalizeMath("")).toBe(""); }); it("returns plain prose unchanged", () => { expect(normalizeMath("No math here at all.")).toBe("No math here at all."); }); it("escapes a lone amount so it renders as text", () => { expect(normalizeMath("it costs $5")).toBe("it costs \\$5"); expect(normalizeMath("it costs $10")).toBe("it costs \\$10"); expect(normalizeMath("it costs $100")).toBe("it costs \\$100"); }); it("escapes both ends of a price pair", () => { expect(normalizeMath("costs $5 and $10 total")).toBe("costs \\$5 and \\$10 total"); }); it("escapes a price range", () => { expect(normalizeMath("between $5-$10")).toBe("between \\$5-\\$10"); expect(normalizeMath("grant of $5,000.50 awarded")).toBe("grant of \\$5,000.50 awarded"); }); it("escapes currency inside a list item", () => { expect(normalizeMath("- costs $5\n- costs $10")).toBe("- costs \\$5\n- costs \\$10"); }); it("escapes currency inside a table row", () => { expect(normalizeMath("| $5 | $10 |")).toBe("| \\$5 | \\$10 |"); }); it("escapes currency mixed with markdown emphasis", () => { expect(normalizeMath("**only $5** today")).toBe("**only \\$5** today"); }); // The other half of the contract: real formulas must survive untouched, // including the ones that legitimately open with a digit. it("keeps real math that starts with a digit", () => { expect(normalizeMath("area is $2\\pi r$ exactly")).toBe("area is $2\\pi r$ exactly"); expect(normalizeMath("$5^2 = 25$")).toBe("$5^2 = 25$"); expect(normalizeMath("$1_{i}$")).toBe("$1_{i}$"); }); it("keeps currency and real math side by side", () => { expect(normalizeMath("costs $5 but $E = mc^2$ holds")).toBe( "costs \\$5 but $E = mc^2$ holds", ); }); it("does not disturb display math that contains digits", () => { expect(normalizeMath("$$\n5x + 2\n$$")).toBe("$$\n5x + 2\n$$"); }); it("does not escape a dollar inside code", () => { expect(normalizeMath("`cost $5`")).toBe("`cost $5`"); expect(normalizeMath("```\ncost $5\n```")).toBe("```\ncost $5\n```"); }); it("leaves already-escaped currency alone", () => { expect(normalizeMath("costs \\$5 and \\$10 total")).toBe("costs \\$5 and \\$10 total"); }); it("does not treat an escaped backslash as a delimiter", () => { // "\\\\(" in the source is a literal backslash followed by "(" — a Windows // path or a regex, not the start of a formula. expect(normalizeMath("path\\\\(x)")).toBe("path\\\\(x)"); }); }); });