Spaces:
Running
Running
File size: 6,788 Bytes
205b3fc bdb685b 205b3fc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | 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)");
});
});
});
|