punctilio 1.4.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,6 +64,17 @@ describe("rehypePunctilio", () => {
64
64
  })
65
65
  })
66
66
 
67
+ describe("dashes across element boundaries", () => {
68
+ it.each([
69
+ ["multi-segment number preserved", "<p>1-<em>2</em>-3</p>", "<p>1-<em>2</em>-3</p>"],
70
+ ["model name preserved", "<p><em>GPT</em>-3</p>", "<p><em>GPT</em>-3</p>"],
71
+ ["simple range still converts", "<p>pages <em>1</em>-5</p>", `<p>pages <em>1</em>${EN_DASH}5</p>`],
72
+ ["genuine negative at element start", "<p><em>-5</em> degrees</p>", "<p><em>\u22125</em> degrees</p>"],
73
+ ])("%s", async (_name, html, expected) => {
74
+ expect(await processHtml(html)).toEqual(expected)
75
+ })
76
+ })
77
+
67
78
  describe("skipped elements", () => {
68
79
  it.each(["code", "pre", "script", "style", "kbd", "var", "samp"])(
69
80
  "skips %s elements",
@@ -388,6 +399,34 @@ describe("rehypePunctilio", () => {
388
399
  const tree = h("div", [h("img")]) as Element
389
400
  expect(collectTransformableElements(tree, ignoreNone)).toHaveLength(0)
390
401
  })
402
+
403
+ it("stops at max recursion depth", () => {
404
+ let deep: Element = h("p", "deep") as Element
405
+ for (let i = 0; i < 1100; i++) deep = h("div", [deep]) as Element
406
+ expect(collectTransformableElements(deep, ignoreNone)).toHaveLength(0)
407
+ })
408
+ })
409
+ })
410
+
411
+ describe("nbsp option", () => {
412
+ const NBSP = UNICODE_SYMBOLS.NBSP
413
+
414
+ it("inserts nbsp when option enabled", async () => {
415
+ const result = await processHtml('<p>Dr. Smith has 5 kg of items.</p>', { nbsp: true })
416
+ expect(result).toContain(`Dr.${NBSP}Smith`)
417
+ expect(result).toContain(`5${NBSP}kg`)
418
+ })
419
+
420
+ it("does not insert nbsp when option disabled (default)", async () => {
421
+ const result = await processHtml('<p>Dr. Smith has 5 kg of items.</p>')
422
+ expect(result).not.toContain(NBSP)
423
+ })
424
+
425
+ it("works across element boundaries", async () => {
426
+ const result = await processHtml('<p>See <em>Fig.</em> 1</p>', { nbsp: true })
427
+ expect(result).toContain(`Fig.`)
428
+ // The nbsp should appear between Fig. and 1
429
+ expect(result).toContain(NBSP)
391
430
  })
392
431
  })
393
432
 
@@ -595,6 +595,15 @@ describe("symbolTransform", () => {
595
595
  })
596
596
  })
597
597
 
598
+ describe("multiplication ReDoS regression", () => {
599
+ it("handles 50k digits without quadratic slowdown", () => {
600
+ const start = performance.now()
601
+ multiplication("1".repeat(50_000))
602
+ // Before fix: ~4400ms (O(n²)). After fix: <50ms (O(n)).
603
+ expect(performance.now() - start).toBeLessThan(500)
604
+ })
605
+ })
606
+
598
607
  describe("multiplication edge cases", () => {
599
608
  it.each([
600
609
  ["1000000x2000000", `1000000${UNICODE_SYMBOLS.MULTIPLICATION}2000000`],
@@ -59,9 +59,26 @@ describe("assertSeparatorCountPreserved", () => {
59
59
  })
60
60
 
61
61
  describe("formatErrorString", () => {
62
+ let originalWrite: typeof process.stderr.write
63
+ let stderrOutput: string[]
64
+
65
+ beforeEach(() => {
66
+ originalWrite = process.stderr.write
67
+ stderrOutput = []
68
+ process.stderr.write = ((chunk: string) => {
69
+ stderrOutput.push(chunk)
70
+ return true
71
+ }) as typeof process.stderr.write
72
+ })
73
+
74
+ afterEach(() => {
75
+ process.stderr.write = originalWrite
76
+ })
77
+
62
78
  it("returns JSON-stringified content for short strings", () => {
63
79
  const result = formatErrorString("short text", "test")
64
80
  expect(result).toBe('"short text"')
81
+ expect(stderrOutput).toHaveLength(0)
65
82
  })
66
83
 
67
84
  it("truncates strings over 2000 chars with label and length", () => {
@@ -77,52 +94,24 @@ describe("formatErrorString", () => {
77
94
  const longText = "a".repeat(1500) + "b".repeat(1000)
78
95
  const result = formatErrorString(longText, "mixed")
79
96
 
80
- // Should contain all 1500 a's and first 500 b's (2000 total)
81
97
  const expected2000 = "a".repeat(1500) + "b".repeat(500)
82
98
  expect(result).toContain(JSON.stringify(expected2000))
83
99
  expect(result).toContain("2500 chars total")
84
100
  })
85
101
 
86
102
  it("writes full content to stderr in Node.js for long strings", () => {
87
- const originalWrite = process.stderr.write
88
- const written: string[] = []
89
- process.stderr.write = ((chunk: string) => {
90
- written.push(chunk)
91
- return true
92
- }) as typeof process.stderr.write
93
-
94
- try {
95
- const longText = "z".repeat(2001)
96
- formatErrorString(longText, "stderr-test")
97
-
98
- expect(written.length).toBe(1)
99
- expect(written[0]).toContain("punctilio stderr-test full content")
100
- expect(written[0]).toContain(longText)
101
- } finally {
102
- process.stderr.write = originalWrite
103
- }
104
- })
105
-
106
- it("does not write to stderr for short strings", () => {
107
- const originalWrite = process.stderr.write
108
- const written: string[] = []
109
- process.stderr.write = ((chunk: string) => {
110
- written.push(chunk)
111
- return true
112
- }) as typeof process.stderr.write
103
+ const longText = "z".repeat(2001)
104
+ formatErrorString(longText, "stderr-test")
113
105
 
114
- try {
115
- formatErrorString("short", "test")
116
- expect(written.length).toBe(0)
117
- } finally {
118
- process.stderr.write = originalWrite
119
- }
106
+ expect(stderrOutput).toHaveLength(1)
107
+ expect(stderrOutput[0]).toContain("punctilio stderr-test full content")
108
+ expect(stderrOutput[0]).toContain(longText)
120
109
  })
121
110
 
122
111
  it("handles strings at exactly the threshold", () => {
123
112
  const exactText = "x".repeat(2000)
124
113
  const result = formatErrorString(exactText, "exact")
125
- // At threshold, should return JSON-stringified (not truncated)
126
114
  expect(result).toBe(JSON.stringify(exactText))
115
+ expect(stderrOutput).toHaveLength(0)
127
116
  })
128
117
  })