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.
- package/README.md +3 -5
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +7 -3
- package/dist/dashes.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/nbsp.d.ts +75 -0
- package/dist/nbsp.d.ts.map +1 -0
- package/dist/nbsp.js +195 -0
- package/dist/nbsp.js.map +1 -0
- package/dist/symbols.js +2 -2
- package/dist/symbols.js.map +1 -1
- package/package.json +1 -1
- package/src/dashes.ts +11 -3
- package/src/index.ts +32 -2
- package/src/nbsp.ts +252 -0
- package/src/symbols.ts +2 -2
- package/src/tests/dashes.test.ts +28 -0
- package/src/tests/index.test.ts +27 -0
- package/src/tests/nbsp.test.ts +255 -0
- package/src/tests/rehype.test.ts +39 -0
- package/src/tests/symbols.test.ts +9 -0
- package/src/tests/utils.test.ts +23 -34
package/src/tests/rehype.test.ts
CHANGED
|
@@ -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`],
|
package/src/tests/utils.test.ts
CHANGED
|
@@ -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
|
|
88
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
})
|