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