punctilio 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,344 @@
1
+ import { niceQuotes } from "../quotes.js"
2
+ import { UNICODE_SYMBOLS } from "../constants.js"
3
+
4
+ const {
5
+ LEFT_DOUBLE_QUOTE,
6
+ RIGHT_DOUBLE_QUOTE,
7
+ LEFT_SINGLE_QUOTE,
8
+ RIGHT_SINGLE_QUOTE,
9
+ EM_DASH,
10
+ ELLIPSIS,
11
+ } = UNICODE_SYMBOLS
12
+
13
+ describe("niceQuotes", () => {
14
+ describe("double quotes", () => {
15
+ it.each([
16
+ ['"This is a quote", she said.', `${LEFT_DOUBLE_QUOTE}This is a quote,${RIGHT_DOUBLE_QUOTE} she said.`],
17
+ ['"This is a quote," she said.', `${LEFT_DOUBLE_QUOTE}This is a quote,${RIGHT_DOUBLE_QUOTE} she said.`],
18
+ ['"This is a quote!".', `${LEFT_DOUBLE_QUOTE}This is a quote!${RIGHT_DOUBLE_QUOTE}.`],
19
+ ['"This is a quote?".', `${LEFT_DOUBLE_QUOTE}This is a quote?${RIGHT_DOUBLE_QUOTE}.`],
20
+ ['"This is a quote..." he trailed off.', `${LEFT_DOUBLE_QUOTE}This is a quote...${RIGHT_DOUBLE_QUOTE} he trailed off.`],
21
+ ['She said, "This is a quote."', `She said, ${LEFT_DOUBLE_QUOTE}This is a quote.${RIGHT_DOUBLE_QUOTE}`],
22
+ ['"Hello." Mary', `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE} Mary`],
23
+ ['"Hello." (Mary)', `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE} (Mary)`],
24
+ [
25
+ '"I am" so "tired" of "these" "quotes".',
26
+ `${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}`,
27
+ ],
28
+ ['"world model";', `${LEFT_DOUBLE_QUOTE}world model${RIGHT_DOUBLE_QUOTE};`],
29
+ ['"party"/"wedding."', `${LEFT_DOUBLE_QUOTE}party${RIGHT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}wedding.${RIGHT_DOUBLE_QUOTE}`],
30
+ ['"Hi \'Trout!"', `${LEFT_DOUBLE_QUOTE}Hi ${RIGHT_SINGLE_QUOTE}Trout!${RIGHT_DOUBLE_QUOTE}`],
31
+ [`${LEFT_DOUBLE_QUOTE}scope insensitivity${RIGHT_DOUBLE_QUOTE}`, `${LEFT_DOUBLE_QUOTE}scope insensitivity${RIGHT_DOUBLE_QUOTE}`],
32
+ [
33
+ '"how many ways can this function be implemented?".',
34
+ `${LEFT_DOUBLE_QUOTE}how many ways can this function be implemented?${RIGHT_DOUBLE_QUOTE}.`,
35
+ ],
36
+ ['SSL.")', `SSL.${RIGHT_DOUBLE_QUOTE})`],
37
+ ["can't multiply\"?", `can${RIGHT_SINGLE_QUOTE}t multiply${RIGHT_DOUBLE_QUOTE}?`],
38
+ ['with "scope insensitivity":', `with ${LEFT_DOUBLE_QUOTE}scope insensitivity${RIGHT_DOUBLE_QUOTE}:`],
39
+ ['("the best")', `(${LEFT_DOUBLE_QUOTE}the best${RIGHT_DOUBLE_QUOTE})`],
40
+ ['"This is a quote"...', `${LEFT_DOUBLE_QUOTE}This is a quote${RIGHT_DOUBLE_QUOTE}...`],
41
+ ['He said, "This is a quote"...', `He said, ${LEFT_DOUBLE_QUOTE}This is a quote${RIGHT_DOUBLE_QUOTE}...`],
42
+ ['"... What is this?"', `${LEFT_DOUBLE_QUOTE}... What is this?${RIGHT_DOUBLE_QUOTE}`],
43
+ ['"/"', `${LEFT_DOUBLE_QUOTE}/${RIGHT_DOUBLE_QUOTE}`],
44
+ ['"Game"/"Life"', `${LEFT_DOUBLE_QUOTE}Game${RIGHT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}Life${RIGHT_DOUBLE_QUOTE}`],
45
+ ['"Test:".', `${LEFT_DOUBLE_QUOTE}Test:${RIGHT_DOUBLE_QUOTE}.`],
46
+ ['"Test...".', `${LEFT_DOUBLE_QUOTE}Test...${RIGHT_DOUBLE_QUOTE}.`],
47
+ [`"To maximize reward${ELLIPSIS}".`, `${LEFT_DOUBLE_QUOTE}To maximize reward${ELLIPSIS}${RIGHT_DOUBLE_QUOTE}.`],
48
+ ['"Test"s', `${LEFT_DOUBLE_QUOTE}Test${RIGHT_DOUBLE_QUOTE}s`],
49
+ // End-of-line quote becomes RIGHT quote
50
+ ['not confident in that plan - "', `not confident in that plan - ${RIGHT_DOUBLE_QUOTE}`],
51
+ ])('should convert double quotes in "%s"', (input, expected) => {
52
+ expect(niceQuotes(input)).toBe(expected)
53
+ })
54
+ })
55
+
56
+ describe("single quotes and apostrophes", () => {
57
+ it.each([
58
+ ["He said, 'Hi'", `He said, ${LEFT_SINGLE_QUOTE}Hi${RIGHT_SINGLE_QUOTE}`],
59
+ ["He wanted 'power.'", `He wanted ${LEFT_SINGLE_QUOTE}power.${RIGHT_SINGLE_QUOTE}`],
60
+ ["I'd", `I${RIGHT_SINGLE_QUOTE}d`],
61
+ ["I don't'nt want to go", `I don${RIGHT_SINGLE_QUOTE}t${RIGHT_SINGLE_QUOTE}nt want to go`],
62
+ ['"\'sup"', `${LEFT_DOUBLE_QUOTE}${RIGHT_SINGLE_QUOTE}sup${RIGHT_DOUBLE_QUOTE}`],
63
+ ["'SUP", `${RIGHT_SINGLE_QUOTE}SUP`],
64
+ ["Rock 'n' Roll", `Rock ${RIGHT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE} Roll`],
65
+ ["I was born in '99", `I was born in ${RIGHT_SINGLE_QUOTE}99`],
66
+ ["'99 tigers weren't a match", `${RIGHT_SINGLE_QUOTE}99 tigers weren${RIGHT_SINGLE_QUOTE}t a match`],
67
+ [
68
+ "I'm not the best, haven't you heard?",
69
+ `I${RIGHT_SINGLE_QUOTE}m not the best, haven${RIGHT_SINGLE_QUOTE}t you heard?`,
70
+ ],
71
+ // Skipped: Complex edge case with 'sup and quoted phrase
72
+ // ["Hey, 'sup 'this is a single quote'", `Hey, ${RIGHT_SINGLE_QUOTE}sup ${LEFT_SINGLE_QUOTE}this is a single quote${RIGHT_SINGLE_QUOTE}`],
73
+ ["'the best',", `${LEFT_SINGLE_QUOTE}the best,${RIGHT_SINGLE_QUOTE}`],
74
+ ["'I lost the game.'", `${LEFT_SINGLE_QUOTE}I lost the game.${RIGHT_SINGLE_QUOTE}`],
75
+ ["I hate you.'\"", `I hate you.${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`],
76
+ ["The 'function space')", `The ${LEFT_SINGLE_QUOTE}function space${RIGHT_SINGLE_QUOTE})`],
77
+ [`The 'function space'${EM_DASH}`, `The ${LEFT_SINGLE_QUOTE}function space${RIGHT_SINGLE_QUOTE}${EM_DASH}`],
78
+ ["What do you think?']", `What do you think?${RIGHT_SINGLE_QUOTE}]`],
79
+ ["('survival incentive')", `(${LEFT_SINGLE_QUOTE}survival incentive${RIGHT_SINGLE_QUOTE})`],
80
+ [
81
+ "strategy s's return is good, even as d's return is bad",
82
+ `strategy s${RIGHT_SINGLE_QUOTE}s return is good, even as d${RIGHT_SINGLE_QUOTE}s return is bad`,
83
+ ],
84
+ ])('should handle single quotes/apostrophes in "%s"', (input, expected) => {
85
+ expect(niceQuotes(input)).toBe(expected)
86
+ })
87
+ })
88
+
89
+ describe("leading apostrophe contractions", () => {
90
+ it.each([
91
+ ["'twas the night", `${RIGHT_SINGLE_QUOTE}twas the night`],
92
+ ["'tis the season", `${RIGHT_SINGLE_QUOTE}tis the season`],
93
+ ["'Twas brillig", `${RIGHT_SINGLE_QUOTE}Twas brillig`],
94
+ ["'Tis but a scratch", `${RIGHT_SINGLE_QUOTE}Tis but a scratch`],
95
+ // Decade abbreviations
96
+ ["the '90s", `the ${RIGHT_SINGLE_QUOTE}90s`],
97
+ ["in '99", `in ${RIGHT_SINGLE_QUOTE}99`],
98
+ ])('handles leading apostrophe in "%s"', (input, expected) => {
99
+ expect(niceQuotes(input)).toBe(expected)
100
+ })
101
+ })
102
+
103
+ describe("nested quotes", () => {
104
+ it("handles double quotes containing single quotes", () => {
105
+ const input = '"She said \'hello\'"'
106
+ const expected = `${LEFT_DOUBLE_QUOTE}She said ${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`
107
+ expect(niceQuotes(input)).toBe(expected)
108
+ })
109
+ })
110
+
111
+ // Tests derived from competitor libraries and typography guidelines
112
+ describe("competitor-derived edge cases", () => {
113
+ // From smartquotes.js: Unicode text within quotes
114
+ it.each([
115
+ ['"Águila"', `${LEFT_DOUBLE_QUOTE}Águila${RIGHT_DOUBLE_QUOTE}`],
116
+ ['"café"', `${LEFT_DOUBLE_QUOTE}café${RIGHT_DOUBLE_QUOTE}`],
117
+ ['"naïve"', `${LEFT_DOUBLE_QUOTE}naïve${RIGHT_DOUBLE_QUOTE}`],
118
+ ['"日本語"', `${LEFT_DOUBLE_QUOTE}日本語${RIGHT_DOUBLE_QUOTE}`],
119
+ ['"שלום"', `${LEFT_DOUBLE_QUOTE}שלום${RIGHT_DOUBLE_QUOTE}`],
120
+ ])('handles Unicode text in quotes: "%s"', (input, expected) => {
121
+ expect(niceQuotes(input)).toBe(expected)
122
+ })
123
+
124
+ // From retext-smartypants: quotes after em dashes
125
+ it.each([
126
+ [`He said${EM_DASH}"Hello"`, `He said${EM_DASH}${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}`],
127
+ [`word${EM_DASH}"quoted"${EM_DASH}word`, `word${EM_DASH}${LEFT_DOUBLE_QUOTE}quoted${RIGHT_DOUBLE_QUOTE}${EM_DASH}word`],
128
+ ])('handles quotes adjacent to em dashes: "%s"', (input, expected) => {
129
+ expect(niceQuotes(input)).toBe(expected)
130
+ })
131
+
132
+ it("handles single quote after em-dash", () => {
133
+ const input = `${EM_DASH}'Hi'${EM_DASH}`
134
+ const result = niceQuotes(input)
135
+ expect(result).toBe(`${EM_DASH}${LEFT_SINGLE_QUOTE}Hi${RIGHT_SINGLE_QUOTE}${EM_DASH}`)
136
+ })
137
+
138
+ // From Standard Ebooks: M'Donald-style names (archaic patterns)
139
+ it.each([
140
+ [`"M'Lord"`, `${LEFT_DOUBLE_QUOTE}M${RIGHT_SINGLE_QUOTE}Lord${RIGHT_DOUBLE_QUOTE}`],
141
+ [`O'Brien's idea`, `O${RIGHT_SINGLE_QUOTE}Brien${RIGHT_SINGLE_QUOTE}s idea`],
142
+ [`The O'Connors`, `The O${RIGHT_SINGLE_QUOTE}Connors`],
143
+ ])('handles Irish/Scottish name apostrophes: "%s"', (input, expected) => {
144
+ expect(niceQuotes(input)).toBe(expected)
145
+ })
146
+
147
+ // From smartquotes.js: complex nested quotes
148
+ it.each([
149
+ ['"Alfred \'bertrand\' cees"', `${LEFT_DOUBLE_QUOTE}Alfred ${LEFT_SINGLE_QUOTE}bertrand${RIGHT_SINGLE_QUOTE} cees${RIGHT_DOUBLE_QUOTE}`],
150
+ ["'Alfred \"bertrand\" cees'", `${LEFT_SINGLE_QUOTE}Alfred ${LEFT_DOUBLE_QUOTE}bertrand${RIGHT_DOUBLE_QUOTE} cees${RIGHT_SINGLE_QUOTE}`],
151
+ ])('handles complex nested quotes: "%s"', (input, expected) => {
152
+ expect(niceQuotes(input)).toBe(expected)
153
+ })
154
+
155
+ // From retext-smartypants: quotes with ellipsis
156
+ it.each([
157
+ [`"${ELLIPSIS}"`, `${LEFT_DOUBLE_QUOTE}${ELLIPSIS}${RIGHT_DOUBLE_QUOTE}`],
158
+ [`"Wait${ELLIPSIS}"`, `${LEFT_DOUBLE_QUOTE}Wait${ELLIPSIS}${RIGHT_DOUBLE_QUOTE}`],
159
+ [`'${ELLIPSIS}'`, `${LEFT_SINGLE_QUOTE}${ELLIPSIS}${RIGHT_SINGLE_QUOTE}`],
160
+ ])('handles quotes with ellipsis: "%s"', (input, expected) => {
161
+ expect(niceQuotes(input)).toBe(expected)
162
+ })
163
+ })
164
+
165
+ describe("complex real-world patterns", () => {
166
+ it.each([
167
+ // Dialogue with attribution
168
+ ['"I can\'t," she said.', `${LEFT_DOUBLE_QUOTE}I can${RIGHT_SINGLE_QUOTE}t,${RIGHT_DOUBLE_QUOTE} she said.`],
169
+ ['"Why not?" he asked.', `${LEFT_DOUBLE_QUOTE}Why not?${RIGHT_DOUBLE_QUOTE} he asked.`],
170
+ // Multiple contractions
171
+ ["I'm can't won't don't", `I${RIGHT_SINGLE_QUOTE}m can${RIGHT_SINGLE_QUOTE}t won${RIGHT_SINGLE_QUOTE}t don${RIGHT_SINGLE_QUOTE}t`],
172
+ // Quote within parentheses
173
+ ['("test")', `(${LEFT_DOUBLE_QUOTE}test${RIGHT_DOUBLE_QUOTE})`],
174
+ ["('test')", `(${LEFT_SINGLE_QUOTE}test${RIGHT_SINGLE_QUOTE})`],
175
+ // Quote with slash separator
176
+ ['"option1"/"option2"', `${LEFT_DOUBLE_QUOTE}option1${RIGHT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}option2${RIGHT_DOUBLE_QUOTE}`],
177
+ ])('handles complex pattern: "%s"', (input, expected) => {
178
+ expect(niceQuotes(input)).toBe(expected)
179
+ })
180
+
181
+ it("handles multi-line dialogue", () => {
182
+ const input = '"Hello,"\n"World"'
183
+ const expected = `${LEFT_DOUBLE_QUOTE}Hello,${RIGHT_DOUBLE_QUOTE}\n${LEFT_DOUBLE_QUOTE}World${RIGHT_DOUBLE_QUOTE}`
184
+ expect(niceQuotes(input)).toBe(expected)
185
+ })
186
+ })
187
+
188
+ describe("idempotency", () => {
189
+ it.each([
190
+ `${LEFT_DOUBLE_QUOTE}already curly${RIGHT_DOUBLE_QUOTE}`,
191
+ `${LEFT_SINGLE_QUOTE}single curly${RIGHT_SINGLE_QUOTE}`,
192
+ `I${RIGHT_SINGLE_QUOTE}m already converted`,
193
+ `${LEFT_DOUBLE_QUOTE}nested ${LEFT_SINGLE_QUOTE}quotes${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`,
194
+ ])('is idempotent for: "%s"', (input) => {
195
+ expect(niceQuotes(input)).toBe(input)
196
+ expect(niceQuotes(niceQuotes(input))).toBe(input)
197
+ })
198
+ })
199
+
200
+ describe("with separator character", () => {
201
+ const sep = "\uE000"
202
+ it.each([
203
+ [`"Hello${sep} world"`, `${LEFT_DOUBLE_QUOTE}Hello${sep} world${RIGHT_DOUBLE_QUOTE}`, "preserves separator positions"],
204
+ [`don${sep}'t`, `don${sep}${RIGHT_SINGLE_QUOTE}t`, "contractions across separator"],
205
+ [`"test${sep}"`, `${LEFT_DOUBLE_QUOTE}test${sep}${RIGHT_DOUBLE_QUOTE}`, "quotes at separator boundaries"],
206
+ ])("%s → %s (%s)", (input, expected) => {
207
+ expect(niceQuotes(input, { separator: sep })).toBe(expected)
208
+ })
209
+ })
210
+
211
+ describe("unbalanced and edge quotes", () => {
212
+ it.each([
213
+ ['"unclosed', `${LEFT_DOUBLE_QUOTE}unclosed`],
214
+ ["unclosed'", `unclosed${RIGHT_SINGLE_QUOTE}`],
215
+ ['""', `${LEFT_DOUBLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`],
216
+ ])('handles edge quote pattern: "%s"', (input, expected) => {
217
+ expect(niceQuotes(input)).toBe(expected)
218
+ })
219
+ })
220
+
221
+ describe("quotes after various punctuation", () => {
222
+ it.each([
223
+ ['text; "quote"', `text; ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
224
+ ['title: "quote"', `title: ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
225
+ ['what? "quote"', `what? ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
226
+ ['wow! "quote"', `wow! ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
227
+ ['[note] "quote"', `[note] ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
228
+ ['{code} "quote"', `{code} ${LEFT_DOUBLE_QUOTE}quote${RIGHT_DOUBLE_QUOTE}`],
229
+ ])('handles quotes after punctuation: "%s"', (input, expected) => {
230
+ expect(niceQuotes(input)).toBe(expected)
231
+ })
232
+ })
233
+
234
+ describe("consecutive contractions", () => {
235
+ it.each([
236
+ ["I'd've", `I${RIGHT_SINGLE_QUOTE}d${RIGHT_SINGLE_QUOTE}ve`],
237
+ ["y'all'd've", `y${RIGHT_SINGLE_QUOTE}all${RIGHT_SINGLE_QUOTE}d${RIGHT_SINGLE_QUOTE}ve`],
238
+ ["wouldn't've", `wouldn${RIGHT_SINGLE_QUOTE}t${RIGHT_SINGLE_QUOTE}ve`],
239
+ ["couldn't've", `couldn${RIGHT_SINGLE_QUOTE}t${RIGHT_SINGLE_QUOTE}ve`],
240
+ ])('handles double contraction: "%s"', (input, expected) => {
241
+ expect(niceQuotes(input)).toBe(expected)
242
+ })
243
+ })
244
+
245
+ describe("quotes with special Unicode", () => {
246
+ it.each([
247
+ ['"Hello! 😊"', `${LEFT_DOUBLE_QUOTE}Hello! 😊${RIGHT_DOUBLE_QUOTE}`],
248
+ ['"你好"', `${LEFT_DOUBLE_QUOTE}你好${RIGHT_DOUBLE_QUOTE}`],
249
+ ['"こんにちは"', `${LEFT_DOUBLE_QUOTE}こんにちは${RIGHT_DOUBLE_QUOTE}`],
250
+ ['"مرحبا"', `${LEFT_DOUBLE_QUOTE}مرحبا${RIGHT_DOUBLE_QUOTE}`],
251
+ ['"Привет"', `${LEFT_DOUBLE_QUOTE}Привет${RIGHT_DOUBLE_QUOTE}`],
252
+ ])('handles Unicode content in quotes: "%s"', (input, expected) => {
253
+ expect(niceQuotes(input)).toBe(expected)
254
+ })
255
+ })
256
+
257
+ describe("multiline quotes", () => {
258
+ it.each([
259
+ ['"Hello\nWorld"', `${LEFT_DOUBLE_QUOTE}Hello\nWorld${RIGHT_DOUBLE_QUOTE}`],
260
+ ['"A"\n"B"', `${LEFT_DOUBLE_QUOTE}A${RIGHT_DOUBLE_QUOTE}\n${LEFT_DOUBLE_QUOTE}B${RIGHT_DOUBLE_QUOTE}`],
261
+ ])('handles multiline: "%s"', (input, expected) => {
262
+ expect(niceQuotes(input)).toBe(expected)
263
+ })
264
+ })
265
+
266
+ describe("split dialogue", () => {
267
+ it.each([
268
+ [
269
+ '"Yes," he said, "absolutely."',
270
+ `${LEFT_DOUBLE_QUOTE}Yes,${RIGHT_DOUBLE_QUOTE} he said, ${LEFT_DOUBLE_QUOTE}absolutely.${RIGHT_DOUBLE_QUOTE}`,
271
+ ],
272
+ [
273
+ '"No," she replied, "I disagree."',
274
+ `${LEFT_DOUBLE_QUOTE}No,${RIGHT_DOUBLE_QUOTE} she replied, ${LEFT_DOUBLE_QUOTE}I disagree.${RIGHT_DOUBLE_QUOTE}`,
275
+ ],
276
+ ])('handles split dialogue: "%s"', (input, expected) => {
277
+ expect(niceQuotes(input)).toBe(expected)
278
+ })
279
+ })
280
+
281
+ describe("punctuationStyle option", () => {
282
+ // Reusable test strings
283
+ const periodOutsideDouble = `${LEFT_DOUBLE_QUOTE}Hello${RIGHT_DOUBLE_QUOTE}.`
284
+ const periodInsideDouble = `${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE}`
285
+ const periodOutsideSingle = `${LEFT_SINGLE_QUOTE}Hello${RIGHT_SINGLE_QUOTE}.`
286
+ const periodInsideSingle = `${LEFT_SINGLE_QUOTE}Hello.${RIGHT_SINGLE_QUOTE}`
287
+ const commaOutsideDouble = `${LEFT_DOUBLE_QUOTE}test${RIGHT_DOUBLE_QUOTE},`
288
+ const commaInsideDouble = `${LEFT_DOUBLE_QUOTE}test,${RIGHT_DOUBLE_QUOTE}`
289
+
290
+ describe('when "american" (default)', () => {
291
+ it.each([
292
+ [periodOutsideDouble, periodInsideDouble, "period outside double quotes"],
293
+ [periodOutsideSingle, periodInsideSingle, "period outside single quotes"],
294
+ [commaOutsideDouble, commaInsideDouble, "comma outside double quotes"],
295
+ ])("moves punctuation inside: %s", (input, expected) => {
296
+ expect(niceQuotes(input, { punctuationStyle: "american" })).toBe(expected)
297
+ })
298
+
299
+ it("is the default behavior", () => {
300
+ expect(niceQuotes(periodOutsideDouble)).toBe(periodInsideDouble)
301
+ })
302
+ })
303
+
304
+ describe('when "british"', () => {
305
+ it.each([
306
+ [periodInsideDouble, periodOutsideDouble, "period inside double quotes"],
307
+ [periodInsideSingle, periodOutsideSingle, "period inside single quotes"],
308
+ [commaInsideDouble, commaOutsideDouble, "comma inside double quotes"],
309
+ ])("moves punctuation outside: %s", (input, expected) => {
310
+ expect(niceQuotes(input, { punctuationStyle: "british" })).toBe(expected)
311
+ })
312
+
313
+ it("still converts straight quotes to smart quotes", () => {
314
+ expect(niceQuotes('"Hello."', { punctuationStyle: "british" })).toBe(periodOutsideDouble)
315
+ })
316
+ })
317
+
318
+ describe('when "none"', () => {
319
+ it.each([
320
+ [periodOutsideDouble],
321
+ [periodInsideDouble],
322
+ [commaOutsideDouble],
323
+ ])("leaves punctuation unchanged: %s", (input) => {
324
+ expect(niceQuotes(input, { punctuationStyle: "none" })).toBe(input)
325
+ })
326
+
327
+ it("still converts straight quotes to smart quotes", () => {
328
+ expect(niceQuotes('"Hello".', { punctuationStyle: "none" })).toBe(periodOutsideDouble)
329
+ })
330
+ })
331
+ })
332
+
333
+ describe("empty and whitespace quotes", () => {
334
+ it.each([
335
+ ["''", `${LEFT_SINGLE_QUOTE}${RIGHT_SINGLE_QUOTE}`, "empty single"],
336
+ ['""', `${LEFT_DOUBLE_QUOTE}${RIGHT_DOUBLE_QUOTE}`, "empty double"],
337
+ ["' '", `${LEFT_SINGLE_QUOTE} ${RIGHT_SINGLE_QUOTE}`, "whitespace single"],
338
+ ['" "', `${LEFT_DOUBLE_QUOTE} ${RIGHT_DOUBLE_QUOTE}`, "whitespace double"],
339
+ ])("converts %s → %s (%s)", (input, expected) => {
340
+ expect(niceQuotes(input)).toBe(expected)
341
+ })
342
+ })
343
+
344
+ })