punctilio 3.7.2 → 3.7.4
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 +28 -28
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +2 -1
- package/dist/dashes.js.map +1 -1
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +5 -20
- package/dist/rehype.js.map +1 -1
- package/dist/symbols.d.ts +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +12 -5
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -1
- package/package.json +5 -4
- package/src/dashes.ts +2 -1
- package/src/rehype.ts +5 -24
- package/src/symbols.ts +11 -5
- package/src/utils.ts +1 -0
- package/tsconfig.json +1 -1
- package/dist/tests/test-helpers.d.ts +0 -33
- package/dist/tests/test-helpers.d.ts.map +0 -1
- package/dist/tests/test-helpers.js +0 -109
- package/dist/tests/test-helpers.js.map +0 -1
- package/src/tests/dashes.test.ts +0 -736
- package/src/tests/index.test.ts +0 -713
- package/src/tests/markdown.test.ts +0 -156
- package/src/tests/nbsp.test.ts +0 -272
- package/src/tests/quotes.test.ts +0 -783
- package/src/tests/rehype.test.ts +0 -701
- package/src/tests/remark.test.ts +0 -200
- package/src/tests/symbols.test.ts +0 -920
- package/src/tests/test-helpers.ts +0 -124
- package/src/tests/utils.test.ts +0 -151
package/src/tests/index.test.ts
DELETED
|
@@ -1,713 +0,0 @@
|
|
|
1
|
-
import { transform, DEFAULT_SEPARATOR, countSeparators } from "../index.js"
|
|
2
|
-
import { ellipsis } from "../symbols.js"
|
|
3
|
-
import { UNICODE_SYMBOLS, REGEX_SPECIAL_CHARS, MAX_REGEX_CACHE_SIZE } from "../constants.js"
|
|
4
|
-
import { assertLinearScaling, buildMixedContent } from "./test-helpers.js"
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
LEFT_DOUBLE_QUOTE,
|
|
8
|
-
RIGHT_DOUBLE_QUOTE,
|
|
9
|
-
RIGHT_SINGLE_QUOTE,
|
|
10
|
-
EM_DASH,
|
|
11
|
-
EN_DASH,
|
|
12
|
-
ELLIPSIS,
|
|
13
|
-
MULTIPLICATION,
|
|
14
|
-
NOT_EQUAL,
|
|
15
|
-
PLUS_MINUS,
|
|
16
|
-
LESS_EQUAL,
|
|
17
|
-
GREATER_EQUAL,
|
|
18
|
-
COPYRIGHT,
|
|
19
|
-
NBSP,
|
|
20
|
-
PRIME,
|
|
21
|
-
DOUBLE_PRIME,
|
|
22
|
-
TRADEMARK,
|
|
23
|
-
REGISTERED,
|
|
24
|
-
DEGREE,
|
|
25
|
-
MINUS,
|
|
26
|
-
FRACTION_1_2,
|
|
27
|
-
SUPERSCRIPT_ST,
|
|
28
|
-
} = UNICODE_SYMBOLS
|
|
29
|
-
|
|
30
|
-
describe("transform", () => {
|
|
31
|
-
it("applies both quote and dash transformations", () => {
|
|
32
|
-
const input = '"Hello," she said - "it\'s pages 1-5."'
|
|
33
|
-
const expected = `${LEFT_DOUBLE_QUOTE}Hello,${RIGHT_DOUBLE_QUOTE} she said${EM_DASH}${LEFT_DOUBLE_QUOTE}it${RIGHT_SINGLE_QUOTE}s pages 1${EN_DASH}5.${RIGHT_DOUBLE_QUOTE}`
|
|
34
|
-
expect(transform(input, { nbsp: false })).toBe(expected)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it("handles complex mixed content", () => {
|
|
38
|
-
const input = 'I was born in \'99 - "the best year" - and pages 10-20 are my favorite.'
|
|
39
|
-
const expected = `I was born in ${RIGHT_SINGLE_QUOTE}99${EM_DASH}${LEFT_DOUBLE_QUOTE}the best year${RIGHT_DOUBLE_QUOTE}${EM_DASH}and pages 10${EN_DASH}20 are my favorite.`
|
|
40
|
-
expect(transform(input, { nbsp: false })).toBe(expected)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it("preserves separator character", () => {
|
|
44
|
-
const sep = DEFAULT_SEPARATOR
|
|
45
|
-
const input = `"Hello${sep}" - test`
|
|
46
|
-
expect(transform(input, { separator: sep, nbsp: false })).toBe(`${LEFT_DOUBLE_QUOTE}Hello${sep}${RIGHT_DOUBLE_QUOTE}${EM_DASH}test`)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
describe("symbol transforms", () => {
|
|
50
|
-
it("applies symbol transforms by default", () => {
|
|
51
|
-
expect(transform('Wait... 5x5 != 25 (c) 2024', { nbsp: false })).toBe(`Wait${ELLIPSIS} 5${MULTIPLICATION}5 ${NOT_EQUAL} 25 ${COPYRIGHT} 2024`)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it("can disable symbol transforms", () => {
|
|
55
|
-
expect(transform('Wait... 5x5 != 25', { symbols: false, nbsp: false })).toBe('Wait... 5x5 != 25')
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
describe("optional transforms", () => {
|
|
60
|
-
it.each([
|
|
61
|
-
["fractions disabled", "1/2", { nbsp: false }, "1/2"],
|
|
62
|
-
["fractions enabled", "1/2", { fractions: true, nbsp: false }, UNICODE_SYMBOLS.FRACTION_1_2],
|
|
63
|
-
["degrees disabled", "20 C", { nbsp: false }, "20 C"],
|
|
64
|
-
["degrees enabled", "20 C", { degrees: true, nbsp: false }, `20 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
65
|
-
["superscript disabled", "1st", { nbsp: false }, "1st"],
|
|
66
|
-
["superscript enabled", "1st", { superscript: true, nbsp: false }, `1${UNICODE_SYMBOLS.SUPERSCRIPT_ST}`],
|
|
67
|
-
["ligatures disabled", "??", { nbsp: false }, "??"],
|
|
68
|
-
["ligatures enabled", "??", { ligatures: true, nbsp: false }, UNICODE_SYMBOLS.DOUBLE_QUESTION],
|
|
69
|
-
["nbsp enabled (default)", "Dr. Smith", {}, `Dr.${NBSP}Smith`],
|
|
70
|
-
["nbsp disabled", "Dr. Smith", { nbsp: false }, "Dr. Smith"],
|
|
71
|
-
] as const)("%s: %s → %s", (_desc, input, options, expected) => {
|
|
72
|
-
expect(transform(input, options)).toBe(expected)
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
describe("punctuationStyle option", () => {
|
|
77
|
-
const periodOutside = `${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}.`
|
|
78
|
-
const periodInside = `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE}`
|
|
79
|
-
|
|
80
|
-
it.each([
|
|
81
|
-
['"Hello".', undefined, periodInside, "american (default)"],
|
|
82
|
-
['"Hello."', "british", periodOutside, "british"],
|
|
83
|
-
['"Hello".', "none", '"Hello".', "none (skips all quote transforms)"],
|
|
84
|
-
] as const)("handles %s with %s style", (input, style, expected) => {
|
|
85
|
-
expect(transform(input, { ...(style ? { punctuationStyle: style } : {}), nbsp: false })).toBe(expected)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it("none skips prime marks too", () => {
|
|
89
|
-
expect(transform('5\'10"', { punctuationStyle: "none", nbsp: false })).toBe('5\'10"')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it("none skips all quote transforms while other transforms still apply", () => {
|
|
93
|
-
expect(transform('"Wait..." she said', { punctuationStyle: "none", nbsp: false })).toBe(`"Wait${ELLIPSIS}" she said`)
|
|
94
|
-
})
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
describe("german and french locale styles", () => {
|
|
98
|
-
it.each([
|
|
99
|
-
['"Guten Tag"', `${UNICODE_SYMBOLS.DOUBLE_LOW_9_QUOTE}Guten Tag${LEFT_DOUBLE_QUOTE}`, { punctuationStyle: "german" as const, nbsp: false }],
|
|
100
|
-
['"Bonjour"', `${UNICODE_SYMBOLS.LEFT_GUILLEMET}${UNICODE_SYMBOLS.NNBSP}Bonjour${UNICODE_SYMBOLS.NNBSP}${UNICODE_SYMBOLS.RIGHT_GUILLEMET}`, { punctuationStyle: "french" as const, nbsp: false }],
|
|
101
|
-
["it's", `it${RIGHT_SINGLE_QUOTE}s`, { punctuationStyle: "german" as const, nbsp: false }],
|
|
102
|
-
["l'homme", `l${RIGHT_SINGLE_QUOTE}homme`, { punctuationStyle: "french" as const, nbsp: false }],
|
|
103
|
-
])('transforms "%s" with locale style', (input, expected, options) => {
|
|
104
|
-
expect(transform(input, options)).toBe(expected)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it("german style is idempotent", () => {
|
|
108
|
-
const input = '"Guten Tag"'
|
|
109
|
-
const first = transform(input, { punctuationStyle: "german", nbsp: false })
|
|
110
|
-
const second = transform(first, { punctuationStyle: "german", nbsp: false })
|
|
111
|
-
expect(second).toBe(first)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it("french style is idempotent", () => {
|
|
115
|
-
const input = '"Bonjour"'
|
|
116
|
-
const first = transform(input, { punctuationStyle: "french", nbsp: false })
|
|
117
|
-
const second = transform(first, { punctuationStyle: "french", nbsp: false })
|
|
118
|
-
expect(second).toBe(first)
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
describe("collapseSpaces option", () => {
|
|
123
|
-
it.each([
|
|
124
|
-
["hello world", "hello world", "multiple spaces"],
|
|
125
|
-
[`foo${NBSP}${NBSP}bar`, `foo${NBSP}bar`, "multiple nbsp"],
|
|
126
|
-
[`a ${NBSP}b`, `a${NBSP}b`, "mixed spaces prefer nbsp"],
|
|
127
|
-
[`a${NBSP} b`, `a${NBSP}b`, "mixed spaces prefer nbsp"],
|
|
128
|
-
["hello\t\tworld", "hello world", "multiple tabs"],
|
|
129
|
-
["hello\t world", "hello world", "mixed tab and space"],
|
|
130
|
-
])("collapses %s by default", (input, expected) => {
|
|
131
|
-
expect(transform(input, { nbsp: false })).toBe(expected)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it.each([
|
|
135
|
-
["hello world", "hello world", "multiple spaces"],
|
|
136
|
-
[`foo${NBSP}${NBSP}bar`, `foo${NBSP}${NBSP}bar`, "multiple nbsp"],
|
|
137
|
-
["hello\t\tworld", "hello\t\tworld", "multiple tabs"],
|
|
138
|
-
])("preserves %s when disabled", (input, expected) => {
|
|
139
|
-
expect(transform(input, { collapseSpaces: false, nbsp: false })).toBe(expected)
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
describe("nbsp option", () => {
|
|
144
|
-
it("inserts nbsp in typographically appropriate places", () => {
|
|
145
|
-
expect(transform("Dr. Smith wrote Fig. 1 on p. 42", { nbsp: true }))
|
|
146
|
-
.toBe(`Dr.${NBSP}Smith wrote Fig.${NBSP}1 on${NBSP}p.${NBSP}42`)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it("collapseSpaces cleans up after nbsp", () => {
|
|
150
|
-
expect(transform("Prof. Wilson arrived", { nbsp: true }))
|
|
151
|
-
.toBe(`Prof.${NBSP}Wilson${NBSP}arrived`)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it("is idempotent with nbsp enabled", () => {
|
|
155
|
-
const input = "Dr. Smith has 5 kg of items in § 3"
|
|
156
|
-
const first = transform(input, { nbsp: true })
|
|
157
|
-
const second = transform(first, { nbsp: true })
|
|
158
|
-
expect(second).toBe(first)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it("preserves separator count with nbsp enabled", () => {
|
|
162
|
-
const sep = DEFAULT_SEPARATOR
|
|
163
|
-
const input = `Dr.${sep} Smith has${sep} 5 kg`
|
|
164
|
-
expect(() => transform(input, { nbsp: true, separator: sep })).not.toThrow()
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
describe("separator preservation", () => {
|
|
169
|
-
it.each([
|
|
170
|
-
[`Wait.${DEFAULT_SEPARATOR}.${DEFAULT_SEPARATOR}. for it`, 2],
|
|
171
|
-
[`"Hello${DEFAULT_SEPARATOR}" - ${DEFAULT_SEPARATOR}she${DEFAULT_SEPARATOR} said`, 3],
|
|
172
|
-
[`.${DEFAULT_SEPARATOR}.${DEFAULT_SEPARATOR}.`, 2],
|
|
173
|
-
])('preserves %i separators in "%s"', (input, expectedCount) => {
|
|
174
|
-
expect(() => transform(input, { separator: DEFAULT_SEPARATOR, nbsp: false })).not.toThrow()
|
|
175
|
-
expect(countSeparators(transform(input, { separator: DEFAULT_SEPARATOR, nbsp: false }), DEFAULT_SEPARATOR)).toBe(expectedCount)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it("preserves separator in ellipsis", () => {
|
|
179
|
-
const input = `.${DEFAULT_SEPARATOR}.${DEFAULT_SEPARATOR}.`
|
|
180
|
-
const result = ellipsis(input, { separator: DEFAULT_SEPARATOR })
|
|
181
|
-
expect(result.split(DEFAULT_SEPARATOR).length - 1).toBe(2)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it("preserves consecutive separators", () => {
|
|
185
|
-
const input = `a${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}b`
|
|
186
|
-
expect(transform(input, { nbsp: false })).toBe(input)
|
|
187
|
-
})
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
describe("Unicode edge cases", () => {
|
|
191
|
-
it.each([
|
|
192
|
-
'"café"',
|
|
193
|
-
'"Hello 👋 world"',
|
|
194
|
-
'"日本語"',
|
|
195
|
-
'"שלום"',
|
|
196
|
-
"a\u200Bb",
|
|
197
|
-
])('preserves content in "%s"', (input) => {
|
|
198
|
-
expect(transform(input, { nbsp: false })).toBe(transform(input, { nbsp: false })) // idempotent
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
// Tests derived from competitor libraries and benchmark cases
|
|
203
|
-
describe("competitor-derived comprehensive tests", () => {
|
|
204
|
-
// These tests document where punctilio excels over competitors
|
|
205
|
-
describe("punctilio advantages (benchmark cases)", () => {
|
|
206
|
-
it.each([
|
|
207
|
-
// Leading apostrophes - smartypants/tipograph fail these
|
|
208
|
-
["'SUP", `${RIGHT_SINGLE_QUOTE}SUP`],
|
|
209
|
-
["Rock 'n' Roll", `Rock ${RIGHT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE} Roll`],
|
|
210
|
-
["I was born in '99", `I was born in ${RIGHT_SINGLE_QUOTE}99`],
|
|
211
|
-
// Em dashes from surrounded hyphens - smartquotes fails
|
|
212
|
-
["This is a - hyphen.", `This is a${EM_DASH}hyphen.`],
|
|
213
|
-
// Number ranges - smartypants/smartquotes/typograf fail
|
|
214
|
-
["Pages 1-5", `Pages 1${EN_DASH}5`],
|
|
215
|
-
["2000-2020", `2000${EN_DASH}2020`],
|
|
216
|
-
// Date ranges - all competitors fail
|
|
217
|
-
["January-March", `January${EN_DASH}March`],
|
|
218
|
-
// Minus signs - most competitors fail
|
|
219
|
-
["-5", `${MINUS}5`],
|
|
220
|
-
["(-5)", `(${MINUS}5)`],
|
|
221
|
-
// Prime marks - smartypants/typograf fail
|
|
222
|
-
['5\'10"', `5${PRIME}10${DOUBLE_PRIME}`],
|
|
223
|
-
['He is 6\'2" tall', `He is 6${PRIME}2${DOUBLE_PRIME} tall`],
|
|
224
|
-
])('correctly transforms: "%s"', (input, expected) => {
|
|
225
|
-
expect(transform(input, { symbols: true, degrees: true, nbsp: false })).toBe(expected)
|
|
226
|
-
})
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
// Compound word preservation (all libraries should preserve these)
|
|
230
|
-
describe("compound word preservation", () => {
|
|
231
|
-
it.each([
|
|
232
|
-
"well-known",
|
|
233
|
-
"a browser- or OS-specific fashion",
|
|
234
|
-
"re-read",
|
|
235
|
-
"self-aware",
|
|
236
|
-
"high-quality",
|
|
237
|
-
])('preserves compound word: "%s"', (input) => {
|
|
238
|
-
expect(transform(input, { nbsp: false })).toBe(input)
|
|
239
|
-
})
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
// Model name preservation (punctilio's unique strength)
|
|
243
|
-
describe("model name preservation", () => {
|
|
244
|
-
it.each([
|
|
245
|
-
"Llama-2-7B",
|
|
246
|
-
"Llama-3-8B-Instruct",
|
|
247
|
-
"GPT-4",
|
|
248
|
-
"Claude-3-Opus",
|
|
249
|
-
"Qwen1.5-1.8B",
|
|
250
|
-
])('preserves model name: "%s"', (input) => {
|
|
251
|
-
expect(transform(input, { nbsp: false })).toBe(input)
|
|
252
|
-
})
|
|
253
|
-
})
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
describe("complex real-world text", () => {
|
|
257
|
-
it("handles dialogue with dashes and quotes", () => {
|
|
258
|
-
const input = '"Wait," she said -- "I don\'t think that\'s right."'
|
|
259
|
-
const expected = `${LEFT_DOUBLE_QUOTE}Wait,${RIGHT_DOUBLE_QUOTE} she said${EM_DASH}${LEFT_DOUBLE_QUOTE}I don${RIGHT_SINGLE_QUOTE}t think that${RIGHT_SINGLE_QUOTE}s right.${RIGHT_DOUBLE_QUOTE}`
|
|
260
|
-
expect(transform(input, { nbsp: false })).toEqual(expected)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it("handles technical documentation", () => {
|
|
264
|
-
const input = 'The API returns x != y when a <= b and c >= d. Error tolerance: +-5%.'
|
|
265
|
-
const expected = `The API returns x ${NOT_EQUAL} y when a ${LESS_EQUAL} b and c ${GREATER_EQUAL} d. Error tolerance: ${PLUS_MINUS}5%.`
|
|
266
|
-
expect(transform(input, { nbsp: false })).toBe(expected)
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
it("handles measurement text", () => {
|
|
271
|
-
const input = 'He is 6\'2" tall.'
|
|
272
|
-
const expected = `He is 6${PRIME}2${DOUBLE_PRIME} tall.`
|
|
273
|
-
expect(transform(input, { symbols: true, nbsp: false })).toEqual(expected)
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it("handles copyright notices", () => {
|
|
277
|
-
const input = '(c) 2024 Company(tm). All rights reserved(r).'
|
|
278
|
-
const expected = `${COPYRIGHT} 2024 Company${TRADEMARK}. All rights reserved${REGISTERED}.`
|
|
279
|
-
expect(transform(input, { nbsp: false })).toEqual(expected)
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
describe("known limitations", () => {
|
|
284
|
-
// These document current behavior for edge cases
|
|
285
|
-
it("documents prime mark vs closing quote ambiguity", () => {
|
|
286
|
-
// When there's an open quote before a number, the quote after should close
|
|
287
|
-
// not become a prime mark
|
|
288
|
-
const input = '"Number 5"'
|
|
289
|
-
const result = transform(input, { nbsp: false })
|
|
290
|
-
// The 5" should be a closing quote, not a prime
|
|
291
|
-
expect(result).toBe(`${LEFT_DOUBLE_QUOTE}Number 5${RIGHT_DOUBLE_QUOTE}`)
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
describe("complex combined transformations", () => {
|
|
296
|
-
it.each([
|
|
297
|
-
[
|
|
298
|
-
'"Wait..." she said - "it\'s pages 1-5."',
|
|
299
|
-
`${LEFT_DOUBLE_QUOTE}Wait${ELLIPSIS}${RIGHT_DOUBLE_QUOTE} she said${EM_DASH}${LEFT_DOUBLE_QUOTE}it${RIGHT_SINGLE_QUOTE}s pages 1${EN_DASH}5.${RIGHT_DOUBLE_QUOTE}`,
|
|
300
|
-
],
|
|
301
|
-
[
|
|
302
|
-
"(c) 2024 - Room is 10x12, set to 72 F",
|
|
303
|
-
`${COPYRIGHT} 2024${EM_DASH}Room is 10${MULTIPLICATION}12, set to 72 ${DEGREE}F`,
|
|
304
|
-
],
|
|
305
|
-
])('handles complex transform: "%s"', (input, expected) => {
|
|
306
|
-
expect(transform(input, { degrees: true, nbsp: false })).toBe(expected)
|
|
307
|
-
})
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
describe("empty and whitespace inputs", () => {
|
|
311
|
-
it.each([
|
|
312
|
-
["", ""],
|
|
313
|
-
[" ", " "],
|
|
314
|
-
["\n", "\n"],
|
|
315
|
-
["\t", "\t"],
|
|
316
|
-
[" ", " "],
|
|
317
|
-
])('handles empty/whitespace: "%s"', (input, expected) => {
|
|
318
|
-
expect(transform(input, { nbsp: false })).toBe(expected)
|
|
319
|
-
})
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
describe("very long inputs", () => {
|
|
323
|
-
it("handles long repeated patterns", () => {
|
|
324
|
-
const input = '"Hello" '.repeat(100)
|
|
325
|
-
const expected = `${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE} `.repeat(100)
|
|
326
|
-
expect(transform(input, { nbsp: false })).toEqual(expected)
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it("handles long continuous text", () => {
|
|
330
|
-
const input = "word ".repeat(1000)
|
|
331
|
-
const result = transform(input, { nbsp: false })
|
|
332
|
-
expect(result).toBe(input.trim() + " ")
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
it("converts ALL quote pairs in repeated pattern", () => {
|
|
336
|
-
const count = 50
|
|
337
|
-
const input = '"Hello" '.repeat(count)
|
|
338
|
-
const result = transform(input, { nbsp: false })
|
|
339
|
-
const leftCount = (result.match(new RegExp(LEFT_DOUBLE_QUOTE, "g")) || []).length
|
|
340
|
-
const rightCount = (result.match(new RegExp(RIGHT_DOUBLE_QUOTE, "g")) || []).length
|
|
341
|
-
expect(leftCount).toBe(count)
|
|
342
|
-
expect(rightCount).toBe(count)
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
it("converts ALL dashes in repeated pattern", () => {
|
|
346
|
-
const count = 50
|
|
347
|
-
const input = "pages 1-5 ".repeat(count)
|
|
348
|
-
const result = transform(input, { nbsp: false })
|
|
349
|
-
const enDashCount = (result.match(new RegExp(EN_DASH, "g")) || []).length
|
|
350
|
-
expect(enDashCount).toBe(count)
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it("converts ALL ellipses in repeated pattern", () => {
|
|
354
|
-
const count = 50
|
|
355
|
-
const input = "Wait... ".repeat(count)
|
|
356
|
-
const result = transform(input, { nbsp: false })
|
|
357
|
-
const ellipsisCount = (result.match(new RegExp(ELLIPSIS, "g")) || []).length
|
|
358
|
-
expect(ellipsisCount).toBe(count)
|
|
359
|
-
})
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
describe("Unicode control characters", () => {
|
|
363
|
-
it.each([
|
|
364
|
-
['"test\u200Bword"', `${LEFT_DOUBLE_QUOTE}test\u200Bword${RIGHT_DOUBLE_QUOTE}`],
|
|
365
|
-
['"test\u200Fword"', `${LEFT_DOUBLE_QUOTE}test\u200Fword${RIGHT_DOUBLE_QUOTE}`],
|
|
366
|
-
['"caf\u0065\u0301"', `${LEFT_DOUBLE_QUOTE}caf\u0065\u0301${RIGHT_DOUBLE_QUOTE}`],
|
|
367
|
-
])('handles Unicode edge case', (input, expected) => {
|
|
368
|
-
expect(transform(input, { nbsp: false })).toBe(expected)
|
|
369
|
-
})
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
describe("option combinations", () => {
|
|
373
|
-
it("applies all optional transforms together", () => {
|
|
374
|
-
const input = "1st place: 1/2 at 72 F - wow!!"
|
|
375
|
-
const expected = `1${SUPERSCRIPT_ST} place: ${FRACTION_1_2} at 72 ${DEGREE}F${EM_DASH}wow!`
|
|
376
|
-
expect(transform(input, {
|
|
377
|
-
fractions: true,
|
|
378
|
-
degrees: true,
|
|
379
|
-
superscript: true,
|
|
380
|
-
ligatures: true,
|
|
381
|
-
nbsp: false,
|
|
382
|
-
})).toEqual(expected)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it("respects disabled transforms", () => {
|
|
386
|
-
const input = "1/2 at 72 F"
|
|
387
|
-
const expected = `${FRACTION_1_2} at 72 F`
|
|
388
|
-
expect(transform(input, {
|
|
389
|
-
fractions: true,
|
|
390
|
-
degrees: false,
|
|
391
|
-
nbsp: false,
|
|
392
|
-
})).toEqual(expected)
|
|
393
|
-
})
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
describe("separator robustness", () => {
|
|
397
|
-
const sep = DEFAULT_SEPARATOR
|
|
398
|
-
|
|
399
|
-
it.each([
|
|
400
|
-
[`${sep}"test"${sep}`, `${sep}${LEFT_DOUBLE_QUOTE}test${RIGHT_DOUBLE_QUOTE}${sep}`],
|
|
401
|
-
[`"test${sep}word"`, `${LEFT_DOUBLE_QUOTE}test${sep}word${RIGHT_DOUBLE_QUOTE}`],
|
|
402
|
-
[`${sep}${sep}"test"${sep}${sep}`, `${sep}${sep}${LEFT_DOUBLE_QUOTE}test${RIGHT_DOUBLE_QUOTE}${sep}${sep}`],
|
|
403
|
-
])('handles separator in quotes', (input, expected) => {
|
|
404
|
-
expect(transform(input, { separator: sep, nbsp: false })).toBe(expected)
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
it.each([
|
|
408
|
-
[`word${sep} - ${sep}word`, `word${sep}${EM_DASH}${sep}word`],
|
|
409
|
-
[`1${sep}-${sep}5`, `1${sep}${EN_DASH}${sep}5`],
|
|
410
|
-
])('handles separator in dashes', (input, expected) => {
|
|
411
|
-
expect(transform(input, { separator: sep, nbsp: false })).toBe(expected)
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
it("preserves exact separator count through transform", () => {
|
|
415
|
-
const input = `${sep}text${sep}more${sep}text${sep}`
|
|
416
|
-
const result = transform(input, { separator: sep, nbsp: false })
|
|
417
|
-
const inputCount = (input.match(new RegExp(sep, "g")) || []).length
|
|
418
|
-
const resultCount = (result.match(new RegExp(sep, "g")) || []).length
|
|
419
|
-
expect(resultCount).toBe(inputCount)
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
it("handles input containing separator character", () => {
|
|
423
|
-
const input = `Text with ${sep} in it`
|
|
424
|
-
expect(() => transform(input, { separator: sep, nbsp: false })).not.toThrow()
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
it("handles separator in fractions", () => {
|
|
428
|
-
const input = `1${sep}/2`
|
|
429
|
-
expect(transform(input, { separator: sep, fractions: true, nbsp: false })).toBe(`${sep}${FRACTION_1_2}`)
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
it("handles separator in degrees", () => {
|
|
433
|
-
const input = `72${sep} F`
|
|
434
|
-
expect(transform(input, { separator: sep, degrees: true, nbsp: false })).toBe(`72${sep} ${DEGREE}F`)
|
|
435
|
-
})
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
describe("additional idempotency", () => {
|
|
439
|
-
it.each([
|
|
440
|
-
'"Hello" - she said, "it\'s pages 1-5..."',
|
|
441
|
-
'Product(tm) 5x5 at 72 F (c) 2024',
|
|
442
|
-
"Add 1/2 cup - that's about 3/4 done",
|
|
443
|
-
'1st place winner said "congrats!!"',
|
|
444
|
-
])('is idempotent: "%s"', (input) => {
|
|
445
|
-
const first = transform(input, { fractions: true, degrees: true, superscript: true, ligatures: true })
|
|
446
|
-
const second = transform(first, { fractions: true, degrees: true, superscript: true, ligatures: true })
|
|
447
|
-
expect(second).toBe(first)
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
it.each([
|
|
451
|
-
// Nested quotes with period — tests the multi-quote-jump fix
|
|
452
|
-
[`"She said, 'Hello.'"`, "british" as const],
|
|
453
|
-
[`"She said, 'Hello.'"`, "german" as const],
|
|
454
|
-
[`"She said, 'Hello.'"`, "french" as const],
|
|
455
|
-
// Leading apostrophe — tests the German RSQ normalization fix
|
|
456
|
-
[`'Twas the night before Christmas.`, "german" as const],
|
|
457
|
-
])('is idempotent across styles: "%s" [%s]', (input, style) => {
|
|
458
|
-
const first = transform(input, { punctuationStyle: style })
|
|
459
|
-
const second = transform(first, { punctuationStyle: style })
|
|
460
|
-
expect(second).toBe(first)
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
it.each([
|
|
464
|
-
`${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}`,
|
|
465
|
-
`word${EM_DASH}word`,
|
|
466
|
-
`1${EN_DASH}5`,
|
|
467
|
-
`Wait${ELLIPSIS}`,
|
|
468
|
-
`5${MULTIPLICATION}5`,
|
|
469
|
-
`20${NBSP}${DEGREE}C`,
|
|
470
|
-
`${COPYRIGHT}${NBSP}2024`,
|
|
471
|
-
])('stable for: "%s"', (input) => {
|
|
472
|
-
expect(transform(input)).toBe(input)
|
|
473
|
-
})
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
describe("style combinations", () => {
|
|
477
|
-
it("applies American conventions throughout", () => {
|
|
478
|
-
const input = '"Hello." - word - "World."'
|
|
479
|
-
const expected = `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE}${EM_DASH}word${EM_DASH}${LEFT_DOUBLE_QUOTE}World.${RIGHT_DOUBLE_QUOTE}`
|
|
480
|
-
expect(transform(input, { punctuationStyle: "american", dashStyle: "american", nbsp: false })).toEqual(expected)
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
it("applies British conventions throughout", () => {
|
|
484
|
-
const input = '"Hello." - word - "World."'
|
|
485
|
-
const expected = `${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}. ${EN_DASH} word ${EN_DASH} ${LEFT_DOUBLE_QUOTE}World${RIGHT_DOUBLE_QUOTE}.`
|
|
486
|
-
expect(transform(input, { punctuationStyle: "british", dashStyle: "british", nbsp: false })).toEqual(expected)
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
it("applies American punctuation with British dashes", () => {
|
|
490
|
-
const input = '"Hello." - word'
|
|
491
|
-
const expected = `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE} ${EN_DASH} word`
|
|
492
|
-
expect(transform(input, { punctuationStyle: "american", dashStyle: "british", nbsp: false })).toEqual(expected)
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
it("skips all dash transforms with dashStyle none", () => {
|
|
496
|
-
const input = '"Hello" - pages 1-5, -3'
|
|
497
|
-
const expected = `${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE} - pages 1-5, -3`
|
|
498
|
-
expect(transform(input, { dashStyle: "none", nbsp: false })).toEqual(expected)
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
it("skips everything with both set to none", () => {
|
|
502
|
-
const input = '"Hello" - pages 1-5'
|
|
503
|
-
expect(transform(input, { punctuationStyle: "none", dashStyle: "none", nbsp: false })).toBe(input)
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
it("skips quotes but applies dashes with mixed none", () => {
|
|
507
|
-
const input = '"Hello" - pages 1-5'
|
|
508
|
-
const expected = `"Hello"${EM_DASH}pages 1${EN_DASH}5`
|
|
509
|
-
expect(transform(input, { punctuationStyle: "none", dashStyle: "american", nbsp: false })).toEqual(expected)
|
|
510
|
-
})
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
describe("regex performance", () => {
|
|
514
|
-
it("scales linearly for pathological quote patterns", () => {
|
|
515
|
-
assertLinearScaling(transform, (n) => '"a"'.repeat(n) + "'")
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
it("scales linearly for pathological dash patterns", () => {
|
|
519
|
-
assertLinearScaling(transform, (n) => "1" + "-1".repeat(n))
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
it("scales linearly for unbalanced single quotes", () => {
|
|
523
|
-
assertLinearScaling(transform, (n) => "'".repeat(n) + "a")
|
|
524
|
-
})
|
|
525
|
-
})
|
|
526
|
-
|
|
527
|
-
describe("special regex characters in input", () => {
|
|
528
|
-
it.each([
|
|
529
|
-
['"test [bracket]"', `${LEFT_DOUBLE_QUOTE}test [bracket]${RIGHT_DOUBLE_QUOTE}`],
|
|
530
|
-
['"test (paren)"', `${LEFT_DOUBLE_QUOTE}test (paren)${RIGHT_DOUBLE_QUOTE}`],
|
|
531
|
-
['"test {brace}"', `${LEFT_DOUBLE_QUOTE}test {brace}${RIGHT_DOUBLE_QUOTE}`],
|
|
532
|
-
['"test $dollar"', `${LEFT_DOUBLE_QUOTE}test $dollar${RIGHT_DOUBLE_QUOTE}`],
|
|
533
|
-
['"test ^caret"', `${LEFT_DOUBLE_QUOTE}test ^caret${RIGHT_DOUBLE_QUOTE}`],
|
|
534
|
-
['"test .dot"', `${LEFT_DOUBLE_QUOTE}test .dot${RIGHT_DOUBLE_QUOTE}`],
|
|
535
|
-
['"test |pipe"', `${LEFT_DOUBLE_QUOTE}test |pipe${RIGHT_DOUBLE_QUOTE}`],
|
|
536
|
-
])('handles regex special chars: "%s"', (input, expected) => {
|
|
537
|
-
expect(transform(input, { nbsp: false })).toBe(expected)
|
|
538
|
-
})
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
describe("boundary conditions", () => {
|
|
542
|
-
it.each([
|
|
543
|
-
['"start', `${LEFT_DOUBLE_QUOTE}start`],
|
|
544
|
-
["'start", `${RIGHT_SINGLE_QUOTE}start`],
|
|
545
|
-
["-5 start", `${MINUS}5 start`],
|
|
546
|
-
["...start", `${ELLIPSIS} start`],
|
|
547
|
-
['end"', `end${RIGHT_DOUBLE_QUOTE}`],
|
|
548
|
-
["end'", `end${RIGHT_SINGLE_QUOTE}`],
|
|
549
|
-
["end...", `end${ELLIPSIS}`],
|
|
550
|
-
["5x", `5${MULTIPLICATION}`],
|
|
551
|
-
])('handles boundary: "%s"', (input, expected) => {
|
|
552
|
-
expect(transform(input, { nbsp: false })).toBe(expected)
|
|
553
|
-
})
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
describe("separator validation", () => {
|
|
557
|
-
const invalidSeparators = [
|
|
558
|
-
["🎉", "emoji (non-BMP)"],
|
|
559
|
-
["", "empty string"],
|
|
560
|
-
] as const
|
|
561
|
-
|
|
562
|
-
it.each(invalidSeparators)("rejects %s separator (%s)", (sep) => {
|
|
563
|
-
expect(() => transform('"Hello"', { separator: sep, nbsp: false })).toThrow(
|
|
564
|
-
/Invalid separator/
|
|
565
|
-
)
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
const validSeparators = ["\uE000\uE001", "\uE000", "|", "\u2603", "ab"] // Default, single PUA, pipe, snowman, multi-char
|
|
569
|
-
|
|
570
|
-
it.each(validSeparators)("accepts '%s' as separator", (sep) => {
|
|
571
|
-
expect(() => transform('"Hello"', { separator: sep, nbsp: false })).not.toThrow()
|
|
572
|
-
})
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
describe("regex-special separator characters", () => {
|
|
576
|
-
it.each(REGEX_SPECIAL_CHARS)("handles '%s' as separator", (sep) => {
|
|
577
|
-
expect(transform('"Hello"', { separator: sep, nbsp: false })).toEqual(
|
|
578
|
-
`${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}`
|
|
579
|
-
)
|
|
580
|
-
})
|
|
581
|
-
})
|
|
582
|
-
|
|
583
|
-
describe("multi-character separator behavior", () => {
|
|
584
|
-
const sep = "\uE000\uE001"
|
|
585
|
-
|
|
586
|
-
it.each([
|
|
587
|
-
["quotes with separator at boundary", `${sep}"Hello"`, `${sep}${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}`],
|
|
588
|
-
["em-dash with adjacent separators", `word${sep} - ${sep}word`, `word${sep}${EM_DASH}${sep}word`],
|
|
589
|
-
["ellipsis with separator between dots", `a.${sep}.${sep}.`, `a\u2026${sep}${sep}`],
|
|
590
|
-
["nbsp before last word with trailing separator", `Hello world${sep}`, `Hello${NBSP}world${sep}`],
|
|
591
|
-
])("%s", (_desc, input, expected) => {
|
|
592
|
-
expect(transform(input, { separator: sep, nbsp: true })).toBe(expected)
|
|
593
|
-
})
|
|
594
|
-
|
|
595
|
-
it("preserves separator count through transform", () => {
|
|
596
|
-
const input = `${sep}"Hello"${sep} -- ${sep}world${sep}`
|
|
597
|
-
const result = transform(input, { separator: sep })
|
|
598
|
-
expect(countSeparators(result, sep)).toBe(countSeparators(input, sep))
|
|
599
|
-
})
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
describe("Unicode preservation", () => {
|
|
603
|
-
it.each([
|
|
604
|
-
["emoji", '"Hello 👋 World"', `${LEFT_DOUBLE_QUOTE}Hello 👋 World${RIGHT_DOUBLE_QUOTE}`],
|
|
605
|
-
["RTL text", '"שלום"', `${LEFT_DOUBLE_QUOTE}שלום${RIGHT_DOUBLE_QUOTE}`],
|
|
606
|
-
["zero-width space", '"Hello\u200BWorld"', `${LEFT_DOUBLE_QUOTE}Hello\u200BWorld${RIGHT_DOUBLE_QUOTE}`],
|
|
607
|
-
] as const)("preserves %s in input", (_, input, expected) => {
|
|
608
|
-
expect(transform(input, { nbsp: false })).toEqual(expected)
|
|
609
|
-
})
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
describe("large input handling", () => {
|
|
613
|
-
it("scales linearly on seeded mixed content", () => {
|
|
614
|
-
assertLinearScaling(transform, (n) => buildMixedContent(n * 100), 50)
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
it("scales linearly with all features enabled", () => {
|
|
618
|
-
const allFeatures = { fractions: true, degrees: true, superscript: true, ligatures: true }
|
|
619
|
-
assertLinearScaling((input) => transform(input, allFeatures), (n) => buildMixedContent(n * 100), 50)
|
|
620
|
-
})
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
describe("idempotency across punctuation styles", () => {
|
|
624
|
-
const mixedInput = `"Hello," she said -- "it's pages 1-5." Wait... 5x5 != 25. He's 5'10" tall.`
|
|
625
|
-
|
|
626
|
-
it.each([
|
|
627
|
-
["american"],
|
|
628
|
-
["british"],
|
|
629
|
-
["german"],
|
|
630
|
-
["french"],
|
|
631
|
-
] as const)("idempotent with %s style", (style) => {
|
|
632
|
-
const opts = { punctuationStyle: style, checkIdempotency: false, nbsp: false } as const
|
|
633
|
-
const first = transform(mixedInput, opts)
|
|
634
|
-
const second = transform(first, opts)
|
|
635
|
-
expect(second).toBe(first)
|
|
636
|
-
})
|
|
637
|
-
})
|
|
638
|
-
|
|
639
|
-
describe("edge case inputs", () => {
|
|
640
|
-
it.each([
|
|
641
|
-
["empty string", "", ""],
|
|
642
|
-
["single space", " ", " "],
|
|
643
|
-
["newlines only", "\n\n\n", "\n\n\n"],
|
|
644
|
-
["tab only", "\t", "\t"],
|
|
645
|
-
["nbsp only", NBSP, NBSP],
|
|
646
|
-
])("handles %s", (_desc, input, expected) => {
|
|
647
|
-
expect(transform(input)).toBe(expected)
|
|
648
|
-
})
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
describe("separator collision", () => {
|
|
652
|
-
it("preserves default separator in plain transform", () => {
|
|
653
|
-
// transform() itself uses the separator internally but tolerates it in input
|
|
654
|
-
// The assertSeparatorAbsent check is in the rehype/remark plugins
|
|
655
|
-
const input = `text${DEFAULT_SEPARATOR}more`
|
|
656
|
-
const result = transform(input)
|
|
657
|
-
expect(countSeparators(result, DEFAULT_SEPARATOR)).toBe(1)
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
it("does not throw for single char of multi-char default separator", () => {
|
|
661
|
-
expect(() => transform(`text${DEFAULT_SEPARATOR[0]}more`)).not.toThrow()
|
|
662
|
-
})
|
|
663
|
-
})
|
|
664
|
-
|
|
665
|
-
describe("regex cache thrashing", () => {
|
|
666
|
-
it("produces correct results after exceeding cache capacity", () => {
|
|
667
|
-
const input = `"Hello," she said.`
|
|
668
|
-
const separatorCount = MAX_REGEX_CACHE_SIZE + 200
|
|
669
|
-
for (let i = 0; i < separatorCount; i++) {
|
|
670
|
-
const sep = String.fromCharCode(0xE100 + i)
|
|
671
|
-
const result = transform(input, { separator: sep, nbsp: false })
|
|
672
|
-
expect(result).toBe(`${LEFT_DOUBLE_QUOTE}Hello,${RIGHT_DOUBLE_QUOTE} she said.`)
|
|
673
|
-
}
|
|
674
|
-
})
|
|
675
|
-
})
|
|
676
|
-
|
|
677
|
-
describe("ReDoS resistance", () => {
|
|
678
|
-
it.each([
|
|
679
|
-
["single quotes", (n: number) => "'".repeat(n)],
|
|
680
|
-
["double quotes", (n: number) => '"'.repeat(n)],
|
|
681
|
-
["unbalanced quotes", (n: number) => '"Hello '.repeat(n)],
|
|
682
|
-
["hyphens", (n: number) => "-".repeat(n)],
|
|
683
|
-
["alternating digits/hyphens", (n: number) => "1-2-3-4-5-6-7-8-9-0".repeat(n / 10)],
|
|
684
|
-
["dots", (n: number) => ".".repeat(n)],
|
|
685
|
-
["fraction-like patterns", (n: number) => "1/2 ".repeat(n)],
|
|
686
|
-
["short words (nbsp)", (n: number) => "a b c d e f ".repeat(n)],
|
|
687
|
-
["math symbol patterns", (n: number) => "!= ".repeat(n)],
|
|
688
|
-
] as const)("scales linearly for %s", (_, buildInput) => {
|
|
689
|
-
assertLinearScaling(transform, buildInput)
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
it("scales linearly on seeded mixed content", () => {
|
|
693
|
-
assertLinearScaling(transform, (n) => buildMixedContent(n * 10))
|
|
694
|
-
})
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
describe("edge cases", () => {
|
|
698
|
-
it("handles empty string input", () => {
|
|
699
|
-
expect(transform("")).toBe("")
|
|
700
|
-
expect(transform("", { nbsp: false })).toBe("")
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
it("treats undefined option values as absent (uses defaults)", () => {
|
|
704
|
-
const input = '"Hello," she said.'
|
|
705
|
-
const withDefault = transform(input)
|
|
706
|
-
expect(transform(input, { nbsp: undefined })).toBe(withDefault)
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
it("scales linearly for long non-transformable content", () => {
|
|
710
|
-
assertLinearScaling(transform, (n) => "abcdefghij ".repeat(n))
|
|
711
|
-
})
|
|
712
|
-
})
|
|
713
|
-
})
|