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.
- package/README.md +32 -25
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -1
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +26 -8
- package/dist/dashes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +17 -12
- package/dist/quotes.js.map +1 -1
- package/dist/rehype.d.ts +161 -0
- package/dist/rehype.d.ts.map +1 -0
- package/dist/rehype.js +401 -0
- package/dist/rehype.js.map +1 -0
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +2 -1
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +19 -0
- package/dist/utils.js.map +1 -1
- package/package.json +28 -4
- package/src/constants.ts +161 -0
- package/src/dashes.ts +211 -0
- package/src/index.ts +243 -0
- package/src/quotes.ts +132 -0
- package/src/rehype.ts +503 -0
- package/src/symbols.ts +324 -0
- package/src/tests/dashes.test.ts +649 -0
- package/src/tests/index.test.ts +528 -0
- package/src/tests/quotes.test.ts +344 -0
- package/src/tests/rehype.test.ts +418 -0
- package/src/tests/symbols.test.ts +708 -0
- package/src/tests/utils.test.ts +98 -0
- package/src/utils.ts +65 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { hyphenReplace, enDashNumberRange, enDashDateRange, minusReplace, numberRangeDisallowedPrefixes } from "../dashes.js"
|
|
2
|
+
import { DEFAULT_SEPARATOR, 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
|
+
EN_DASH,
|
|
11
|
+
MINUS,
|
|
12
|
+
ELLIPSIS,
|
|
13
|
+
} = UNICODE_SYMBOLS
|
|
14
|
+
|
|
15
|
+
describe("hyphenReplace", () => {
|
|
16
|
+
describe("em dashes from surrounded hyphens", () => {
|
|
17
|
+
it.each([
|
|
18
|
+
["This is a - hyphen.", `This is a${EM_DASH}hyphen.`],
|
|
19
|
+
// Parenthetical dash before a digit (not math subtraction)
|
|
20
|
+
["Safari) - 9 combinations", `Safari)${EM_DASH}9 combinations`],
|
|
21
|
+
[`This is an ${EM_DASH} em dash.`, `This is an${EM_DASH}em dash.`],
|
|
22
|
+
[`word ${EM_DASH} word`, `word${EM_DASH}word`],
|
|
23
|
+
["word ---", `word${EM_DASH}`],
|
|
24
|
+
[`word${EM_DASH} word`, `word${EM_DASH}word`],
|
|
25
|
+
[`word ${EM_DASH}word`, `word${EM_DASH}word`],
|
|
26
|
+
['"I love dogs." - Me', `"I love dogs."${EM_DASH}Me`],
|
|
27
|
+
["- Me", `${EM_DASH} Me`],
|
|
28
|
+
["-- Me", `${EM_DASH} Me`],
|
|
29
|
+
["Hi-- what do you think?", `Hi${EM_DASH}what do you think?`],
|
|
30
|
+
[
|
|
31
|
+
`${EM_DASH}such behaviors still have to be retrodicted`,
|
|
32
|
+
`${EM_DASH}such behaviors still have to be retrodicted`,
|
|
33
|
+
],
|
|
34
|
+
[`emphasis" ${EM_DASH}`, `emphasis"${EM_DASH}`],
|
|
35
|
+
["- Intro text\n then more", `${EM_DASH} Intro text\n then more`],
|
|
36
|
+
[
|
|
37
|
+
`reward${ELLIPSIS} ${EM_DASH} [Model-based RL, Desires, Brains, Wireheading](https://www.alignmentforum.org/posts/K5ikTdaNymfWXQHFb/model-based-rl-desires-brains-wireheading#Self_aware_desires_1__wireheading)`,
|
|
38
|
+
`reward${ELLIPSIS}${EM_DASH}[Model-based RL, Desires, Brains, Wireheading](https://www.alignmentforum.org/posts/K5ikTdaNymfWXQHFb/model-based-rl-desires-brains-wireheading#Self_aware_desires_1__wireheading)`,
|
|
39
|
+
],
|
|
40
|
+
["a browser- or OS-specific fashion", "a browser- or OS-specific fashion"],
|
|
41
|
+
["since--as you know", `since${EM_DASH}as you know`],
|
|
42
|
+
// Arrow patterns should be preserved (not converted to em-dashes)
|
|
43
|
+
["word -> arrow", "word -> arrow"],
|
|
44
|
+
["word --> arrow", "word --> arrow"],
|
|
45
|
+
["-> start", "-> start"],
|
|
46
|
+
["--> start", "--> start"],
|
|
47
|
+
])('should replace hyphens in "%s"', (input, expected) => {
|
|
48
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("multiple dashes within words", () => {
|
|
53
|
+
it.each([
|
|
54
|
+
["Since--as you know", `Since${EM_DASH}as you know`, "double dashes"],
|
|
55
|
+
["word---another", `word${EM_DASH}another`, "triple dashes"],
|
|
56
|
+
])("%s → %s (%s)", (input, expected) => {
|
|
57
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("dashes at start of line", () => {
|
|
62
|
+
it.each([
|
|
63
|
+
["- Author", `${EM_DASH} Author`],
|
|
64
|
+
["--- Author attribution", `${EM_DASH} Author attribution`],
|
|
65
|
+
["Quote\n- Author", `Quote\n${EM_DASH} Author`],
|
|
66
|
+
])('handles "%s"', (input, expected) => {
|
|
67
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("spaces around em dashes", () => {
|
|
72
|
+
it.each([
|
|
73
|
+
[`word ${EM_DASH} another`, `word${EM_DASH}another`],
|
|
74
|
+
[`word${EM_DASH} another`, `word${EM_DASH}another`],
|
|
75
|
+
[`word ${EM_DASH}another`, `word${EM_DASH}another`],
|
|
76
|
+
])('removes spaces in "%s"', (input, expected) => {
|
|
77
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe("quote-to-quote em dash (Chicago: no spaces)", () => {
|
|
82
|
+
it.each([
|
|
83
|
+
// Chicago style: no spaces around em-dashes, even between quotes
|
|
84
|
+
`"Hello."${EM_DASH}"World"`,
|
|
85
|
+
`'Hi.'${EM_DASH}'There'`,
|
|
86
|
+
`${LEFT_DOUBLE_QUOTE}Hello.${RIGHT_DOUBLE_QUOTE}${EM_DASH}${LEFT_DOUBLE_QUOTE}World${RIGHT_DOUBLE_QUOTE}`,
|
|
87
|
+
`${LEFT_SINGLE_QUOTE}Hi.${RIGHT_SINGLE_QUOTE}${EM_DASH}${LEFT_SINGLE_QUOTE}There${RIGHT_SINGLE_QUOTE}`,
|
|
88
|
+
])('preserves unspaced em-dash in "%s"', (input) => {
|
|
89
|
+
expect(hyphenReplace(input)).toBe(input)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe("em dashes at start of line", () => {
|
|
94
|
+
it.each([
|
|
95
|
+
[`${EM_DASH}Start of line`, `${EM_DASH} Start of line`],
|
|
96
|
+
[`Line 1\n${EM_DASH}Line 2`, `Line 1\n${EM_DASH} Line 2`],
|
|
97
|
+
[`${EM_DASH} Already correct`, `${EM_DASH} Already correct`],
|
|
98
|
+
])('handles "%s"', (input, expected) => {
|
|
99
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe("number ranges to en dashes", () => {
|
|
104
|
+
it.each([
|
|
105
|
+
["Pages 1-5", `Pages 1${EN_DASH}5`],
|
|
106
|
+
["2000-2020", `2000${EN_DASH}2020`],
|
|
107
|
+
["2018-2021. Then 1-3", `2018${EN_DASH}2021. Then 1${EN_DASH}3`],
|
|
108
|
+
["p.10-15", `p.10${EN_DASH}15`],
|
|
109
|
+
])('converts "%s"', (input, expected) => {
|
|
110
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe("with separator character", () => {
|
|
115
|
+
const sep = "\uE000"
|
|
116
|
+
it.each([
|
|
117
|
+
[`word${sep} - ${sep}another`, `word${sep}${EM_DASH}${sep}another`, "em dash context"],
|
|
118
|
+
[`pages 1${sep}-${sep}5`, `pages 1${sep}${EN_DASH}${sep}5`, "number ranges"],
|
|
119
|
+
// American em-dash (Chicago style) consumes surrounding spaces, even across separator boundaries
|
|
120
|
+
[`text ${EM_DASH} ${sep} more text`, `text${EM_DASH}${sep}more text`, "consumes space after separator for em-dash"],
|
|
121
|
+
[`text - ${sep} more`, `text${EM_DASH}${sep}more`, "consumes space after separator with hyphen"],
|
|
122
|
+
])("%s → %s (%s)", (input, expected) => {
|
|
123
|
+
expect(hyphenReplace(input, { separator: sep })).toBe(expected)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it.each([
|
|
127
|
+
// British en-dash (spaced) preserves trailing space after separator
|
|
128
|
+
[`text - ${sep} more`, `text ${EN_DASH} ${sep} more`, "preserves space after separator for en-dash"],
|
|
129
|
+
])("%s → %s (%s)", (input, expected) => {
|
|
130
|
+
expect(hyphenReplace(input, { separator: sep, dashStyle: "british" })).toBe(expected)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe("enDashNumberRange", () => {
|
|
136
|
+
it.each([
|
|
137
|
+
["1-2", `1${EN_DASH}2`],
|
|
138
|
+
["10-20", `10${EN_DASH}20`],
|
|
139
|
+
["100-200", `100${EN_DASH}200`],
|
|
140
|
+
["1000-2000", `1000${EN_DASH}2000`],
|
|
141
|
+
["1,000-2,000", `1,000${EN_DASH}2,000`],
|
|
142
|
+
["1.000-2.000", `1.000${EN_DASH}2.000`],
|
|
143
|
+
["1-2 and 3-4", `1${EN_DASH}2 and 3${EN_DASH}4`],
|
|
144
|
+
["from 5-10 to 15-20", `from 5${EN_DASH}10 to 15${EN_DASH}20`],
|
|
145
|
+
["1-2-3", "1-2-3"], // Multi-segment patterns are not ranges
|
|
146
|
+
["a-b", "a-b"], // Don't replace non-numeric ranges
|
|
147
|
+
["1a-2b", "1a-2b"], // Don't replace if not purely numeric
|
|
148
|
+
["a1-2b", "a1-2b"], // Don't replace if not purely numeric
|
|
149
|
+
["p. 206-207)", `p. 206${EN_DASH}207)`],
|
|
150
|
+
["Qwen1.5-1.8", "Qwen1.5-1.8"], // Don't replace if there's a decimal
|
|
151
|
+
["$100-$200", `$100${EN_DASH}$200`],
|
|
152
|
+
["$1.50-$3.50", `$1.50${EN_DASH}$3.50`],
|
|
153
|
+
["$1-3", `$1${EN_DASH}3`],
|
|
154
|
+
["1 - 2", "1 - 2"], // Spaced ranges should not change
|
|
155
|
+
// Multiplier suffixes
|
|
156
|
+
["1-10x", `1${EN_DASH}10x`], // lowercase x for multiplier
|
|
157
|
+
["1-10K", `1${EN_DASH}10K`], // uppercase K for kilo
|
|
158
|
+
["1-10M", `1${EN_DASH}10M`], // uppercase M for million
|
|
159
|
+
["1-10B", `1${EN_DASH}10B`], // uppercase B for billion
|
|
160
|
+
["1-10T", `1${EN_DASH}10T`], // uppercase T for trillion
|
|
161
|
+
["1-10X", "1-10X"], // uppercase X should NOT match (only lowercase)
|
|
162
|
+
["1-10k", "1-10k"], // lowercase k should NOT match (only uppercase)
|
|
163
|
+
])('should convert "%s" to "%s"', (input, expected) => {
|
|
164
|
+
expect(enDashNumberRange(input)).toBe(expected)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe("disallowed prefix characters", () => {
|
|
168
|
+
it.each(numberRangeDisallowedPrefixes)(
|
|
169
|
+
'should not convert number ranges preceded by "%s"',
|
|
170
|
+
(prefix) => {
|
|
171
|
+
const input = `foo${prefix}2-7B`
|
|
172
|
+
expect(enDashNumberRange(input)).toBe(input)
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
it.each([
|
|
177
|
+
// Model names with version numbers should NOT be converted
|
|
178
|
+
["Llama-2-7B-chat", "Llama-2-7B-chat"],
|
|
179
|
+
["Llama-2-7B", "Llama-2-7B"],
|
|
180
|
+
["Llama-3-8B-Instruct", "Llama-3-8B-Instruct"],
|
|
181
|
+
["ReLU-2-4", "ReLU-2-4"],
|
|
182
|
+
])('should not convert "%s"', (input, expected) => {
|
|
183
|
+
expect(enDashNumberRange(input)).toBe(expected)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe("marker robustness", () => {
|
|
188
|
+
// Tests that separators don't create false word boundaries
|
|
189
|
+
const sep = DEFAULT_SEPARATOR
|
|
190
|
+
|
|
191
|
+
it.each([
|
|
192
|
+
// [description, input, expected]
|
|
193
|
+
["false leading boundary", `x${sep}1-10`, `x${sep}1-10`], // "x1-10" - should NOT convert
|
|
194
|
+
["multiplier suffix with separator", `1-10${sep}x`, `1${EN_DASH}10${sep}x`], // range of multipliers - should convert
|
|
195
|
+
["false trailing boundary (non-suffix)", `1-10${sep}y`, `1-10${sep}y`], // "y" is not a valid suffix
|
|
196
|
+
["valid boundaries with space", `pages ${sep}1-10${sep} total`, `pages ${sep}1${EN_DASH}10${sep} total`], // should convert
|
|
197
|
+
])("handles %s", (_desc, input, expected) => {
|
|
198
|
+
expect(enDashNumberRange(input, { separator: sep })).toBe(expected)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe("enDashDateRange", () => {
|
|
204
|
+
describe("american style (default, unspaced)", () => {
|
|
205
|
+
it.each([
|
|
206
|
+
["January-March", `January${EN_DASH}March`],
|
|
207
|
+
["Jan-Mar", `Jan${EN_DASH}Mar`],
|
|
208
|
+
["February-April 2024", `February${EN_DASH}April 2024`],
|
|
209
|
+
["May-June", `May${EN_DASH}June`],
|
|
210
|
+
["Sep-Nov", `Sep${EN_DASH}Nov`],
|
|
211
|
+
["December-January", `December${EN_DASH}January`],
|
|
212
|
+
["October 2012 - December 2014", `October 2012${EN_DASH}December 2014`],
|
|
213
|
+
["Oct 2012 - Dec 2014", `Oct 2012${EN_DASH}Dec 2014`],
|
|
214
|
+
["January 2020 - March 2020", `January 2020${EN_DASH}March 2020`],
|
|
215
|
+
["May 2019-June 2020", `May 2019${EN_DASH}June 2020`],
|
|
216
|
+
["Jan 2000 - Feb", `Jan 2000${EN_DASH}Feb`],
|
|
217
|
+
["March - April 2025", `March${EN_DASH}April 2025`],
|
|
218
|
+
])('should convert "%s" to "%s"', (input, expected) => {
|
|
219
|
+
expect(enDashDateRange(input)).toBe(expected)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe("british style (spaced)", () => {
|
|
224
|
+
it.each([
|
|
225
|
+
["January-March", `January ${EN_DASH} March`],
|
|
226
|
+
["Jan-Mar", `Jan ${EN_DASH} Mar`],
|
|
227
|
+
["October 2012 - December 2014", `October 2012 ${EN_DASH} December 2014`],
|
|
228
|
+
["Oct 2012 - Dec 2014", `Oct 2012 ${EN_DASH} Dec 2014`],
|
|
229
|
+
["May 2019-June 2020", `May 2019 ${EN_DASH} June 2020`],
|
|
230
|
+
])('should convert "%s" to "%s"', (input, expected) => {
|
|
231
|
+
expect(enDashDateRange(input, { dashStyle: "british" })).toBe(expected)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe("none style (preserve spacing)", () => {
|
|
236
|
+
it.each([
|
|
237
|
+
["January-March", `January${EN_DASH}March`],
|
|
238
|
+
["January - March", `January ${EN_DASH} March`],
|
|
239
|
+
["October 2012 - December 2014", `October 2012 ${EN_DASH} December 2014`],
|
|
240
|
+
["Oct 2012-Dec 2014", `Oct 2012${EN_DASH}Dec 2014`],
|
|
241
|
+
])('should convert "%s" to "%s"', (input, expected) => {
|
|
242
|
+
expect(enDashDateRange(input, { dashStyle: "none" })).toBe(expected)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it("should not convert non-month words", () => {
|
|
247
|
+
expect(enDashDateRange("hello-world")).toBe("hello-world")
|
|
248
|
+
expect(enDashDateRange("Mon-Fri")).toBe("Mon-Fri") // Days, not months
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe("marker robustness", () => {
|
|
252
|
+
// Tests that separators don't create false word boundaries
|
|
253
|
+
const sep = DEFAULT_SEPARATOR
|
|
254
|
+
|
|
255
|
+
it.each([
|
|
256
|
+
// [description, input, expected]
|
|
257
|
+
["false leading boundary", `x${sep}January-March`, `x${sep}January-March`], // "xJanuary-March" - should NOT convert
|
|
258
|
+
["false trailing boundary", `January-March${sep}x`, `January-March${sep}x`], // "January-Marchx" - should NOT convert
|
|
259
|
+
["valid boundaries with space", `from ${sep}January-March${sep} period`, `from ${sep}January${EN_DASH}March${sep} period`], // should convert
|
|
260
|
+
])("handles %s", (_desc, input, expected) => {
|
|
261
|
+
expect(enDashDateRange(input, { separator: sep })).toBe(expected)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe("minusReplace", () => {
|
|
267
|
+
it.each([
|
|
268
|
+
["-5", `${MINUS}5`],
|
|
269
|
+
["-5.5", `${MINUS}5.5`],
|
|
270
|
+
["(-5)", `(${MINUS}5)`],
|
|
271
|
+
["The value is -10", `The value is ${MINUS}10`],
|
|
272
|
+
[" -3", ` ${MINUS}3`],
|
|
273
|
+
['"-5"', `"${MINUS}5"`],
|
|
274
|
+
// Spaced math subtraction (digit before)
|
|
275
|
+
["5 - 3", `5 ${MINUS} 3`],
|
|
276
|
+
["10 - 5 = 5", `10 ${MINUS} 5 = 5`],
|
|
277
|
+
["(100 - 50)", `(100 ${MINUS} 50)`],
|
|
278
|
+
])('should convert "%s" to use minus sign', (input, expected) => {
|
|
279
|
+
expect(minusReplace(input)).toBe(expected)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it("should not convert hyphens in other contexts", () => {
|
|
283
|
+
expect(minusReplace("well-known")).toBe("well-known")
|
|
284
|
+
expect(minusReplace("re-read")).toBe("re-read")
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it("should not convert spaced hyphens without digit before (parenthetical)", () => {
|
|
288
|
+
// These should be left for em-dash conversion, not minus
|
|
289
|
+
expect(minusReplace("Safari) - 9 combinations")).toBe("Safari) - 9 combinations")
|
|
290
|
+
expect(minusReplace("word - 5")).toBe("word - 5")
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it("should handle subtraction of negative numbers", () => {
|
|
294
|
+
expect(minusReplace("5 - -3")).toBe(`5 ${MINUS} ${MINUS}3`)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
describe("enDashNumberRange preserves", () => {
|
|
299
|
+
it.each([
|
|
300
|
+
"555-123-4567", "+1-555-123-4567", "(555)-123-4567", // phones
|
|
301
|
+
"978-3-16-148410-0", "0-13-468599-1", // ISBNs
|
|
302
|
+
"2024-01-15", "2024-01", "1999-12", // ISO dates
|
|
303
|
+
"192-168-1-1", "12-34-5678", // IPs
|
|
304
|
+
])('"%s"', (input) => {
|
|
305
|
+
expect(enDashNumberRange(input)).toBe(input)
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
describe("negative number ranges", () => {
|
|
310
|
+
it.each([
|
|
311
|
+
["-5--2", `${MINUS}5${EN_DASH}${MINUS}2`],
|
|
312
|
+
["-5-5", `${MINUS}5${EN_DASH}5`],
|
|
313
|
+
["-5-3-7", `${MINUS}5-3-7`], // multi-segment preserved
|
|
314
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
315
|
+
expect(enDashNumberRange(minusReplace(input))).toBe(expected)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// Tests derived from competitor libraries and typography guidelines
|
|
320
|
+
describe("competitor-derived edge cases", () => {
|
|
321
|
+
// From retext-smartypants: oldschool dash handling
|
|
322
|
+
describe("double/triple dash patterns", () => {
|
|
323
|
+
it.each([
|
|
324
|
+
// From retext-smartypants oldschool mode
|
|
325
|
+
["word--word", `word${EM_DASH}word`],
|
|
326
|
+
["word---word", `word${EM_DASH}word`],
|
|
327
|
+
["start-- end", `start${EM_DASH}end`],
|
|
328
|
+
])('converts "%s"', (input, expected) => {
|
|
329
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("handles double dash after quote", () => {
|
|
333
|
+
const input = '"Hello"-- she said'
|
|
334
|
+
const result = hyphenReplace(input)
|
|
335
|
+
expect(result).toBe(`"Hello"${EM_DASH}she said`)
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// Phone numbers and ISBNs - documented via enDashNumberRange tests above
|
|
340
|
+
// These multi-segment patterns are correctly preserved
|
|
341
|
+
describe("multi-segment pattern preservation", () => {
|
|
342
|
+
it.each([
|
|
343
|
+
// Already tested in enDashNumberRange preserves section
|
|
344
|
+
"555-123-4567", // phone
|
|
345
|
+
"978-3-16-148410-0", // ISBN
|
|
346
|
+
"2024-01-15", // ISO date
|
|
347
|
+
])('preserves multi-segment pattern: "%s"', (input) => {
|
|
348
|
+
expect(enDashNumberRange(input)).toBe(input)
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
// From tipograph: dash after quoted text
|
|
353
|
+
describe("dashes with quotes", () => {
|
|
354
|
+
it.each([
|
|
355
|
+
// Em dash after closing quote with space
|
|
356
|
+
[`${RIGHT_DOUBLE_QUOTE} - author`, `${RIGHT_DOUBLE_QUOTE}${EM_DASH}author`],
|
|
357
|
+
[`${RIGHT_SINGLE_QUOTE} -- source`, `${RIGHT_SINGLE_QUOTE}${EM_DASH}source`],
|
|
358
|
+
])('handles spaced dashes after quotes: "%s"', (input, expected) => {
|
|
359
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it("handles unspaced double dash between quotes", () => {
|
|
363
|
+
const input = '"first"--"second"'
|
|
364
|
+
const result = hyphenReplace(input)
|
|
365
|
+
expect(result).toBe(`"first"${EM_DASH}"second"`)
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
describe("complex real-world patterns", () => {
|
|
371
|
+
describe("year ranges", () => {
|
|
372
|
+
it.each([
|
|
373
|
+
["2020-2024", `2020${EN_DASH}2024`],
|
|
374
|
+
["the 1990-1999 decade", `the 1990${EN_DASH}1999 decade`],
|
|
375
|
+
["(2010-2015)", `(2010${EN_DASH}2015)`],
|
|
376
|
+
])('handles year range: "%s"', (input, expected) => {
|
|
377
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
describe("score ranges", () => {
|
|
382
|
+
it.each([
|
|
383
|
+
["won 3-2", `won 3${EN_DASH}2`],
|
|
384
|
+
["final score 21-17", `final score 21${EN_DASH}17`],
|
|
385
|
+
["led 14-7", `led 14${EN_DASH}7`],
|
|
386
|
+
])('handles score: "%s"', (input, expected) => {
|
|
387
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
describe("price ranges", () => {
|
|
392
|
+
it.each([
|
|
393
|
+
// Dollar sign is supported
|
|
394
|
+
["$10-$20", `$10${EN_DASH}$20`],
|
|
395
|
+
])('handles USD price range: "%s"', (input, expected) => {
|
|
396
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it.each([
|
|
400
|
+
["€5-€10", `€5${EN_DASH}€10`],
|
|
401
|
+
["£100-£200", `£100${EN_DASH}£200`],
|
|
402
|
+
["¥1000-¥2000", `¥1000${EN_DASH}¥2000`],
|
|
403
|
+
])('handles non-USD currency range: "%s"', (input, expected) => {
|
|
404
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
describe("mixed content", () => {
|
|
409
|
+
it("handles multiple em dash transformations", () => {
|
|
410
|
+
const input = 'The meeting -- scheduled for today -- covers important topics.'
|
|
411
|
+
const expected = `The meeting${EM_DASH}scheduled for today${EM_DASH}covers important topics.`
|
|
412
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it("handles page ranges", () => {
|
|
416
|
+
const input = 'See pages 10-20 for details.'
|
|
417
|
+
const expected = `See pages 10${EN_DASH}20 for details.`
|
|
418
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it("handles em dash before URL-like text", () => {
|
|
422
|
+
const input = 'Visit -- https://example.com'
|
|
423
|
+
const expected = `Visit${EM_DASH}https://example.com`
|
|
424
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it.each([
|
|
428
|
+
["2-3pm", `2${EN_DASH}3pm`],
|
|
429
|
+
["9-10am", `9${EN_DASH}10am`],
|
|
430
|
+
["2-3PM", `2${EN_DASH}3PM`],
|
|
431
|
+
["9-10AM", `9${EN_DASH}10AM`],
|
|
432
|
+
])('handles time range: "%s"', (input, expected) => {
|
|
433
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
describe("idempotency", () => {
|
|
439
|
+
it.each([
|
|
440
|
+
`word${EM_DASH}word`,
|
|
441
|
+
`1${EN_DASH}5`,
|
|
442
|
+
`${MINUS}10`,
|
|
443
|
+
`January${EN_DASH}March`,
|
|
444
|
+
])('is idempotent for: "%s"', (input) => {
|
|
445
|
+
expect(hyphenReplace(input)).toBe(input)
|
|
446
|
+
expect(hyphenReplace(hyphenReplace(input))).toBe(input)
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
describe("dashStyle option", () => {
|
|
451
|
+
describe('when "american" (default)', () => {
|
|
452
|
+
it.each([
|
|
453
|
+
["word - word", `word${EM_DASH}word`],
|
|
454
|
+
["word -- word", `word${EM_DASH}word`],
|
|
455
|
+
["since--as you know", `since${EM_DASH}as you know`],
|
|
456
|
+
])("uses unspaced em dash: %s", (input, expected) => {
|
|
457
|
+
expect(hyphenReplace(input, { dashStyle: "american" })).toBe(expected)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it("is the default behavior", () => {
|
|
461
|
+
expect(hyphenReplace("word - word")).toBe(`word${EM_DASH}word`)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
describe('when "british" (Oxford style)', () => {
|
|
466
|
+
it.each([
|
|
467
|
+
["word - word", `word ${EN_DASH} word`],
|
|
468
|
+
["word -- word", `word ${EN_DASH} word`],
|
|
469
|
+
["since--as you know", `since ${EN_DASH} as you know`],
|
|
470
|
+
])("uses spaced en dash: %s", (input, expected) => {
|
|
471
|
+
expect(hyphenReplace(input, { dashStyle: "british" })).toBe(expected)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it("still converts number ranges to en dashes", () => {
|
|
475
|
+
expect(hyphenReplace("pages 1-5", { dashStyle: "british" })).toBe(`pages 1${EN_DASH}5`)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it("converts existing em-dashes to spaced en-dashes", () => {
|
|
479
|
+
// Oxford style uses spaced en-dashes, so em-dashes get converted
|
|
480
|
+
expect(hyphenReplace(`word ${EM_DASH} word`, { dashStyle: "british" })).toBe(`word ${EN_DASH} word`)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it("converts date ranges with spaced en dash", () => {
|
|
484
|
+
expect(hyphenReplace("January-March", { dashStyle: "british" })).toBe(`January ${EN_DASH} March`)
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
describe('when "none"', () => {
|
|
489
|
+
it.each([
|
|
490
|
+
["word - word"],
|
|
491
|
+
["word -- word"],
|
|
492
|
+
])("leaves parenthetical dashes unchanged: %s", (input) => {
|
|
493
|
+
expect(hyphenReplace(input, { dashStyle: "none" })).toBe(input)
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it("still converts number ranges", () => {
|
|
497
|
+
expect(hyphenReplace("pages 1-5", { dashStyle: "none" })).toBe(`pages 1${EN_DASH}5`)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it("still converts minus signs", () => {
|
|
501
|
+
expect(hyphenReplace("-5", { dashStyle: "none" })).toBe(`${MINUS}5`)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
describe("style conversion consistency", () => {
|
|
506
|
+
const testCases = [
|
|
507
|
+
"word - word",
|
|
508
|
+
"word -- word",
|
|
509
|
+
"since--as you know",
|
|
510
|
+
"Hello. -- Author",
|
|
511
|
+
`word${EM_DASH}word`,
|
|
512
|
+
`"Quote."${EM_DASH}Author`,
|
|
513
|
+
]
|
|
514
|
+
|
|
515
|
+
it.each(testCases)(
|
|
516
|
+
"American → British equals direct British: %s",
|
|
517
|
+
(input) => {
|
|
518
|
+
const directBritish = hyphenReplace(input, { dashStyle: "british" })
|
|
519
|
+
const viaAmerican = hyphenReplace(
|
|
520
|
+
hyphenReplace(input, { dashStyle: "american" }),
|
|
521
|
+
{ dashStyle: "british" }
|
|
522
|
+
)
|
|
523
|
+
expect(viaAmerican).toBe(directBritish)
|
|
524
|
+
}
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
it.each(testCases)(
|
|
528
|
+
"British → American equals direct American: %s",
|
|
529
|
+
(input) => {
|
|
530
|
+
const directAmerican = hyphenReplace(input, { dashStyle: "american" })
|
|
531
|
+
const viaBritish = hyphenReplace(
|
|
532
|
+
hyphenReplace(input, { dashStyle: "british" }),
|
|
533
|
+
{ dashStyle: "american" }
|
|
534
|
+
)
|
|
535
|
+
expect(viaBritish).toBe(directAmerican)
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
})
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
describe("technical patterns preservation", () => {
|
|
542
|
+
it.each([
|
|
543
|
+
["https://example-site.com", "https://example-site.com"],
|
|
544
|
+
["http://sub-domain.example.com/path-to-file", "http://sub-domain.example.com/path-to-file"],
|
|
545
|
+
["user-name@example.com", "user-name@example.com"],
|
|
546
|
+
["550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000"],
|
|
547
|
+
["commit 1a2b3c4d-5e6f", "commit 1a2b3c4d-5e6f"],
|
|
548
|
+
])('preserves technical pattern: "%s"', (input, expected) => {
|
|
549
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
describe("scientific notation", () => {
|
|
554
|
+
it.each([
|
|
555
|
+
["1e-10", "1e-10"],
|
|
556
|
+
["5.5e-3", "5.5e-3"],
|
|
557
|
+
["1E-5", "1E-5"],
|
|
558
|
+
["3.14e+10", "3.14e+10"],
|
|
559
|
+
])('preserves scientific notation: "%s"', (input, expected) => {
|
|
560
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
describe("version numbers", () => {
|
|
565
|
+
it.each([
|
|
566
|
+
["v1.0.0-beta", "v1.0.0-beta"],
|
|
567
|
+
["1.0.0-rc.1", "1.0.0-rc.1"],
|
|
568
|
+
["2.0.0-alpha.1", "2.0.0-alpha.1"],
|
|
569
|
+
["1.0.0-beta.1-hotfix", "1.0.0-beta.1-hotfix"],
|
|
570
|
+
])('preserves version number: "%s"', (input, expected) => {
|
|
571
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
describe("edge number ranges", () => {
|
|
576
|
+
it.each([
|
|
577
|
+
["1000000-2000000", `1000000${EN_DASH}2000000`],
|
|
578
|
+
["1.5-2.5", `1.5${EN_DASH}2.5`],
|
|
579
|
+
["pp. 100-200", `pp. 100${EN_DASH}200`],
|
|
580
|
+
["I-V", "I-V"],
|
|
581
|
+
["i-v", "i-v"],
|
|
582
|
+
["Chapter I-III", "Chapter I-III"],
|
|
583
|
+
])('handles number range edge case: "%s"', (input, expected) => {
|
|
584
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
585
|
+
})
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
describe("social media patterns", () => {
|
|
589
|
+
it.each([
|
|
590
|
+
["#my-hashtag", "#my-hashtag"],
|
|
591
|
+
["@user-name", "@user-name"],
|
|
592
|
+
])('preserves social pattern: "%s"', (input, expected) => {
|
|
593
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
describe("mixed dash types", () => {
|
|
598
|
+
it.each([
|
|
599
|
+
[`pages 1${EN_DASH}5`, `pages 1${EN_DASH}5`],
|
|
600
|
+
[`word${EM_DASH}word`, `word${EM_DASH}word`],
|
|
601
|
+
[`pages 1-5 and word${EM_DASH}word`, `pages 1${EN_DASH}5 and word${EM_DASH}word`],
|
|
602
|
+
])('handles mixed dashes: "%s"', (input, expected) => {
|
|
603
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
604
|
+
})
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
describe("negative temperatures", () => {
|
|
608
|
+
it.each([
|
|
609
|
+
["-5 to -10", `${MINUS}5 to ${MINUS}10`],
|
|
610
|
+
["High: 5, Low: -10", `High: 5, Low: ${MINUS}10`],
|
|
611
|
+
])('handles negative temperatures: "%s"', (input, expected) => {
|
|
612
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
613
|
+
})
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
describe("phone number preservation", () => {
|
|
617
|
+
it.each([
|
|
618
|
+
// Preserved phone patterns
|
|
619
|
+
["555-123-4567", "555-123-4567", "full phone with area code"],
|
|
620
|
+
["(555) 123-4567", "(555) 123-4567", "area code in parens"],
|
|
621
|
+
["1-800", "1-800", "toll-free prefix 800"],
|
|
622
|
+
["1-888", "1-888", "toll-free prefix 888"],
|
|
623
|
+
["1-877", "1-877", "toll-free prefix 877"],
|
|
624
|
+
["1-866", "1-866", "toll-free prefix 866"],
|
|
625
|
+
["1-855", "1-855", "toll-free prefix 855"],
|
|
626
|
+
["Call 1-800-...", "Call 1-800-...", "truncated toll-free"],
|
|
627
|
+
["1-800-555-1234", "1-800-555-1234", "full toll-free"],
|
|
628
|
+
["1-888-555-1234", "1-888-555-1234", "full toll-free 888"],
|
|
629
|
+
["+1-800-555-1234", "+1-800-555-1234", "international US"],
|
|
630
|
+
["+44-20-7946-0958", "+44-20-7946-0958", "international UK"],
|
|
631
|
+
["+1 (800) 555-1234", "+1 (800) 555-1234", "international with parens"],
|
|
632
|
+
// Converted range patterns
|
|
633
|
+
["555-1234", `555${EN_DASH}1234`, "standalone 3+4 as range"],
|
|
634
|
+
["1-5", `1${EN_DASH}5`, "simple range"],
|
|
635
|
+
["1-50", `1${EN_DASH}50`, "range to two digits"],
|
|
636
|
+
["1-99", `1${EN_DASH}99`, "range to 99"],
|
|
637
|
+
["2-800", `2${EN_DASH}800`, "non-US country code pattern"],
|
|
638
|
+
])("%s → %s (%s)", (input, expected) => {
|
|
639
|
+
expect(hyphenReplace(input)).toBe(expected)
|
|
640
|
+
})
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
describe("minusReplace regex-special separators", () => {
|
|
644
|
+
const regexSpecialChars = [".", "*", "+", "?", "^", "$", "[", "]", "\\", "|", "(", ")"]
|
|
645
|
+
|
|
646
|
+
it.each(regexSpecialChars)("handles '%s' as separator", (sep) => {
|
|
647
|
+
expect(() => minusReplace("-5", { separator: sep })).not.toThrow()
|
|
648
|
+
})
|
|
649
|
+
})
|