punctilio 3.7.3 → 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/quotes.test.ts
DELETED
|
@@ -1,783 +0,0 @@
|
|
|
1
|
-
import { niceQuotes, classifyApostrophes } from "../quotes.js"
|
|
2
|
-
import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR, TERMINAL_PUNCTUATION } from "../constants.js"
|
|
3
|
-
import { assertLinearScaling } from "./test-helpers.js"
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
LEFT_DOUBLE_QUOTE,
|
|
7
|
-
RIGHT_DOUBLE_QUOTE,
|
|
8
|
-
LEFT_SINGLE_QUOTE,
|
|
9
|
-
RIGHT_SINGLE_QUOTE,
|
|
10
|
-
MODIFIER_LETTER_APOSTROPHE,
|
|
11
|
-
EM_DASH,
|
|
12
|
-
ELLIPSIS,
|
|
13
|
-
DOUBLE_LOW_9_QUOTE,
|
|
14
|
-
SINGLE_LOW_9_QUOTE,
|
|
15
|
-
LEFT_GUILLEMET,
|
|
16
|
-
RIGHT_GUILLEMET,
|
|
17
|
-
NNBSP,
|
|
18
|
-
} = UNICODE_SYMBOLS
|
|
19
|
-
|
|
20
|
-
describe("niceQuotes", () => {
|
|
21
|
-
describe("double quotes", () => {
|
|
22
|
-
it.each([
|
|
23
|
-
['"This is a quote", she said.', `${LEFT_DOUBLE_QUOTE}This is a quote,${RIGHT_DOUBLE_QUOTE} she said.`],
|
|
24
|
-
['"This is a quote," she said.', `${LEFT_DOUBLE_QUOTE}This is a quote,${RIGHT_DOUBLE_QUOTE} she said.`],
|
|
25
|
-
['"This is a quote!".', `${LEFT_DOUBLE_QUOTE}This is a quote!${RIGHT_DOUBLE_QUOTE}.`],
|
|
26
|
-
['"This is a quote..." he trailed off.', `${LEFT_DOUBLE_QUOTE}This is a quote...${RIGHT_DOUBLE_QUOTE} he trailed off.`],
|
|
27
|
-
['She said, "This is a quote."', `She said, ${LEFT_DOUBLE_QUOTE}This is a quote.${RIGHT_DOUBLE_QUOTE}`],
|
|
28
|
-
['"Hello." Mary', `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE} Mary`],
|
|
29
|
-
['"Hello." (Mary)', `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE} (Mary)`],
|
|
30
|
-
[
|
|
31
|
-
'"I am" so "tired" of "these" "quotes".',
|
|
32
|
-
`${LEFT_DOUBLE_QUOTE}I am${RIGHT_DOUBLE_QUOTE} so ${LEFT_DOUBLE_QUOTE}tired${RIGHT_DOUBLE_QUOTE} of ${LEFT_DOUBLE_QUOTE}these${RIGHT_DOUBLE_QUOTE} ${LEFT_DOUBLE_QUOTE}quotes.${RIGHT_DOUBLE_QUOTE}`,
|
|
33
|
-
],
|
|
34
|
-
['"world model";', `${LEFT_DOUBLE_QUOTE}world model${RIGHT_DOUBLE_QUOTE};`],
|
|
35
|
-
['"party"/"wedding."', `${LEFT_DOUBLE_QUOTE}party${RIGHT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}wedding.${RIGHT_DOUBLE_QUOTE}`],
|
|
36
|
-
['"Hi \'Trout!"', `${LEFT_DOUBLE_QUOTE}Hi ${MODIFIER_LETTER_APOSTROPHE}Trout!${RIGHT_DOUBLE_QUOTE}`],
|
|
37
|
-
[`${LEFT_DOUBLE_QUOTE}scope insensitivity${RIGHT_DOUBLE_QUOTE}`, `${LEFT_DOUBLE_QUOTE}scope insensitivity${RIGHT_DOUBLE_QUOTE}`],
|
|
38
|
-
[
|
|
39
|
-
'"how many ways can this function be implemented?".',
|
|
40
|
-
`${LEFT_DOUBLE_QUOTE}how many ways can this function be implemented?${RIGHT_DOUBLE_QUOTE}.`,
|
|
41
|
-
],
|
|
42
|
-
['SSL.")', `SSL.${RIGHT_DOUBLE_QUOTE})`],
|
|
43
|
-
["can't multiply\"?", `can${MODIFIER_LETTER_APOSTROPHE}t multiply${RIGHT_DOUBLE_QUOTE}?`],
|
|
44
|
-
['with "scope insensitivity":', `with ${LEFT_DOUBLE_QUOTE}scope insensitivity${RIGHT_DOUBLE_QUOTE}:`],
|
|
45
|
-
['("the best")', `(${LEFT_DOUBLE_QUOTE}the best${RIGHT_DOUBLE_QUOTE})`],
|
|
46
|
-
['"This is a quote"...', `${LEFT_DOUBLE_QUOTE}This is a quote${RIGHT_DOUBLE_QUOTE}...`],
|
|
47
|
-
['He said, "This is a quote"...', `He said, ${LEFT_DOUBLE_QUOTE}This is a quote${RIGHT_DOUBLE_QUOTE}...`],
|
|
48
|
-
['"... What is this?"', `${LEFT_DOUBLE_QUOTE}... What is this?${RIGHT_DOUBLE_QUOTE}`],
|
|
49
|
-
['"/"', `${LEFT_DOUBLE_QUOTE}/${RIGHT_DOUBLE_QUOTE}`],
|
|
50
|
-
['"Game"/"Life"', `${LEFT_DOUBLE_QUOTE}Game${RIGHT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}Life${RIGHT_DOUBLE_QUOTE}`],
|
|
51
|
-
['"Test:".', `${LEFT_DOUBLE_QUOTE}Test:${RIGHT_DOUBLE_QUOTE}.`],
|
|
52
|
-
['"Test...".', `${LEFT_DOUBLE_QUOTE}Test...${RIGHT_DOUBLE_QUOTE}.`],
|
|
53
|
-
[`"To maximize reward${ELLIPSIS}".`, `${LEFT_DOUBLE_QUOTE}To maximize reward${ELLIPSIS}${RIGHT_DOUBLE_QUOTE}.`],
|
|
54
|
-
['"Test"s', `${LEFT_DOUBLE_QUOTE}Test${RIGHT_DOUBLE_QUOTE}s`],
|
|
55
|
-
// End-of-line quote becomes RIGHT quote
|
|
56
|
-
['not confident in that plan - "', `not confident in that plan - ${RIGHT_DOUBLE_QUOTE}`],
|
|
57
|
-
])('should convert double quotes in "%s"', (input, expected) => {
|
|
58
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
describe("quoted punctuation (short strings of punctuation inside quotes)", () => {
|
|
63
|
-
it.each([
|
|
64
|
-
['"?"', `${LEFT_DOUBLE_QUOTE}?${RIGHT_DOUBLE_QUOTE}`],
|
|
65
|
-
['"!"', `${LEFT_DOUBLE_QUOTE}!${RIGHT_DOUBLE_QUOTE}`],
|
|
66
|
-
['"."', `${LEFT_DOUBLE_QUOTE}.${RIGHT_DOUBLE_QUOTE}`],
|
|
67
|
-
['","', `${LEFT_DOUBLE_QUOTE},${RIGHT_DOUBLE_QUOTE}`],
|
|
68
|
-
['";"', `${LEFT_DOUBLE_QUOTE};${RIGHT_DOUBLE_QUOTE}`],
|
|
69
|
-
['"?!"', `${LEFT_DOUBLE_QUOTE}?!${RIGHT_DOUBLE_QUOTE}`],
|
|
70
|
-
['why not "?"', `why not ${LEFT_DOUBLE_QUOTE}?${RIGHT_DOUBLE_QUOTE}`],
|
|
71
|
-
['She asked "?" and left.', `She asked ${LEFT_DOUBLE_QUOTE}?${RIGHT_DOUBLE_QUOTE} and left.`],
|
|
72
|
-
['("?")', `(${LEFT_DOUBLE_QUOTE}?${RIGHT_DOUBLE_QUOTE})`],
|
|
73
|
-
])('handles quoted punctuation: "%s"', (input, expected) => {
|
|
74
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
describe("single quotes and apostrophes", () => {
|
|
79
|
-
it.each([
|
|
80
|
-
["He said, 'Hi'", `He said, ${LEFT_SINGLE_QUOTE}Hi${RIGHT_SINGLE_QUOTE}`],
|
|
81
|
-
["He wanted 'power.'", `He wanted ${LEFT_SINGLE_QUOTE}power.${RIGHT_SINGLE_QUOTE}`],
|
|
82
|
-
["I'd", `I${MODIFIER_LETTER_APOSTROPHE}d`],
|
|
83
|
-
["I don't'nt want to go", `I don${MODIFIER_LETTER_APOSTROPHE}t${MODIFIER_LETTER_APOSTROPHE}nt want to go`],
|
|
84
|
-
['"\'sup"', `${LEFT_DOUBLE_QUOTE}${MODIFIER_LETTER_APOSTROPHE}sup${RIGHT_DOUBLE_QUOTE}`],
|
|
85
|
-
["'SUP", `${MODIFIER_LETTER_APOSTROPHE}SUP`],
|
|
86
|
-
["Rock 'n' Roll", `Rock ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} Roll`],
|
|
87
|
-
["I was born in '99", `I was born in ${MODIFIER_LETTER_APOSTROPHE}99`],
|
|
88
|
-
["'99 tigers weren't a match", `${MODIFIER_LETTER_APOSTROPHE}99 tigers weren${MODIFIER_LETTER_APOSTROPHE}t a match`],
|
|
89
|
-
[
|
|
90
|
-
"I'm not the best, haven't you heard?",
|
|
91
|
-
`I${MODIFIER_LETTER_APOSTROPHE}m not the best, haven${MODIFIER_LETTER_APOSTROPHE}t you heard?`,
|
|
92
|
-
],
|
|
93
|
-
// Skipped: Complex edge case with 'sup and quoted phrase
|
|
94
|
-
// ["Hey, 'sup 'this is a single quote'", `Hey, ${RIGHT_SINGLE_QUOTE}sup ${LEFT_SINGLE_QUOTE}this is a single quote${RIGHT_SINGLE_QUOTE}`],
|
|
95
|
-
["'the best',", `${LEFT_SINGLE_QUOTE}the best,${RIGHT_SINGLE_QUOTE}`],
|
|
96
|
-
["'I lost the game.'", `${LEFT_SINGLE_QUOTE}I lost the game.${RIGHT_SINGLE_QUOTE}`],
|
|
97
|
-
["I hate you.'\"", `I hate you.${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`],
|
|
98
|
-
["The 'function space')", `The ${LEFT_SINGLE_QUOTE}function space${RIGHT_SINGLE_QUOTE})`],
|
|
99
|
-
[`The 'function space'${EM_DASH}`, `The ${LEFT_SINGLE_QUOTE}function space${RIGHT_SINGLE_QUOTE}${EM_DASH}`],
|
|
100
|
-
["What do you think?']", `What do you think?${RIGHT_SINGLE_QUOTE}]`],
|
|
101
|
-
["('survival incentive')", `(${LEFT_SINGLE_QUOTE}survival incentive${RIGHT_SINGLE_QUOTE})`],
|
|
102
|
-
[
|
|
103
|
-
"strategy s's return is good, even as d's return is bad",
|
|
104
|
-
`strategy s${MODIFIER_LETTER_APOSTROPHE}s return is good, even as d${MODIFIER_LETTER_APOSTROPHE}s return is bad`,
|
|
105
|
-
],
|
|
106
|
-
// Chained possessives
|
|
107
|
-
["the dog's owner's car", `the dog${MODIFIER_LETTER_APOSTROPHE}s owner${MODIFIER_LETTER_APOSTROPHE}s car`],
|
|
108
|
-
// Hyphenated possessive
|
|
109
|
-
["mother-in-law's house", `mother-in-law${MODIFIER_LETTER_APOSTROPHE}s house`],
|
|
110
|
-
// Accented character before possessive
|
|
111
|
-
["café's menu", `café${MODIFIER_LETTER_APOSTROPHE}s menu`],
|
|
112
|
-
// Digit before possessive
|
|
113
|
-
["the 747's engine", `the 747${MODIFIER_LETTER_APOSTROPHE}s engine`],
|
|
114
|
-
// Compound contraction + possessive
|
|
115
|
-
["shouldn't's", `shouldn${MODIFIER_LETTER_APOSTROPHE}t${MODIFIER_LETTER_APOSTROPHE}s`],
|
|
116
|
-
// Contraction before em-dash
|
|
117
|
-
[`don't${EM_DASH}stop`, `don${MODIFIER_LETTER_APOSTROPHE}t${EM_DASH}stop`],
|
|
118
|
-
// Contraction + digit context
|
|
119
|
-
["it's 5 o'clock", `it${MODIFIER_LETTER_APOSTROPHE}s 5 o${MODIFIER_LETTER_APOSTROPHE}clock`],
|
|
120
|
-
// French contraction (single-letter prefix + accented Latin)
|
|
121
|
-
["l'homme", `l${MODIFIER_LETTER_APOSTROPHE}homme`],
|
|
122
|
-
// Uppercase contraction/name
|
|
123
|
-
["O'NEILL", `O${MODIFIER_LETTER_APOSTROPHE}NEILL`],
|
|
124
|
-
// Possessive in parenthetical
|
|
125
|
-
["(Brien's)", `(Brien${MODIFIER_LETTER_APOSTROPHE}s)`],
|
|
126
|
-
// Possessive inside single-quoted phrase
|
|
127
|
-
["'the dog's bowl'", `${LEFT_SINGLE_QUOTE}the dog${MODIFIER_LETTER_APOSTROPHE}s bowl${RIGHT_SINGLE_QUOTE}`],
|
|
128
|
-
// Straight apostrophe inside pre-converted double quotes
|
|
129
|
-
[`${LEFT_DOUBLE_QUOTE}the dog's${RIGHT_DOUBLE_QUOTE}`, `${LEFT_DOUBLE_QUOTE}the dog${MODIFIER_LETTER_APOSTROPHE}s${RIGHT_DOUBLE_QUOTE}`],
|
|
130
|
-
// Decade in double quotes
|
|
131
|
-
[`"the '90s"`, `${LEFT_DOUBLE_QUOTE}the ${MODIFIER_LETTER_APOSTROPHE}90s${RIGHT_DOUBLE_QUOTE}`],
|
|
132
|
-
// 'n' + possessive combination
|
|
133
|
-
["Rock 'n' Roll's greatest hits", `Rock ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} Roll${MODIFIER_LETTER_APOSTROPHE}s greatest hits`],
|
|
134
|
-
// Non-Latin character before possessive
|
|
135
|
-
["Привет's", `Привет${MODIFIER_LETTER_APOSTROPHE}s`],
|
|
136
|
-
// Closing RSQ then opening LDQ
|
|
137
|
-
[`'hello' "world"`, `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} ${LEFT_DOUBLE_QUOTE}world${RIGHT_DOUBLE_QUOTE}`],
|
|
138
|
-
// Latin Extended Additional: Vietnamese contraction-like patterns
|
|
139
|
-
["l'\u1EA3nh", `l${MODIFIER_LETTER_APOSTROPHE}\u1EA3nh`],
|
|
140
|
-
// Latin Extended Additional: Welsh w-acute (U+1E83, in U+1E00-1EFF range)
|
|
141
|
-
["l'\u1E83r", `l${MODIFIER_LETTER_APOSTROPHE}\u1E83r`],
|
|
142
|
-
])('should handle single quotes/apostrophes in "%s"', (input, expected) => {
|
|
143
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
describe("plural possessive apostrophes", () => {
|
|
148
|
-
it.each([
|
|
149
|
-
// Unmatched trailing s' → MLA
|
|
150
|
-
["the models' behavior", `the models${MODIFIER_LETTER_APOSTROPHE} behavior`],
|
|
151
|
-
["Bayes' rule", `Bayes${MODIFIER_LETTER_APOSTROPHE} rule`],
|
|
152
|
-
["Thomas' gaze", `Thomas${MODIFIER_LETTER_APOSTROPHE} gaze`],
|
|
153
|
-
["dogs' and cats' toys", `dogs${MODIFIER_LETTER_APOSTROPHE} and cats${MODIFIER_LETTER_APOSTROPHE} toys`],
|
|
154
|
-
["belongs to the dogs'", `belongs to the dogs${MODIFIER_LETTER_APOSTROPHE}`],
|
|
155
|
-
["the DOGS' owner", `the DOGS${MODIFIER_LETTER_APOSTROPHE} owner`],
|
|
156
|
-
["Prisoners' Dilemma", `Prisoners${MODIFIER_LETTER_APOSTROPHE} Dilemma`],
|
|
157
|
-
// Mixed possessive + quoted phrase
|
|
158
|
-
["dogs' and 'hello'", `dogs${MODIFIER_LETTER_APOSTROPHE} and ${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE}`],
|
|
159
|
-
["'cats' and dogs'", `${LEFT_SINGLE_QUOTE}cats${RIGHT_SINGLE_QUOTE} and dogs${MODIFIER_LETTER_APOSTROPHE}`],
|
|
160
|
-
// Matched closing quote after s stays RSQ
|
|
161
|
-
["'yes'", `${LEFT_SINGLE_QUOTE}yes${RIGHT_SINGLE_QUOTE}`],
|
|
162
|
-
["'dogs'", `${LEFT_SINGLE_QUOTE}dogs${RIGHT_SINGLE_QUOTE}`],
|
|
163
|
-
["'he tells' stories", `${LEFT_SINGLE_QUOTE}he tells${RIGHT_SINGLE_QUOTE} stories`],
|
|
164
|
-
])('handles plural possessive: "%s"', (input, expected) => {
|
|
165
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it.each([
|
|
169
|
-
`the dogs${MODIFIER_LETTER_APOSTROPHE} owner`,
|
|
170
|
-
`Bayes${MODIFIER_LETTER_APOSTROPHE} rule`,
|
|
171
|
-
`dogs${MODIFIER_LETTER_APOSTROPHE} and ${LEFT_SINGLE_QUOTE}cats${RIGHT_SINGLE_QUOTE}`,
|
|
172
|
-
])('plural possessive is idempotent: "%s"', (input) => {
|
|
173
|
-
expect(classifyApostrophes(input)).toBe(input)
|
|
174
|
-
expect(classifyApostrophes(classifyApostrophes(input))).toBe(input)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it("stress: many possessives interleaved with quotes", () => {
|
|
178
|
-
// 50 alternating possessives and quoted phrases
|
|
179
|
-
const pairs = Array.from({ length: 50 }, (_, i) =>
|
|
180
|
-
i % 2 === 0 ? `dogs'` : `'yes'`
|
|
181
|
-
)
|
|
182
|
-
const input = pairs.join(" ")
|
|
183
|
-
const result = classifyApostrophes(input)
|
|
184
|
-
// Every dogs' → MLA, every 'yes' → LSQ+yes+RSQ
|
|
185
|
-
const expectedParts = Array.from({ length: 50 }, (_, i) =>
|
|
186
|
-
i % 2 === 0
|
|
187
|
-
? `dogs${MODIFIER_LETTER_APOSTROPHE}`
|
|
188
|
-
: `${LEFT_SINGLE_QUOTE}yes${RIGHT_SINGLE_QUOTE}`
|
|
189
|
-
)
|
|
190
|
-
expect(result).toBe(expectedParts.join(" "))
|
|
191
|
-
expect(classifyApostrophes(result)).toBe(result)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it("stress: 200 consecutive plural possessives scales linearly", () => {
|
|
195
|
-
const words = ["dogs'", "cats'", "Bayes'", "Thomas'", "PLAYERS'"]
|
|
196
|
-
const buildInput = (n: number) => Array.from({ length: n }, (_, i) => words[i % words.length]).join(" ")
|
|
197
|
-
|
|
198
|
-
assertLinearScaling(classifyApostrophes, buildInput)
|
|
199
|
-
// All should be MLA (no LSQ to pair with)
|
|
200
|
-
const result = classifyApostrophes(buildInput(200))
|
|
201
|
-
expect(result).not.toContain(RIGHT_SINGLE_QUOTE)
|
|
202
|
-
expect(result).not.toContain("'")
|
|
203
|
-
expect(classifyApostrophes(result)).toBe(result)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it("stress: deeply nested alternating pattern", () => {
|
|
207
|
-
// Pattern: 'a' dogs' 'b' cats' 'c' ...
|
|
208
|
-
const possessives = ["dogs", "cats", "items", "players", "Bayes"]
|
|
209
|
-
const input = Array.from({ length: 30 }, (_, i) =>
|
|
210
|
-
i % 2 === 0 ? `'word${i}'` : `${possessives[i % possessives.length]}'`
|
|
211
|
-
).join(" ")
|
|
212
|
-
const result = classifyApostrophes(input)
|
|
213
|
-
// Quoted words get LSQ/RSQ, possessives get MLA
|
|
214
|
-
for (let i = 0; i < 30; i++) {
|
|
215
|
-
if (i % 2 === 0) {
|
|
216
|
-
expect(result).toContain(`${LEFT_SINGLE_QUOTE}word${i}${RIGHT_SINGLE_QUOTE}`)
|
|
217
|
-
} else {
|
|
218
|
-
expect(result).toContain(`${possessives[i % possessives.length]}${MODIFIER_LETTER_APOSTROPHE}`)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
expect(classifyApostrophes(result)).toBe(result)
|
|
222
|
-
})
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
describe("leading apostrophe contractions", () => {
|
|
226
|
-
it.each([
|
|
227
|
-
["'twas the night", `${MODIFIER_LETTER_APOSTROPHE}twas the night`],
|
|
228
|
-
["'tis the season", `${MODIFIER_LETTER_APOSTROPHE}tis the season`],
|
|
229
|
-
["'Twas brillig", `${MODIFIER_LETTER_APOSTROPHE}Twas brillig`],
|
|
230
|
-
["'Tis but a scratch", `${MODIFIER_LETTER_APOSTROPHE}Tis but a scratch`],
|
|
231
|
-
// Decade abbreviations
|
|
232
|
-
["the '90s", `the ${MODIFIER_LETTER_APOSTROPHE}90s`],
|
|
233
|
-
["in '99", `in ${MODIFIER_LETTER_APOSTROPHE}99`],
|
|
234
|
-
["the '00s", `the ${MODIFIER_LETTER_APOSTROPHE}00s`],
|
|
235
|
-
// Additional leading contractions
|
|
236
|
-
["'cause", `${MODIFIER_LETTER_APOSTROPHE}cause`],
|
|
237
|
-
["'twas the night, 'twas indeed", `${MODIFIER_LETTER_APOSTROPHE}twas the night, ${MODIFIER_LETTER_APOSTROPHE}twas indeed`],
|
|
238
|
-
[`"'twas the night"`, `${LEFT_DOUBLE_QUOTE}${MODIFIER_LETTER_APOSTROPHE}twas the night${RIGHT_DOUBLE_QUOTE}`],
|
|
239
|
-
])('handles leading apostrophe in "%s"', (input, expected) => {
|
|
240
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
it("multiple decades in one sentence", () => {
|
|
244
|
-
expect(classifyApostrophes("The '60s, '70s, and '80s were different.")).toBe(
|
|
245
|
-
`The ${MODIFIER_LETTER_APOSTROPHE}60s, ${MODIFIER_LETTER_APOSTROPHE}70s, and ${MODIFIER_LETTER_APOSTROPHE}80s were different.`
|
|
246
|
-
)
|
|
247
|
-
})
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
describe("'n' abbreviation edge cases", () => {
|
|
251
|
-
it.each([
|
|
252
|
-
["uppercase 'N' is not abbreviated", "Rock 'N' Roll", `Rock ${LEFT_SINGLE_QUOTE}N${RIGHT_SINGLE_QUOTE} Roll`],
|
|
253
|
-
["multiple 'n' abbreviations", "Rock 'n' Roll and fish 'n' chips", `Rock ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} Roll and fish ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} chips`],
|
|
254
|
-
["digit neighbors (\\w includes digits)", "1 'n' 2", `1 ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} 2`],
|
|
255
|
-
["known false positive: quoted letter", "the letter 'n' is common", `the letter ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} is common`],
|
|
256
|
-
])('%s', (_label, input, expected) => {
|
|
257
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
it.each([
|
|
261
|
-
["start of string (no preceding word)", "'n' Roll", `${LEFT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE} Roll`],
|
|
262
|
-
["end of string (no following word)", "Rock 'n'", `Rock ${LEFT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE}`],
|
|
263
|
-
["punctuation precedes first apostrophe", "said, 'n' is good", `said, ${LEFT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE} is good`],
|
|
264
|
-
["newline replaces space", "Rock 'n'\nRoll", `Rock ${LEFT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE}\nRoll`],
|
|
265
|
-
])('does not produce MLA-n-MLA: %s', (_label, input, expected) => {
|
|
266
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
267
|
-
})
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
// Tests derived from competitor libraries and typography guidelines
|
|
271
|
-
describe("competitor-derived edge cases", () => {
|
|
272
|
-
// From smartquotes.js: Unicode text within quotes
|
|
273
|
-
it.each([
|
|
274
|
-
['"Águila"', `${LEFT_DOUBLE_QUOTE}Águila${RIGHT_DOUBLE_QUOTE}`],
|
|
275
|
-
['"café"', `${LEFT_DOUBLE_QUOTE}café${RIGHT_DOUBLE_QUOTE}`],
|
|
276
|
-
['"naïve"', `${LEFT_DOUBLE_QUOTE}naïve${RIGHT_DOUBLE_QUOTE}`],
|
|
277
|
-
['"日本語"', `${LEFT_DOUBLE_QUOTE}日本語${RIGHT_DOUBLE_QUOTE}`],
|
|
278
|
-
['"שלום"', `${LEFT_DOUBLE_QUOTE}שלום${RIGHT_DOUBLE_QUOTE}`],
|
|
279
|
-
])('handles Unicode text in quotes: "%s"', (input, expected) => {
|
|
280
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
// From retext-smartypants: quotes after em dashes
|
|
284
|
-
it.each([
|
|
285
|
-
[`He said${EM_DASH}"Hello"`, `He said${EM_DASH}${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}`],
|
|
286
|
-
[`word${EM_DASH}"quoted"${EM_DASH}word`, `word${EM_DASH}${LEFT_DOUBLE_QUOTE}quoted${RIGHT_DOUBLE_QUOTE}${EM_DASH}word`],
|
|
287
|
-
])('handles quotes adjacent to em dashes: "%s"', (input, expected) => {
|
|
288
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
it("handles single quote after em-dash", () => {
|
|
292
|
-
const input = `${EM_DASH}'Hi'${EM_DASH}`
|
|
293
|
-
const result = niceQuotes(input)
|
|
294
|
-
expect(result).toBe(`${EM_DASH}${LEFT_SINGLE_QUOTE}Hi${RIGHT_SINGLE_QUOTE}${EM_DASH}`)
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
// From Standard Ebooks: M'Donald-style names (archaic patterns)
|
|
298
|
-
it.each([
|
|
299
|
-
[`"M'Lord"`, `${LEFT_DOUBLE_QUOTE}M${MODIFIER_LETTER_APOSTROPHE}Lord${RIGHT_DOUBLE_QUOTE}`],
|
|
300
|
-
[`O'Brien's idea`, `O${MODIFIER_LETTER_APOSTROPHE}Brien${MODIFIER_LETTER_APOSTROPHE}s idea`],
|
|
301
|
-
[`The O'Connors`, `The O${MODIFIER_LETTER_APOSTROPHE}Connors`],
|
|
302
|
-
])('handles Irish/Scottish name apostrophes: "%s"', (input, expected) => {
|
|
303
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
// From smartquotes.js: complex nested quotes
|
|
307
|
-
it.each([
|
|
308
|
-
['"Alfred \'bertrand\' cees"', `${LEFT_DOUBLE_QUOTE}Alfred ${LEFT_SINGLE_QUOTE}bertrand${RIGHT_SINGLE_QUOTE} cees${RIGHT_DOUBLE_QUOTE}`],
|
|
309
|
-
["'Alfred \"bertrand\" cees'", `${LEFT_SINGLE_QUOTE}Alfred ${LEFT_DOUBLE_QUOTE}bertrand${RIGHT_DOUBLE_QUOTE} cees${RIGHT_SINGLE_QUOTE}`],
|
|
310
|
-
])('handles complex nested quotes: "%s"', (input, expected) => {
|
|
311
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
// From retext-smartypants: quotes with ellipsis
|
|
315
|
-
it.each([
|
|
316
|
-
[`"${ELLIPSIS}"`, `${LEFT_DOUBLE_QUOTE}${ELLIPSIS}${RIGHT_DOUBLE_QUOTE}`],
|
|
317
|
-
[`"Wait${ELLIPSIS}"`, `${LEFT_DOUBLE_QUOTE}Wait${ELLIPSIS}${RIGHT_DOUBLE_QUOTE}`],
|
|
318
|
-
[`'${ELLIPSIS}'`, `${LEFT_SINGLE_QUOTE}${ELLIPSIS}${RIGHT_SINGLE_QUOTE}`],
|
|
319
|
-
])('handles quotes with ellipsis: "%s"', (input, expected) => {
|
|
320
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
321
|
-
})
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
describe("complex real-world patterns", () => {
|
|
325
|
-
it.each([
|
|
326
|
-
// Dialogue with attribution
|
|
327
|
-
['"I can\'t," she said.', `${LEFT_DOUBLE_QUOTE}I can${MODIFIER_LETTER_APOSTROPHE}t,${RIGHT_DOUBLE_QUOTE} she said.`],
|
|
328
|
-
['"Why not?" he asked.', `${LEFT_DOUBLE_QUOTE}Why not?${RIGHT_DOUBLE_QUOTE} he asked.`],
|
|
329
|
-
// Multiple contractions
|
|
330
|
-
["I'm can't won't don't", `I${MODIFIER_LETTER_APOSTROPHE}m can${MODIFIER_LETTER_APOSTROPHE}t won${MODIFIER_LETTER_APOSTROPHE}t don${MODIFIER_LETTER_APOSTROPHE}t`],
|
|
331
|
-
// Quote within parentheses
|
|
332
|
-
['("test")', `(${LEFT_DOUBLE_QUOTE}test${RIGHT_DOUBLE_QUOTE})`],
|
|
333
|
-
["('test')", `(${LEFT_SINGLE_QUOTE}test${RIGHT_SINGLE_QUOTE})`],
|
|
334
|
-
// Quote with slash separator
|
|
335
|
-
['"option1"/"option2"', `${LEFT_DOUBLE_QUOTE}option1${RIGHT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}option2${RIGHT_DOUBLE_QUOTE}`],
|
|
336
|
-
])('handles complex pattern: "%s"', (input, expected) => {
|
|
337
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
it("handles multi-line dialogue", () => {
|
|
341
|
-
const input = '"Hello,"\n"World"'
|
|
342
|
-
const expected = `${LEFT_DOUBLE_QUOTE}Hello,${RIGHT_DOUBLE_QUOTE}\n${LEFT_DOUBLE_QUOTE}World${RIGHT_DOUBLE_QUOTE}`
|
|
343
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
344
|
-
})
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
describe("idempotency", () => {
|
|
348
|
-
it.each([
|
|
349
|
-
`${LEFT_DOUBLE_QUOTE}already curly${RIGHT_DOUBLE_QUOTE}`,
|
|
350
|
-
`${LEFT_SINGLE_QUOTE}single curly${RIGHT_SINGLE_QUOTE}`,
|
|
351
|
-
`I${MODIFIER_LETTER_APOSTROPHE}m already converted`,
|
|
352
|
-
`${LEFT_DOUBLE_QUOTE}nested ${LEFT_SINGLE_QUOTE}quotes${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`,
|
|
353
|
-
// French contractions
|
|
354
|
-
`l${MODIFIER_LETTER_APOSTROPHE}homme d${MODIFIER_LETTER_APOSTROPHE}accord`,
|
|
355
|
-
// Uppercase
|
|
356
|
-
`O${MODIFIER_LETTER_APOSTROPHE}NEILL`,
|
|
357
|
-
`CAN${MODIFIER_LETTER_APOSTROPHE}T`,
|
|
358
|
-
// Plural possessive
|
|
359
|
-
`the dogs${MODIFIER_LETTER_APOSTROPHE} owner`,
|
|
360
|
-
// Mixed quote types
|
|
361
|
-
`${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} ${LEFT_DOUBLE_QUOTE}world${RIGHT_DOUBLE_QUOTE}`,
|
|
362
|
-
])('is idempotent for: "%s"', (input) => {
|
|
363
|
-
expect(classifyApostrophes(input)).toBe(input)
|
|
364
|
-
expect(classifyApostrophes(classifyApostrophes(input))).toBe(input)
|
|
365
|
-
})
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
describe("with separator character", () => {
|
|
369
|
-
const sep = DEFAULT_SEPARATOR
|
|
370
|
-
it.each([
|
|
371
|
-
[`"Hello${sep} world"`, `${LEFT_DOUBLE_QUOTE}Hello${sep} world${RIGHT_DOUBLE_QUOTE}`, "preserves separator positions"],
|
|
372
|
-
[`don${sep}'t`, `don${sep}${MODIFIER_LETTER_APOSTROPHE}t`, "contractions across separator"],
|
|
373
|
-
[`"test${sep}"`, `${LEFT_DOUBLE_QUOTE}test${sep}${RIGHT_DOUBLE_QUOTE}`, "quotes at separator boundaries"],
|
|
374
|
-
// Possessive across separator
|
|
375
|
-
[`dog${sep}'s bone`, `dog${sep}${MODIFIER_LETTER_APOSTROPHE}s bone`, "possessive across separator"],
|
|
376
|
-
// Separator between apostrophe and s
|
|
377
|
-
[`the dog'${sep}s bone`, `the dog${MODIFIER_LETTER_APOSTROPHE}${sep}s bone`, "separator between apostrophe and s"],
|
|
378
|
-
// Opening after separator
|
|
379
|
-
[`${sep}'hello'`, `${sep}${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE}`, "opening quote after separator"],
|
|
380
|
-
// Closing after separator
|
|
381
|
-
[`'hello${sep}'`, `${LEFT_SINGLE_QUOTE}hello${sep}${RIGHT_SINGLE_QUOTE}`, "closing quote after separator"],
|
|
382
|
-
])("%s → %s (%s)", (input, expected) => {
|
|
383
|
-
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
it("plural possessive across separator", () => {
|
|
387
|
-
const input = `dogs${sep}' food`
|
|
388
|
-
const expected = `dogs${sep}${MODIFIER_LETTER_APOSTROPHE} food`
|
|
389
|
-
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
390
|
-
expect(classifyApostrophes(expected, { separator: sep })).toBe(expected)
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
it("'n' with separators around word boundaries", () => {
|
|
394
|
-
const input = `Rock${sep} 'n' ${sep}Roll`
|
|
395
|
-
const expected = `Rock${sep} ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} ${sep}Roll`
|
|
396
|
-
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
397
|
-
expect(classifyApostrophes(expected, { separator: sep })).toBe(expected)
|
|
398
|
-
})
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
describe("unbalanced and edge quotes", () => {
|
|
402
|
-
it.each([
|
|
403
|
-
['"unclosed', `${LEFT_DOUBLE_QUOTE}unclosed`],
|
|
404
|
-
["unclosed'", `unclosed${RIGHT_SINGLE_QUOTE}`],
|
|
405
|
-
['""', `${LEFT_DOUBLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`],
|
|
406
|
-
// Minimal inputs
|
|
407
|
-
["", ""],
|
|
408
|
-
[" ", " "],
|
|
409
|
-
["'", `${MODIFIER_LETTER_APOSTROPHE}`],
|
|
410
|
-
["'a", `${MODIFIER_LETTER_APOSTROPHE}a`],
|
|
411
|
-
["a'", `a${RIGHT_SINGLE_QUOTE}`],
|
|
412
|
-
])('handles edge quote pattern: "%s"', (input, expected) => {
|
|
413
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
414
|
-
})
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
describe("quotes after various punctuation", () => {
|
|
418
|
-
it.each([
|
|
419
|
-
['text; "quote"', `text; ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
|
|
420
|
-
['title: "quote"', `title: ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
|
|
421
|
-
['what? "quote"', `what? ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
|
|
422
|
-
['wow! "quote"', `wow! ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
|
|
423
|
-
['[note] "quote"', `[note] ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
|
|
424
|
-
['{code} "quote"', `{code} ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
|
|
425
|
-
])('handles quotes after punctuation: "%s"', (input, expected) => {
|
|
426
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
427
|
-
})
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
describe("consecutive contractions", () => {
|
|
431
|
-
it.each([
|
|
432
|
-
["I'd've", `I${MODIFIER_LETTER_APOSTROPHE}d${MODIFIER_LETTER_APOSTROPHE}ve`],
|
|
433
|
-
["y'all'd've", `y${MODIFIER_LETTER_APOSTROPHE}all${MODIFIER_LETTER_APOSTROPHE}d${MODIFIER_LETTER_APOSTROPHE}ve`],
|
|
434
|
-
["wouldn't've", `wouldn${MODIFIER_LETTER_APOSTROPHE}t${MODIFIER_LETTER_APOSTROPHE}ve`],
|
|
435
|
-
["couldn't've", `couldn${MODIFIER_LETTER_APOSTROPHE}t${MODIFIER_LETTER_APOSTROPHE}ve`],
|
|
436
|
-
])('handles double contraction: "%s"', (input, expected) => {
|
|
437
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
438
|
-
})
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
describe("multiline quotes", () => {
|
|
442
|
-
it.each([
|
|
443
|
-
['"Hello\nWorld"', `${LEFT_DOUBLE_QUOTE}Hello\nWorld${RIGHT_DOUBLE_QUOTE}`],
|
|
444
|
-
['"A"\n"B"', `${LEFT_DOUBLE_QUOTE}A${RIGHT_DOUBLE_QUOTE}\n${LEFT_DOUBLE_QUOTE}B${RIGHT_DOUBLE_QUOTE}`],
|
|
445
|
-
// Contractions across lines
|
|
446
|
-
["I can't\nbelieve it's\nnot butter", `I can${MODIFIER_LETTER_APOSTROPHE}t\nbelieve it${MODIFIER_LETTER_APOSTROPHE}s\nnot butter`],
|
|
447
|
-
// Newline stops lookahead → MLA (not LSQ) since no closing quote visible
|
|
448
|
-
["'hello\nworld'", `${MODIFIER_LETTER_APOSTROPHE}hello\nworld${RIGHT_SINGLE_QUOTE}`],
|
|
449
|
-
])('handles multiline: "%s"', (input, expected) => {
|
|
450
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
451
|
-
})
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
describe("split dialogue", () => {
|
|
455
|
-
it.each([
|
|
456
|
-
[
|
|
457
|
-
'"Yes," he said, "absolutely."',
|
|
458
|
-
`${LEFT_DOUBLE_QUOTE}Yes,${RIGHT_DOUBLE_QUOTE} he said, ${LEFT_DOUBLE_QUOTE}absolutely.${RIGHT_DOUBLE_QUOTE}`,
|
|
459
|
-
],
|
|
460
|
-
[
|
|
461
|
-
'"No," she replied, "I disagree."',
|
|
462
|
-
`${LEFT_DOUBLE_QUOTE}No,${RIGHT_DOUBLE_QUOTE} she replied, ${LEFT_DOUBLE_QUOTE}I disagree.${RIGHT_DOUBLE_QUOTE}`,
|
|
463
|
-
],
|
|
464
|
-
])('handles split dialogue: "%s"', (input, expected) => {
|
|
465
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
466
|
-
})
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
describe("punctuationStyle option", () => {
|
|
470
|
-
// Reusable test strings
|
|
471
|
-
const periodOutsideDouble = `${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}.`
|
|
472
|
-
const periodInsideDouble = `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE}`
|
|
473
|
-
const periodOutsideSingle = `${LEFT_SINGLE_QUOTE}Hello${RIGHT_SINGLE_QUOTE}.`
|
|
474
|
-
const periodInsideSingle = `${LEFT_SINGLE_QUOTE}Hello.${RIGHT_SINGLE_QUOTE}`
|
|
475
|
-
const commaOutsideDouble = `${LEFT_DOUBLE_QUOTE}test${RIGHT_DOUBLE_QUOTE},`
|
|
476
|
-
const commaInsideDouble = `${LEFT_DOUBLE_QUOTE}test,${RIGHT_DOUBLE_QUOTE}`
|
|
477
|
-
|
|
478
|
-
describe('when "american" (default)', () => {
|
|
479
|
-
it.each([
|
|
480
|
-
[periodOutsideDouble, periodInsideDouble, "period outside double quotes"],
|
|
481
|
-
[periodOutsideSingle, periodInsideSingle, "period outside single quotes"],
|
|
482
|
-
[commaOutsideDouble, commaInsideDouble, "comma outside double quotes"],
|
|
483
|
-
])("moves punctuation inside: %s", (input, expected) => {
|
|
484
|
-
expect(niceQuotes(input, { punctuationStyle: "american" })).toBe(expected)
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
it("is the default behavior", () => {
|
|
488
|
-
expect(niceQuotes(periodOutsideDouble)).toBe(periodInsideDouble)
|
|
489
|
-
})
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
describe('when "british"', () => {
|
|
493
|
-
it.each([
|
|
494
|
-
[periodInsideDouble, periodOutsideDouble, "period inside double quotes"],
|
|
495
|
-
[periodInsideSingle, periodOutsideSingle, "period inside single quotes"],
|
|
496
|
-
[commaInsideDouble, commaOutsideDouble, "comma inside double quotes"],
|
|
497
|
-
])("moves punctuation outside: %s", (input, expected) => {
|
|
498
|
-
expect(niceQuotes(input, { punctuationStyle: "british" })).toBe(expected)
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
it("still converts straight quotes to smart quotes", () => {
|
|
502
|
-
expect(niceQuotes('"Hello."', { punctuationStyle: "british" })).toBe(periodOutsideDouble)
|
|
503
|
-
})
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
describe("terminal punctuation blocks movement", () => {
|
|
507
|
-
// American: comma outside stays outside for every terminal mark
|
|
508
|
-
it.each(TERMINAL_PUNCTUATION.map((mark) => [`${LEFT_DOUBLE_QUOTE}Test${mark}${RIGHT_DOUBLE_QUOTE},`]))(
|
|
509
|
-
"american comma stays outside: %s", (input) => {
|
|
510
|
-
expect(niceQuotes(input, { punctuationStyle: "american" })).toBe(input)
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
// American: period outside stays outside for every terminal mark
|
|
514
|
-
it.each(TERMINAL_PUNCTUATION.map((mark) => [`${LEFT_DOUBLE_QUOTE}Test${mark}${RIGHT_DOUBLE_QUOTE}.`]))(
|
|
515
|
-
"american period stays outside: %s", (input) => {
|
|
516
|
-
expect(niceQuotes(input, { punctuationStyle: "american" })).toBe(input)
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
// American: single quotes work the same as double
|
|
520
|
-
it("american comma stays outside for single quotes", () => {
|
|
521
|
-
expect(niceQuotes(`${LEFT_SINGLE_QUOTE}Stop!${RIGHT_SINGLE_QUOTE},`, { punctuationStyle: "american" }))
|
|
522
|
-
.toBe(`${LEFT_SINGLE_QUOTE}Stop!${RIGHT_SINGLE_QUOTE},`)
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
// British: comma inside always moves outside (even after terminal punctuation)
|
|
526
|
-
// Exclude "." — the period regex also fires, creating a cascading double-movement
|
|
527
|
-
it.each(TERMINAL_PUNCTUATION.filter((m) => m !== ".").map((mark) => [
|
|
528
|
-
`${LEFT_DOUBLE_QUOTE}Test${mark},${RIGHT_DOUBLE_QUOTE}`,
|
|
529
|
-
`${LEFT_DOUBLE_QUOTE}Test${mark}${RIGHT_DOUBLE_QUOTE},`,
|
|
530
|
-
]))("british comma moves outside: %s → %s", (input, expected) => {
|
|
531
|
-
expect(niceQuotes(input, { punctuationStyle: "british" })).toBe(expected)
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
// British: period inside always moves outside (even after terminal punctuation)
|
|
535
|
-
// Exclude "," — the comma regex also fires, creating a cascading double-movement
|
|
536
|
-
it.each(TERMINAL_PUNCTUATION.filter((m) => m !== ",").map((mark) => [
|
|
537
|
-
`${LEFT_DOUBLE_QUOTE}Test${mark}.${RIGHT_DOUBLE_QUOTE}`,
|
|
538
|
-
`${LEFT_DOUBLE_QUOTE}Test${mark}${RIGHT_DOUBLE_QUOTE}.`,
|
|
539
|
-
]))("british period moves outside: %s → %s", (input, expected) => {
|
|
540
|
-
expect(niceQuotes(input, { punctuationStyle: "british" })).toBe(expected)
|
|
541
|
-
})
|
|
542
|
-
})
|
|
543
|
-
|
|
544
|
-
describe("MLA excluded from punctuation movement", () => {
|
|
545
|
-
it.each([
|
|
546
|
-
// American: MLA period/comma stay put (MLA is not a quote character)
|
|
547
|
-
[`the dog${MODIFIER_LETTER_APOSTROPHE}s.`, `the dog${MODIFIER_LETTER_APOSTROPHE}s.`, "american"],
|
|
548
|
-
[`Brien${MODIFIER_LETTER_APOSTROPHE}s,`, `Brien${MODIFIER_LETTER_APOSTROPHE}s,`, "american"],
|
|
549
|
-
// American: RSQ period/comma move inside (contrast)
|
|
550
|
-
[`${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE}.`, `${LEFT_SINGLE_QUOTE}hello.${RIGHT_SINGLE_QUOTE}`, "american"],
|
|
551
|
-
// British: RSQ period moves outside (contrast)
|
|
552
|
-
[`${LEFT_SINGLE_QUOTE}hello.${RIGHT_SINGLE_QUOTE}`, `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE}.`, "british"],
|
|
553
|
-
])('%s → %s (%s)', (input, expected, style) => {
|
|
554
|
-
expect(classifyApostrophes(input, { punctuationStyle: style as "american" | "british" })).toBe(expected)
|
|
555
|
-
})
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
describe('when "none"', () => {
|
|
559
|
-
it.each([
|
|
560
|
-
[periodOutsideDouble],
|
|
561
|
-
[periodInsideDouble],
|
|
562
|
-
[commaOutsideDouble],
|
|
563
|
-
['"Hello".'],
|
|
564
|
-
["It's a test"],
|
|
565
|
-
])("skips all quote transforms: %s", (input) => {
|
|
566
|
-
expect(niceQuotes(input, { punctuationStyle: "none" })).toBe(input)
|
|
567
|
-
})
|
|
568
|
-
})
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
describe("empty and whitespace quotes", () => {
|
|
572
|
-
it.each([
|
|
573
|
-
["''", `${LEFT_SINGLE_QUOTE}${RIGHT_SINGLE_QUOTE}`, "empty single"],
|
|
574
|
-
['""', `${LEFT_DOUBLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`, "empty double"],
|
|
575
|
-
["' '", `${LEFT_SINGLE_QUOTE} ${RIGHT_SINGLE_QUOTE}`, "whitespace single"],
|
|
576
|
-
['" "', `${LEFT_DOUBLE_QUOTE} ${RIGHT_DOUBLE_QUOTE}`, "whitespace double"],
|
|
577
|
-
])("converts %s → %s (%s)", (input, expected) => {
|
|
578
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
579
|
-
})
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
describe("niceQuotes vs classifyApostrophes", () => {
|
|
583
|
-
const RSQ = RIGHT_SINGLE_QUOTE
|
|
584
|
-
|
|
585
|
-
it.each([
|
|
586
|
-
["contractions", "don't", `don${RSQ}t`],
|
|
587
|
-
["possessives", "the dog's bone", `the dog${RSQ}s bone`],
|
|
588
|
-
["plural possessives", "the dogs' owner", `the dogs${RSQ} owner`],
|
|
589
|
-
["O'Brien-style names", "O'Brien's idea", `O${RSQ}Brien${RSQ}s idea`],
|
|
590
|
-
["'n' abbreviation", "Rock 'n' Roll", `Rock ${RSQ}n${RSQ} Roll`],
|
|
591
|
-
["decades", "the '90s", `the ${RSQ}90s`],
|
|
592
|
-
["leading apostrophes", "'twas", `${RSQ}twas`],
|
|
593
|
-
])('defaults to RSQ for %s', (_label, input, expected) => {
|
|
594
|
-
expect(niceQuotes(input)).toBe(expected)
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
it("classifyApostrophes uses MLA for apostrophes", () => {
|
|
598
|
-
expect(classifyApostrophes("don't")).toBe(`don${MODIFIER_LETTER_APOSTROPHE}t`)
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
it("RSQ output is idempotent", () => {
|
|
602
|
-
const inputs = ["don't", "the dog's", "the dogs'", "O'Brien", "Rock 'n' Roll", "'90s"]
|
|
603
|
-
for (const input of inputs) {
|
|
604
|
-
const first = niceQuotes(input)
|
|
605
|
-
expect(niceQuotes(first)).toBe(first)
|
|
606
|
-
}
|
|
607
|
-
})
|
|
608
|
-
|
|
609
|
-
it("classifyApostrophes output is idempotent", () => {
|
|
610
|
-
const inputs = ["don't", "the dog's", "the dogs'", "O'Brien", "Rock 'n' Roll", "'90s"]
|
|
611
|
-
for (const input of inputs) {
|
|
612
|
-
const first = classifyApostrophes(input)
|
|
613
|
-
expect(classifyApostrophes(first)).toBe(first)
|
|
614
|
-
}
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
describe("RSQ→MLA normalization (external system input)", () => {
|
|
618
|
-
it.each([
|
|
619
|
-
[`don${RIGHT_SINGLE_QUOTE}t`, `don${MODIFIER_LETTER_APOSTROPHE}t`],
|
|
620
|
-
[`I${RIGHT_SINGLE_QUOTE}m`, `I${MODIFIER_LETTER_APOSTROPHE}m`],
|
|
621
|
-
[`they${RIGHT_SINGLE_QUOTE}re`, `they${MODIFIER_LETTER_APOSTROPHE}re`],
|
|
622
|
-
[`O${RIGHT_SINGLE_QUOTE}Brien`, `O${MODIFIER_LETTER_APOSTROPHE}Brien`],
|
|
623
|
-
[`the dog${RIGHT_SINGLE_QUOTE}s bone`, `the dog${MODIFIER_LETTER_APOSTROPHE}s bone`],
|
|
624
|
-
// Mixed MLA + RSQ in same text
|
|
625
|
-
[`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`],
|
|
626
|
-
])('normalizes RSQ contraction "%s"', (input, expected) => {
|
|
627
|
-
expect(classifyApostrophes(input)).toBe(expected)
|
|
628
|
-
})
|
|
629
|
-
|
|
630
|
-
it("does NOT normalize RSQ when it is a closing quote", () => {
|
|
631
|
-
const input = `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} world`
|
|
632
|
-
expect(classifyApostrophes(input)).toBe(input)
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
it.each([
|
|
636
|
-
[`[test]${RIGHT_SINGLE_QUOTE}`, "bracket"],
|
|
637
|
-
[`${LEFT_SINGLE_QUOTE}Hello.${RIGHT_SINGLE_QUOTE}`, "period"],
|
|
638
|
-
[`${LEFT_SINGLE_QUOTE}Really?${RIGHT_SINGLE_QUOTE}`, "question mark"],
|
|
639
|
-
[`${LEFT_SINGLE_QUOTE}Stop!${RIGHT_SINGLE_QUOTE}`, "exclamation"],
|
|
640
|
-
[`${LEFT_SINGLE_QUOTE}test,${RIGHT_SINGLE_QUOTE} more`, "comma"],
|
|
641
|
-
])("preserves closing RSQ preceded by non-word char (%s)", (input) => {
|
|
642
|
-
expect(classifyApostrophes(input)).toBe(input)
|
|
643
|
-
})
|
|
644
|
-
|
|
645
|
-
it("normalizes RSQ possessive with digit prefix", () => {
|
|
646
|
-
expect(classifyApostrophes(`GPT-2${RIGHT_SINGLE_QUOTE}s`)).toBe(`GPT-2${MODIFIER_LETTER_APOSTROPHE}s`)
|
|
647
|
-
})
|
|
648
|
-
|
|
649
|
-
it("normalizes RSQ plural possessive to MLA", () => {
|
|
650
|
-
expect(classifyApostrophes(`the dogs${RIGHT_SINGLE_QUOTE} owner`)).toBe(`the dogs${MODIFIER_LETTER_APOSTROPHE} owner`)
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
it("preserves RSQ plural possessive when matched with LSQ", () => {
|
|
654
|
-
const input = `${LEFT_SINGLE_QUOTE}dogs${RIGHT_SINGLE_QUOTE} and more`
|
|
655
|
-
expect(classifyApostrophes(input)).toBe(input)
|
|
656
|
-
})
|
|
657
|
-
})
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
describe("locale quotes", () => {
|
|
661
|
-
describe.each([
|
|
662
|
-
// German double quotes
|
|
663
|
-
['"Guten Tag"', `${DOUBLE_LOW_9_QUOTE}Guten Tag${LEFT_DOUBLE_QUOTE}`, { punctuationStyle: "german" as const }],
|
|
664
|
-
['"Hello," she said.', `${DOUBLE_LOW_9_QUOTE}Hello${LEFT_DOUBLE_QUOTE}, she said.`, { punctuationStyle: "german" as const }],
|
|
665
|
-
// German single quotes
|
|
666
|
-
["'Hallo'", `${SINGLE_LOW_9_QUOTE}Hallo${LEFT_SINGLE_QUOTE}`, { punctuationStyle: "german" as const }],
|
|
667
|
-
// German apostrophes stay as RSQ (after MLA→RSQ conversion in niceQuotes)
|
|
668
|
-
["it's", `it${RIGHT_SINGLE_QUOTE}s`, { punctuationStyle: "german" as const }],
|
|
669
|
-
// French double quotes (NNBSP per Imprimerie nationale / Unicode CLDR)
|
|
670
|
-
['"Bonjour"', `${LEFT_GUILLEMET}${NNBSP}Bonjour${NNBSP}${RIGHT_GUILLEMET}`, { punctuationStyle: "french" as const }],
|
|
671
|
-
// French apostrophes stay as RSQ
|
|
672
|
-
["l'homme", `l${RIGHT_SINGLE_QUOTE}homme`, { punctuationStyle: "french" as const }],
|
|
673
|
-
// Nested quotes in German
|
|
674
|
-
[`"She said 'hello'"`, `${DOUBLE_LOW_9_QUOTE}She said ${SINGLE_LOW_9_QUOTE}hello${LEFT_SINGLE_QUOTE}${LEFT_DOUBLE_QUOTE}`, { punctuationStyle: "german" as const }],
|
|
675
|
-
// French punctuation placement
|
|
676
|
-
['"Bonjour," dit-il.', `${LEFT_GUILLEMET}${NNBSP}Bonjour${NNBSP}${RIGHT_GUILLEMET}, dit-il.`, { punctuationStyle: "french" as const }],
|
|
677
|
-
// German leading apostrophe ('Twas)
|
|
678
|
-
["'Twas the night", `${RIGHT_SINGLE_QUOTE}Twas the night`, { punctuationStyle: "german" as const }],
|
|
679
|
-
])("locale quotes: %s → %s", (input, expected, options) => {
|
|
680
|
-
it("transforms correctly", () => {
|
|
681
|
-
expect(niceQuotes(input, options)).toBe(expected)
|
|
682
|
-
})
|
|
683
|
-
})
|
|
684
|
-
|
|
685
|
-
it.each([
|
|
686
|
-
[`${DOUBLE_LOW_9_QUOTE}Guten Tag${LEFT_DOUBLE_QUOTE}`, { punctuationStyle: "german" as const }],
|
|
687
|
-
[`${SINGLE_LOW_9_QUOTE}Hallo${LEFT_SINGLE_QUOTE}`, { punctuationStyle: "german" as const }],
|
|
688
|
-
[`${LEFT_GUILLEMET}${NNBSP}Bonjour${NNBSP}${RIGHT_GUILLEMET}`, { punctuationStyle: "french" as const }],
|
|
689
|
-
// German apostrophe idempotency — RSQ must not flip to LSQ on re-processing
|
|
690
|
-
[`${RIGHT_SINGLE_QUOTE}Twas the night`, { punctuationStyle: "german" as const }],
|
|
691
|
-
])("locale output is idempotent: %s", (input, options) => {
|
|
692
|
-
expect(niceQuotes(input, options)).toBe(input)
|
|
693
|
-
})
|
|
694
|
-
|
|
695
|
-
it("german normalizer re-classifies pre-existing RDQ", () => {
|
|
696
|
-
// RDQ is not used in German — should become German closing double
|
|
697
|
-
const input = `${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}`
|
|
698
|
-
expect(niceQuotes(input, { punctuationStyle: "german" })).toBe(`${DOUBLE_LOW_9_QUOTE}Hello${LEFT_DOUBLE_QUOTE}`)
|
|
699
|
-
})
|
|
700
|
-
})
|
|
701
|
-
|
|
702
|
-
describe("nested quote punctuation placement", () => {
|
|
703
|
-
it.each([
|
|
704
|
-
// Period moves past all closing quotes in one pass
|
|
705
|
-
[
|
|
706
|
-
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello.${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`,
|
|
707
|
-
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}.`,
|
|
708
|
-
],
|
|
709
|
-
// Comma moves past all closing quotes in one pass
|
|
710
|
-
[
|
|
711
|
-
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello,${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE} she replied.`,
|
|
712
|
-
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}, she replied.`,
|
|
713
|
-
],
|
|
714
|
-
])("british moves past all closing quotes: %s → %s", (input, expected) => {
|
|
715
|
-
expect(niceQuotes(input, { punctuationStyle: "british" })).toBe(expected)
|
|
716
|
-
})
|
|
717
|
-
|
|
718
|
-
it.each(["british", "german", "french"] as const)("%s nested quote period is idempotent", (style) => {
|
|
719
|
-
const first = niceQuotes(`"He said, 'Hello.'"`, { punctuationStyle: style })
|
|
720
|
-
expect(niceQuotes(first, { punctuationStyle: style })).toBe(first)
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
it.each([
|
|
724
|
-
// Period moves past all closing quotes in one pass
|
|
725
|
-
[
|
|
726
|
-
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}.`,
|
|
727
|
-
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello.${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`,
|
|
728
|
-
],
|
|
729
|
-
// Comma moves past all closing quotes in one pass
|
|
730
|
-
[
|
|
731
|
-
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}, she replied.`,
|
|
732
|
-
`${LEFT_DOUBLE_QUOTE}He said, ${LEFT_SINGLE_QUOTE}Hello,${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE} she replied.`,
|
|
733
|
-
],
|
|
734
|
-
])("american moves past all closing quotes: %s → %s", (input, expected) => {
|
|
735
|
-
const result = niceQuotes(input, { punctuationStyle: "american" })
|
|
736
|
-
expect(result).toBe(expected)
|
|
737
|
-
expect(niceQuotes(result, { punctuationStyle: "american" })).toBe(result)
|
|
738
|
-
})
|
|
739
|
-
})
|
|
740
|
-
|
|
741
|
-
describe("pathological inputs", () => {
|
|
742
|
-
it("scales linearly for input without closing quote", () => {
|
|
743
|
-
assertLinearScaling(classifyApostrophes, (n) => `'${"a".repeat(n)}`)
|
|
744
|
-
const input = `'${"a".repeat(1500)}`
|
|
745
|
-
expect(classifyApostrophes(classifyApostrophes(input))).toBe(classifyApostrophes(input))
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
it("handles 16 rapid apostrophes", () => {
|
|
749
|
-
const input = "a'b'c'd'e'f'g'h'i'j'k'l'm'n'o'p"
|
|
750
|
-
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`
|
|
751
|
-
const result = classifyApostrophes(input)
|
|
752
|
-
expect(result).toBe(expected)
|
|
753
|
-
expect(classifyApostrophes(result)).toBe(result)
|
|
754
|
-
})
|
|
755
|
-
|
|
756
|
-
it("handles 500-char word + possessive", () => {
|
|
757
|
-
const input = `${"a".repeat(500)}'s thing`
|
|
758
|
-
expect(classifyApostrophes(input)).toBe(`${"a".repeat(500)}${MODIFIER_LETTER_APOSTROPHE}s thing`)
|
|
759
|
-
})
|
|
760
|
-
})
|
|
761
|
-
|
|
762
|
-
describe("mixed-locale stress", () => {
|
|
763
|
-
it.each([
|
|
764
|
-
["german", `Er sagte, "es ist gro\u00DFartig" und sie sagte, "h\u00F6r nicht auf"`, DOUBLE_LOW_9_QUOTE],
|
|
765
|
-
["french", `"C'est magnifique," dit-elle. "N'arr\u00EAte pas."`, LEFT_GUILLEMET],
|
|
766
|
-
["american", `He said, "She said, 'They said, "No."'"`, LEFT_DOUBLE_QUOTE],
|
|
767
|
-
["british", `He said, "She said, 'They said, "No."'"`, LEFT_DOUBLE_QUOTE],
|
|
768
|
-
["german", `Er fragte, "Hat sie 'Nein' gesagt?"`, DOUBLE_LOW_9_QUOTE],
|
|
769
|
-
["french", `"Qui a dit 'bonjour'?" demanda-t-il.`, LEFT_GUILLEMET],
|
|
770
|
-
] as const)("%s style produces locale-appropriate quotes and is idempotent", (style, input, expectedChar) => {
|
|
771
|
-
const opts = { punctuationStyle: style }
|
|
772
|
-
const first = niceQuotes(input, opts)
|
|
773
|
-
expect(first).toContain(expectedChar)
|
|
774
|
-
expect(niceQuotes(first, opts)).toBe(first)
|
|
775
|
-
})
|
|
776
|
-
})
|
|
777
|
-
|
|
778
|
-
describe("apostrophe regex scaling", () => {
|
|
779
|
-
it("scales linearly for repeated apostrophe patterns", () => {
|
|
780
|
-
assertLinearScaling(classifyApostrophes, (n) => "'a".repeat(n))
|
|
781
|
-
})
|
|
782
|
-
})
|
|
783
|
-
})
|