punctilio 2.3.0 → 3.0.1
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 +1 -2
- package/dist/dashes.js +4 -4
- package/dist/dashes.js.map +1 -1
- package/dist/index.d.ts +1 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -16
- package/dist/index.js.map +1 -1
- package/dist/quotes.d.ts +11 -11
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +21 -7
- package/dist/quotes.js.map +1 -1
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +9 -4
- package/dist/rehype.js.map +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +17 -22
- package/dist/symbols.js.map +1 -1
- package/package.json +1 -1
- package/src/dashes.ts +4 -4
- package/src/index.ts +5 -28
- package/src/quotes.ts +22 -18
- package/src/rehype.ts +8 -4
- package/src/symbols.ts +28 -22
- package/src/tests/dashes.test.ts +11 -0
- package/src/tests/index.test.ts +32 -18
- package/src/tests/nbsp.test.ts +9 -9
- package/src/tests/quotes.test.ts +46 -49
- package/src/tests/rehype.test.ts +9 -8
- package/src/tests/remark.test.ts +1 -1
- package/src/tests/symbols.test.ts +16 -6
package/src/tests/quotes.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { niceQuotes } from "../quotes.js"
|
|
1
|
+
import { niceQuotes, classifyApostrophes } from "../quotes.js"
|
|
2
2
|
import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR } from "../constants.js"
|
|
3
3
|
|
|
4
4
|
const {
|
|
@@ -11,9 +11,6 @@ const {
|
|
|
11
11
|
ELLIPSIS,
|
|
12
12
|
} = UNICODE_SYMBOLS
|
|
13
13
|
|
|
14
|
-
/** Enable MLA output for tests that assert the apostrophe codepoint. */
|
|
15
|
-
const MLA = { useModifierLetterApostrophe: true } as const
|
|
16
|
-
|
|
17
14
|
describe("niceQuotes", () => {
|
|
18
15
|
describe("double quotes", () => {
|
|
19
16
|
it.each([
|
|
@@ -52,7 +49,7 @@ describe("niceQuotes", () => {
|
|
|
52
49
|
// End-of-line quote becomes RIGHT quote
|
|
53
50
|
['not confident in that plan - "', `not confident in that plan - ${RIGHT_DOUBLE_QUOTE}`],
|
|
54
51
|
])('should convert double quotes in "%s"', (input, expected) => {
|
|
55
|
-
expect(
|
|
52
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
56
53
|
})
|
|
57
54
|
})
|
|
58
55
|
|
|
@@ -117,7 +114,7 @@ describe("niceQuotes", () => {
|
|
|
117
114
|
// Closing RSQ then opening LDQ
|
|
118
115
|
[`'hello' "world"`, `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} ${LEFT_DOUBLE_QUOTE}world${RIGHT_DOUBLE_QUOTE}`],
|
|
119
116
|
])('should handle single quotes/apostrophes in "%s"', (input, expected) => {
|
|
120
|
-
expect(
|
|
117
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
121
118
|
})
|
|
122
119
|
})
|
|
123
120
|
|
|
@@ -139,7 +136,7 @@ describe("niceQuotes", () => {
|
|
|
139
136
|
["'dogs'", `${LEFT_SINGLE_QUOTE}dogs${RIGHT_SINGLE_QUOTE}`],
|
|
140
137
|
["'he tells' stories", `${LEFT_SINGLE_QUOTE}he tells${RIGHT_SINGLE_QUOTE} stories`],
|
|
141
138
|
])('handles plural possessive: "%s"', (input, expected) => {
|
|
142
|
-
expect(
|
|
139
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
143
140
|
})
|
|
144
141
|
|
|
145
142
|
it.each([
|
|
@@ -147,8 +144,8 @@ describe("niceQuotes", () => {
|
|
|
147
144
|
`Bayes${MODIFIER_LETTER_APOSTROPHE} rule`,
|
|
148
145
|
`dogs${MODIFIER_LETTER_APOSTROPHE} and ${LEFT_SINGLE_QUOTE}cats${RIGHT_SINGLE_QUOTE}`,
|
|
149
146
|
])('plural possessive is idempotent: "%s"', (input) => {
|
|
150
|
-
expect(
|
|
151
|
-
expect(
|
|
147
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
148
|
+
expect(classifyApostrophes(classifyApostrophes(input))).toBe(input)
|
|
152
149
|
})
|
|
153
150
|
|
|
154
151
|
it("stress: many possessives interleaved with quotes", () => {
|
|
@@ -157,7 +154,7 @@ describe("niceQuotes", () => {
|
|
|
157
154
|
i % 2 === 0 ? `dogs'` : `'yes'`
|
|
158
155
|
)
|
|
159
156
|
const input = pairs.join(" ")
|
|
160
|
-
const result =
|
|
157
|
+
const result = classifyApostrophes(input)
|
|
161
158
|
// Every dogs' → MLA, every 'yes' → LSQ+yes+RSQ
|
|
162
159
|
const expectedParts = Array.from({ length: 50 }, (_, i) =>
|
|
163
160
|
i % 2 === 0
|
|
@@ -165,19 +162,19 @@ describe("niceQuotes", () => {
|
|
|
165
162
|
: `${LEFT_SINGLE_QUOTE}yes${RIGHT_SINGLE_QUOTE}`
|
|
166
163
|
)
|
|
167
164
|
expect(result).toBe(expectedParts.join(" "))
|
|
168
|
-
expect(
|
|
165
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
169
166
|
})
|
|
170
167
|
|
|
171
168
|
it("stress: 200 consecutive plural possessives", () => {
|
|
172
169
|
const words = ["dogs'", "cats'", "Bayes'", "Thomas'", "PLAYERS'"]
|
|
173
170
|
const input = Array.from({ length: 200 }, (_, i) => words[i % words.length]).join(" ")
|
|
174
171
|
const start = performance.now()
|
|
175
|
-
const result =
|
|
172
|
+
const result = classifyApostrophes(input)
|
|
176
173
|
expect(performance.now() - start).toBeLessThan(1000)
|
|
177
174
|
// All should be MLA (no LSQ to pair with)
|
|
178
175
|
expect(result).not.toContain(RIGHT_SINGLE_QUOTE)
|
|
179
176
|
expect(result).not.toContain("'")
|
|
180
|
-
expect(
|
|
177
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
181
178
|
})
|
|
182
179
|
|
|
183
180
|
it("stress: deeply nested alternating pattern", () => {
|
|
@@ -186,7 +183,7 @@ describe("niceQuotes", () => {
|
|
|
186
183
|
const input = Array.from({ length: 30 }, (_, i) =>
|
|
187
184
|
i % 2 === 0 ? `'word${i}'` : `${possessives[i % possessives.length]}'`
|
|
188
185
|
).join(" ")
|
|
189
|
-
const result =
|
|
186
|
+
const result = classifyApostrophes(input)
|
|
190
187
|
// Quoted words get LSQ/RSQ, possessives get MLA
|
|
191
188
|
for (let i = 0; i < 30; i++) {
|
|
192
189
|
if (i % 2 === 0) {
|
|
@@ -195,7 +192,7 @@ describe("niceQuotes", () => {
|
|
|
195
192
|
expect(result).toContain(`${possessives[i % possessives.length]}${MODIFIER_LETTER_APOSTROPHE}`)
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
|
-
expect(
|
|
195
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
199
196
|
})
|
|
200
197
|
})
|
|
201
198
|
|
|
@@ -214,11 +211,11 @@ describe("niceQuotes", () => {
|
|
|
214
211
|
["'twas the night, 'twas indeed", `${MODIFIER_LETTER_APOSTROPHE}twas the night, ${MODIFIER_LETTER_APOSTROPHE}twas indeed`],
|
|
215
212
|
[`"'twas the night"`, `${LEFT_DOUBLE_QUOTE}${MODIFIER_LETTER_APOSTROPHE}twas the night${RIGHT_DOUBLE_QUOTE}`],
|
|
216
213
|
])('handles leading apostrophe in "%s"', (input, expected) => {
|
|
217
|
-
expect(
|
|
214
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
218
215
|
})
|
|
219
216
|
|
|
220
217
|
it("multiple decades in one sentence", () => {
|
|
221
|
-
expect(
|
|
218
|
+
expect(classifyApostrophes("The '60s, '70s, and '80s were different.")).toBe(
|
|
222
219
|
`The ${MODIFIER_LETTER_APOSTROPHE}60s, ${MODIFIER_LETTER_APOSTROPHE}70s, and ${MODIFIER_LETTER_APOSTROPHE}80s were different.`
|
|
223
220
|
)
|
|
224
221
|
})
|
|
@@ -231,7 +228,7 @@ describe("niceQuotes", () => {
|
|
|
231
228
|
["digit neighbors (\\w includes digits)", "1 'n' 2", `1 ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} 2`],
|
|
232
229
|
["known false positive: quoted letter", "the letter 'n' is common", `the letter ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} is common`],
|
|
233
230
|
])('%s', (_label, input, expected) => {
|
|
234
|
-
expect(
|
|
231
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
235
232
|
})
|
|
236
233
|
|
|
237
234
|
it.each([
|
|
@@ -240,7 +237,7 @@ describe("niceQuotes", () => {
|
|
|
240
237
|
["punctuation precedes first apostrophe", "said, 'n' is good", `said, ${LEFT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE} is good`],
|
|
241
238
|
["newline replaces space", "Rock 'n'\nRoll", `Rock ${LEFT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE}\nRoll`],
|
|
242
239
|
])('does not produce MLA-n-MLA: %s', (_label, input, expected) => {
|
|
243
|
-
expect(
|
|
240
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
244
241
|
})
|
|
245
242
|
})
|
|
246
243
|
|
|
@@ -277,7 +274,7 @@ describe("niceQuotes", () => {
|
|
|
277
274
|
[`O'Brien's idea`, `O${MODIFIER_LETTER_APOSTROPHE}Brien${MODIFIER_LETTER_APOSTROPHE}s idea`],
|
|
278
275
|
[`The O'Connors`, `The O${MODIFIER_LETTER_APOSTROPHE}Connors`],
|
|
279
276
|
])('handles Irish/Scottish name apostrophes: "%s"', (input, expected) => {
|
|
280
|
-
expect(
|
|
277
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
281
278
|
})
|
|
282
279
|
|
|
283
280
|
// From smartquotes.js: complex nested quotes
|
|
@@ -311,7 +308,7 @@ describe("niceQuotes", () => {
|
|
|
311
308
|
// Quote with slash separator
|
|
312
309
|
['"option1"/"option2"', `${LEFT_DOUBLE_QUOTE}option1${RIGHT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}option2${RIGHT_DOUBLE_QUOTE}`],
|
|
313
310
|
])('handles complex pattern: "%s"', (input, expected) => {
|
|
314
|
-
expect(
|
|
311
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
315
312
|
})
|
|
316
313
|
|
|
317
314
|
it("handles multi-line dialogue", () => {
|
|
@@ -337,8 +334,8 @@ describe("niceQuotes", () => {
|
|
|
337
334
|
// Mixed quote types
|
|
338
335
|
`${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} ${LEFT_DOUBLE_QUOTE}world${RIGHT_DOUBLE_QUOTE}`,
|
|
339
336
|
])('is idempotent for: "%s"', (input) => {
|
|
340
|
-
expect(
|
|
341
|
-
expect(
|
|
337
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
338
|
+
expect(classifyApostrophes(classifyApostrophes(input))).toBe(input)
|
|
342
339
|
})
|
|
343
340
|
})
|
|
344
341
|
|
|
@@ -357,21 +354,21 @@ describe("niceQuotes", () => {
|
|
|
357
354
|
// Closing after separator
|
|
358
355
|
[`'hello${sep}'`, `${LEFT_SINGLE_QUOTE}hello${sep}${RIGHT_SINGLE_QUOTE}`, "closing quote after separator"],
|
|
359
356
|
])("%s → %s (%s)", (input, expected) => {
|
|
360
|
-
expect(
|
|
357
|
+
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
361
358
|
})
|
|
362
359
|
|
|
363
360
|
it("plural possessive across separator", () => {
|
|
364
361
|
const input = `dogs${sep}' food`
|
|
365
362
|
const expected = `dogs${sep}${MODIFIER_LETTER_APOSTROPHE} food`
|
|
366
|
-
expect(
|
|
367
|
-
expect(
|
|
363
|
+
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
364
|
+
expect(classifyApostrophes(expected, { separator: sep })).toBe(expected)
|
|
368
365
|
})
|
|
369
366
|
|
|
370
367
|
it("'n' with separators around word boundaries", () => {
|
|
371
368
|
const input = `Rock${sep} 'n' ${sep}Roll`
|
|
372
369
|
const expected = `Rock${sep} ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} ${sep}Roll`
|
|
373
|
-
expect(
|
|
374
|
-
expect(
|
|
370
|
+
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
371
|
+
expect(classifyApostrophes(expected, { separator: sep })).toBe(expected)
|
|
375
372
|
})
|
|
376
373
|
})
|
|
377
374
|
|
|
@@ -387,7 +384,7 @@ describe("niceQuotes", () => {
|
|
|
387
384
|
["'a", `${MODIFIER_LETTER_APOSTROPHE}a`],
|
|
388
385
|
["a'", `a${RIGHT_SINGLE_QUOTE}`],
|
|
389
386
|
])('handles edge quote pattern: "%s"', (input, expected) => {
|
|
390
|
-
expect(
|
|
387
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
391
388
|
})
|
|
392
389
|
})
|
|
393
390
|
|
|
@@ -411,7 +408,7 @@ describe("niceQuotes", () => {
|
|
|
411
408
|
["wouldn't've", `wouldn${MODIFIER_LETTER_APOSTROPHE}t${MODIFIER_LETTER_APOSTROPHE}ve`],
|
|
412
409
|
["couldn't've", `couldn${MODIFIER_LETTER_APOSTROPHE}t${MODIFIER_LETTER_APOSTROPHE}ve`],
|
|
413
410
|
])('handles double contraction: "%s"', (input, expected) => {
|
|
414
|
-
expect(
|
|
411
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
415
412
|
})
|
|
416
413
|
})
|
|
417
414
|
|
|
@@ -424,7 +421,7 @@ describe("niceQuotes", () => {
|
|
|
424
421
|
// Newline stops lookahead → MLA (not LSQ) since no closing quote visible
|
|
425
422
|
["'hello\nworld'", `${MODIFIER_LETTER_APOSTROPHE}hello\nworld${RIGHT_SINGLE_QUOTE}`],
|
|
426
423
|
])('handles multiline: "%s"', (input, expected) => {
|
|
427
|
-
expect(
|
|
424
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
428
425
|
})
|
|
429
426
|
})
|
|
430
427
|
|
|
@@ -490,7 +487,7 @@ describe("niceQuotes", () => {
|
|
|
490
487
|
// British: RSQ period moves outside (contrast)
|
|
491
488
|
[`${LEFT_SINGLE_QUOTE}hello.${RIGHT_SINGLE_QUOTE}`, `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE}.`, "british"],
|
|
492
489
|
])('%s → %s (%s)', (input, expected, style) => {
|
|
493
|
-
expect(
|
|
490
|
+
expect(classifyApostrophes(input, { punctuationStyle: style as "american" | "british" })).toBe(expected)
|
|
494
491
|
})
|
|
495
492
|
})
|
|
496
493
|
|
|
@@ -518,7 +515,7 @@ describe("niceQuotes", () => {
|
|
|
518
515
|
})
|
|
519
516
|
})
|
|
520
517
|
|
|
521
|
-
describe("
|
|
518
|
+
describe("niceQuotes vs classifyApostrophes", () => {
|
|
522
519
|
const RSQ = RIGHT_SINGLE_QUOTE
|
|
523
520
|
|
|
524
521
|
it.each([
|
|
@@ -533,8 +530,8 @@ describe("niceQuotes", () => {
|
|
|
533
530
|
expect(niceQuotes(input)).toBe(expected)
|
|
534
531
|
})
|
|
535
532
|
|
|
536
|
-
it("uses MLA
|
|
537
|
-
expect(
|
|
533
|
+
it("classifyApostrophes uses MLA for apostrophes", () => {
|
|
534
|
+
expect(classifyApostrophes("don't")).toBe(`don${MODIFIER_LETTER_APOSTROPHE}t`)
|
|
538
535
|
})
|
|
539
536
|
|
|
540
537
|
it("RSQ output is idempotent", () => {
|
|
@@ -545,11 +542,11 @@ describe("niceQuotes", () => {
|
|
|
545
542
|
}
|
|
546
543
|
})
|
|
547
544
|
|
|
548
|
-
it("
|
|
545
|
+
it("classifyApostrophes output is idempotent", () => {
|
|
549
546
|
const inputs = ["don't", "the dog's", "the dogs'", "O'Brien", "Rock 'n' Roll", "'90s"]
|
|
550
547
|
for (const input of inputs) {
|
|
551
|
-
const first =
|
|
552
|
-
expect(
|
|
548
|
+
const first = classifyApostrophes(input)
|
|
549
|
+
expect(classifyApostrophes(first)).toBe(first)
|
|
553
550
|
}
|
|
554
551
|
})
|
|
555
552
|
|
|
@@ -563,12 +560,12 @@ describe("niceQuotes", () => {
|
|
|
563
560
|
// Mixed MLA + RSQ in same text
|
|
564
561
|
[`I${MODIFIER_LETTER_APOSTROPHE}m fine and you${RIGHT_SINGLE_QUOTE}re great`, `I${MODIFIER_LETTER_APOSTROPHE}m fine and you${MODIFIER_LETTER_APOSTROPHE}re great`],
|
|
565
562
|
])('normalizes RSQ contraction "%s"', (input, expected) => {
|
|
566
|
-
expect(
|
|
563
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
567
564
|
})
|
|
568
565
|
|
|
569
566
|
it("does NOT normalize RSQ when it is a closing quote", () => {
|
|
570
567
|
const input = `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} world`
|
|
571
|
-
expect(
|
|
568
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
572
569
|
})
|
|
573
570
|
|
|
574
571
|
it.each([
|
|
@@ -578,20 +575,20 @@ describe("niceQuotes", () => {
|
|
|
578
575
|
[`${LEFT_SINGLE_QUOTE}Stop!${RIGHT_SINGLE_QUOTE}`, "exclamation"],
|
|
579
576
|
[`${LEFT_SINGLE_QUOTE}test,${RIGHT_SINGLE_QUOTE} more`, "comma"],
|
|
580
577
|
])("preserves closing RSQ preceded by non-word char (%s)", (input) => {
|
|
581
|
-
expect(
|
|
578
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
582
579
|
})
|
|
583
580
|
|
|
584
581
|
it("normalizes RSQ possessive with digit prefix", () => {
|
|
585
|
-
expect(
|
|
582
|
+
expect(classifyApostrophes(`GPT-2${RIGHT_SINGLE_QUOTE}s`)).toBe(`GPT-2${MODIFIER_LETTER_APOSTROPHE}s`)
|
|
586
583
|
})
|
|
587
584
|
|
|
588
585
|
it("normalizes RSQ plural possessive to MLA", () => {
|
|
589
|
-
expect(
|
|
586
|
+
expect(classifyApostrophes(`the dogs${RIGHT_SINGLE_QUOTE} owner`)).toBe(`the dogs${MODIFIER_LETTER_APOSTROPHE} owner`)
|
|
590
587
|
})
|
|
591
588
|
|
|
592
589
|
it("preserves RSQ plural possessive when matched with LSQ", () => {
|
|
593
590
|
const input = `${LEFT_SINGLE_QUOTE}dogs${RIGHT_SINGLE_QUOTE} and more`
|
|
594
|
-
expect(
|
|
591
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
595
592
|
})
|
|
596
593
|
})
|
|
597
594
|
})
|
|
@@ -600,24 +597,24 @@ describe("niceQuotes", () => {
|
|
|
600
597
|
it("handles 1500-char input without closing quote", () => {
|
|
601
598
|
const input = `'${"a".repeat(1500)}`
|
|
602
599
|
const start = performance.now()
|
|
603
|
-
const result =
|
|
600
|
+
const result = classifyApostrophes(input)
|
|
604
601
|
expect(performance.now() - start).toBeLessThan(1000)
|
|
605
|
-
expect(
|
|
602
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
606
603
|
})
|
|
607
604
|
|
|
608
605
|
it("handles 16 rapid apostrophes", () => {
|
|
609
606
|
const input = "a'b'c'd'e'f'g'h'i'j'k'l'm'n'o'p"
|
|
610
607
|
const expected = `a${MODIFIER_LETTER_APOSTROPHE}b${MODIFIER_LETTER_APOSTROPHE}c${MODIFIER_LETTER_APOSTROPHE}d${MODIFIER_LETTER_APOSTROPHE}e${MODIFIER_LETTER_APOSTROPHE}f${MODIFIER_LETTER_APOSTROPHE}g${MODIFIER_LETTER_APOSTROPHE}h${MODIFIER_LETTER_APOSTROPHE}i${MODIFIER_LETTER_APOSTROPHE}j${MODIFIER_LETTER_APOSTROPHE}k${MODIFIER_LETTER_APOSTROPHE}l${MODIFIER_LETTER_APOSTROPHE}m${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE}o${MODIFIER_LETTER_APOSTROPHE}p`
|
|
611
608
|
const start = performance.now()
|
|
612
|
-
const result =
|
|
609
|
+
const result = classifyApostrophes(input)
|
|
613
610
|
expect(performance.now() - start).toBeLessThan(1000)
|
|
614
611
|
expect(result).toBe(expected)
|
|
615
|
-
expect(
|
|
612
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
616
613
|
})
|
|
617
614
|
|
|
618
615
|
it("handles 500-char word + possessive", () => {
|
|
619
616
|
const input = `${"a".repeat(500)}'s thing`
|
|
620
|
-
expect(
|
|
617
|
+
expect(classifyApostrophes(input)).toBe(`${"a".repeat(500)}${MODIFIER_LETTER_APOSTROPHE}s thing`)
|
|
621
618
|
})
|
|
622
619
|
})
|
|
623
620
|
|
package/src/tests/rehype.test.ts
CHANGED
|
@@ -69,7 +69,7 @@ describe("rehypePunctilio", () => {
|
|
|
69
69
|
["multi-segment number preserved", "<p>1-<em>2</em>-3</p>", "<p>1-<em>2</em>-3</p>"],
|
|
70
70
|
["model name preserved", "<p><em>GPT</em>-3</p>", "<p><em>GPT</em>-3</p>"],
|
|
71
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>",
|
|
72
|
+
["genuine negative at element start", "<p><em>-5</em> degrees</p>", `<p><em>${UNICODE_SYMBOLS.MINUS}5</em> degrees</p>`],
|
|
73
73
|
])("%s", async (_name, html, expected) => {
|
|
74
74
|
expect(await processHtml(html, { nbsp: false })).toEqual(expected)
|
|
75
75
|
})
|
|
@@ -259,8 +259,8 @@ describe("rehypePunctilio", () => {
|
|
|
259
259
|
|
|
260
260
|
it("applies custom transforms", () => {
|
|
261
261
|
const element = h("p", "pages 1-5") as Element
|
|
262
|
-
transformElement(element, (s) => s.replace(/-/g,
|
|
263
|
-
expect((element.children[0] as Text).value).toBe(
|
|
262
|
+
transformElement(element, (s) => s.replace(/-/g, EN_DASH), ignoreNone, DEFAULT_SEPARATOR)
|
|
263
|
+
expect((element.children[0] as Text).value).toBe(`pages 1${EN_DASH}5`)
|
|
264
264
|
})
|
|
265
265
|
|
|
266
266
|
it("handles missing children gracefully", () => {
|
|
@@ -351,8 +351,8 @@ describe("rehypePunctilio", () => {
|
|
|
351
351
|
|
|
352
352
|
describe("assertSmartQuotesMatch", () => {
|
|
353
353
|
it.each([
|
|
354
|
-
["matched double quotes",
|
|
355
|
-
["nested quotes",
|
|
354
|
+
["matched double quotes", `${LDQ}Hello${RDQ}`],
|
|
355
|
+
["nested quotes", `${LDQ}Outer ${LDQ}inner${RDQ} outer${RDQ}`],
|
|
356
356
|
["empty string", ""],
|
|
357
357
|
["no quotes", "Hello, world!"],
|
|
358
358
|
])("passes for %s", (_name, input) => {
|
|
@@ -360,9 +360,10 @@ describe("rehypePunctilio", () => {
|
|
|
360
360
|
})
|
|
361
361
|
|
|
362
362
|
it.each([
|
|
363
|
-
["unmatched opening",
|
|
364
|
-
["unmatched closing",
|
|
365
|
-
["extra opening",
|
|
363
|
+
["unmatched opening", `${LDQ}Hello`],
|
|
364
|
+
["unmatched closing", `Hello${RDQ}`],
|
|
365
|
+
["extra opening", `${LDQ}${LDQ}Hello${RDQ}`],
|
|
366
|
+
["reversed quotes", `${RDQ}Hello${LDQ}`],
|
|
366
367
|
])("throws for %s", (_name, input) => {
|
|
367
368
|
expect(() => assertSmartQuotesMatch(input)).toThrow("Mismatched quotes")
|
|
368
369
|
})
|
package/src/tests/remark.test.ts
CHANGED
|
@@ -48,7 +48,7 @@ describe("remarkPunctilio", () => {
|
|
|
48
48
|
describe("basic transformations (nbsp enabled)", () => {
|
|
49
49
|
it.each([
|
|
50
50
|
["quotes", '"Hello," she said.', `${LDQ}Hello,${RDQ} she${NBSP}said.`],
|
|
51
|
-
["apostrophes", "It's a test.", `It${RSQ}s
|
|
51
|
+
["apostrophes", "It's a test.", `It${RSQ}s${NBSP}a${NBSP}test.`],
|
|
52
52
|
["em dashes", "Wait -- here it comes.", `Wait${EM_DASH}here it${NBSP}comes.`],
|
|
53
53
|
["en dashes (number range)", "Pages 1-5", `Pages${NBSP}1${EN_DASH}5`],
|
|
54
54
|
["ellipses", "Wait...", `Wait${ELLIPSIS}`],
|
|
@@ -129,7 +129,7 @@ describe("legalSymbols", () => {
|
|
|
129
129
|
// (c) preceded by "copyright" → copyright
|
|
130
130
|
["Copyright (c) Company", `Copyright ${UNICODE_SYMBOLS.COPYRIGHT} Company`],
|
|
131
131
|
["copyright (C) by Author", `copyright ${UNICODE_SYMBOLS.COPYRIGHT} by Author`],
|
|
132
|
-
// (r)
|
|
132
|
+
// (r) converts in trademark context
|
|
133
133
|
["Brand(r)", `Brand${UNICODE_SYMBOLS.REGISTERED}`],
|
|
134
134
|
["Product (R)", `Product ${UNICODE_SYMBOLS.REGISTERED}`],
|
|
135
135
|
["Name(tm)", `Name${UNICODE_SYMBOLS.TRADEMARK}`],
|
|
@@ -154,6 +154,16 @@ describe("legalSymbols", () => {
|
|
|
154
154
|
])('preserves (c) in non-copyright context "%s"', (input, expected) => {
|
|
155
155
|
expect(legalSymbols(input)).toBe(expected)
|
|
156
156
|
})
|
|
157
|
+
|
|
158
|
+
it.each([
|
|
159
|
+
// (r) in enumerations
|
|
160
|
+
["(p), (q), (r), (s)", "(p), (q), (r), (s)"],
|
|
161
|
+
["(Q); (R); (S)", "(Q); (R); (S)"],
|
|
162
|
+
// (r) in legal citations
|
|
163
|
+
["See (r)(1)(A)", "See (r)(1)(A)"],
|
|
164
|
+
])('preserves (r) in non-trademark context "%s"', (input, expected) => {
|
|
165
|
+
expect(legalSymbols(input)).toBe(expected)
|
|
166
|
+
})
|
|
157
167
|
})
|
|
158
168
|
|
|
159
169
|
describe("arrows", () => {
|
|
@@ -227,14 +237,14 @@ describe("primeMarks", () => {
|
|
|
227
237
|
['He is 6\'2" tall', `He is 6${UNICODE_SYMBOLS.PRIME}2${UNICODE_SYMBOLS.DOUBLE_PRIME} tall`],
|
|
228
238
|
["The board is 8' long", `The board is 8${UNICODE_SYMBOLS.PRIME} long`],
|
|
229
239
|
['12"', `12${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
230
|
-
[
|
|
240
|
+
[`Location: 45${UNICODE_SYMBOLS.DEGREE} 30' 15"`, `Location: 45${UNICODE_SYMBOLS.DEGREE} 30${UNICODE_SYMBOLS.PRIME} 15${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
231
241
|
["don't", "don't"],
|
|
232
242
|
["it's", "it's"],
|
|
233
243
|
["'Twas the night", "'Twas the night"],
|
|
234
244
|
["'hello'", "'hello'"],
|
|
235
245
|
['"test"', '"test"'],
|
|
236
246
|
['The ceiling is 9\'6" high', `The ceiling is 9${UNICODE_SYMBOLS.PRIME}6${UNICODE_SYMBOLS.DOUBLE_PRIME} high`],
|
|
237
|
-
[
|
|
247
|
+
[`40${UNICODE_SYMBOLS.DEGREE} 44' 54" N`, `40${UNICODE_SYMBOLS.DEGREE} 44${UNICODE_SYMBOLS.PRIME} 54${UNICODE_SYMBOLS.DOUBLE_PRIME} N`],
|
|
238
248
|
['The board is 12" wide', `The board is 12${UNICODE_SYMBOLS.DOUBLE_PRIME} wide`],
|
|
239
249
|
['12" long', `12${UNICODE_SYMBOLS.DOUBLE_PRIME} long`],
|
|
240
250
|
// Multiple primes in one string
|
|
@@ -534,9 +544,9 @@ describe("competitor-derived edge cases", () => {
|
|
|
534
544
|
// From Standard Ebooks: coordinates
|
|
535
545
|
describe("coordinate notation", () => {
|
|
536
546
|
it.each([
|
|
537
|
-
[
|
|
538
|
-
[
|
|
539
|
-
[
|
|
547
|
+
[`40${UNICODE_SYMBOLS.DEGREE} 44' 54" N`, `40${UNICODE_SYMBOLS.DEGREE} 44${UNICODE_SYMBOLS.PRIME} 54${UNICODE_SYMBOLS.DOUBLE_PRIME} N`],
|
|
548
|
+
[`74${UNICODE_SYMBOLS.DEGREE} 0' 21" W`, `74${UNICODE_SYMBOLS.DEGREE} 0${UNICODE_SYMBOLS.PRIME} 21${UNICODE_SYMBOLS.DOUBLE_PRIME} W`],
|
|
549
|
+
[`51${UNICODE_SYMBOLS.DEGREE} 30' N`, `51${UNICODE_SYMBOLS.DEGREE} 30${UNICODE_SYMBOLS.PRIME} N`],
|
|
540
550
|
])('converts coordinates: "%s"', (input, expected) => {
|
|
541
551
|
expect(primeMarks(input)).toBe(expected)
|
|
542
552
|
})
|