Spaces:
Running
Running
| 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)"); | |
| }); | |
| }); | |
| }); | |