punctilio 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +29 -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 +8 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +25 -0
- package/dist/utils.js.map +1 -1
- package/package.json +28 -4
- package/src/constants.ts +161 -0
- package/src/dashes.ts +217 -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 +653 -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 +128 -0
- package/src/utils.ts +72 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ellipsis,
|
|
3
|
+
multiplication,
|
|
4
|
+
mathSymbols,
|
|
5
|
+
legalSymbols,
|
|
6
|
+
arrows,
|
|
7
|
+
degrees,
|
|
8
|
+
primeMarks,
|
|
9
|
+
fractions,
|
|
10
|
+
collapseSpaces,
|
|
11
|
+
superscriptOrdinal,
|
|
12
|
+
punctuationLigatures,
|
|
13
|
+
symbolTransform,
|
|
14
|
+
} from "../symbols.js"
|
|
15
|
+
import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR } from "../constants.js"
|
|
16
|
+
|
|
17
|
+
describe("ellipsis", () => {
|
|
18
|
+
it.each([
|
|
19
|
+
["Wait for it...", `Wait for it${UNICODE_SYMBOLS.ELLIPSIS}`],
|
|
20
|
+
["Hmm... let me think", `Hmm${UNICODE_SYMBOLS.ELLIPSIS} let me think`],
|
|
21
|
+
["...", UNICODE_SYMBOLS.ELLIPSIS],
|
|
22
|
+
["Hello...world", `Hello${UNICODE_SYMBOLS.ELLIPSIS} world`],
|
|
23
|
+
["End of sentence...", `End of sentence${UNICODE_SYMBOLS.ELLIPSIS}`],
|
|
24
|
+
['..."', `${UNICODE_SYMBOLS.ELLIPSIS}"`],
|
|
25
|
+
["...!", `${UNICODE_SYMBOLS.ELLIPSIS}!`],
|
|
26
|
+
["...?", `${UNICODE_SYMBOLS.ELLIPSIS}?`],
|
|
27
|
+
["e.g.", "e.g."],
|
|
28
|
+
["U.S.A.", "U.S.A."],
|
|
29
|
+
["a.b", "a.b"],
|
|
30
|
+
["End....", `End${UNICODE_SYMBOLS.ELLIPSIS}.`], // 4 dots
|
|
31
|
+
["......", `${UNICODE_SYMBOLS.ELLIPSIS}${UNICODE_SYMBOLS.ELLIPSIS}`], // 6 dots
|
|
32
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
33
|
+
expect(ellipsis(input)).toBe(expected)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it.each([
|
|
37
|
+
["between all dots", `.${DEFAULT_SEPARATOR}.${DEFAULT_SEPARATOR}.`, `${UNICODE_SYMBOLS.ELLIPSIS}${DEFAULT_SEPARATOR}${DEFAULT_SEPARATOR}`],
|
|
38
|
+
["after ellipsis", `...${DEFAULT_SEPARATOR}`, `${UNICODE_SYMBOLS.ELLIPSIS}${DEFAULT_SEPARATOR}`],
|
|
39
|
+
["after first dot", `.${DEFAULT_SEPARATOR}..`, `${UNICODE_SYMBOLS.ELLIPSIS}${DEFAULT_SEPARATOR}`],
|
|
40
|
+
["after second dot", `..${DEFAULT_SEPARATOR}.`, `${UNICODE_SYMBOLS.ELLIPSIS}${DEFAULT_SEPARATOR}`],
|
|
41
|
+
])("preserves separators: %s", (_desc, input, expected) => {
|
|
42
|
+
const result = ellipsis(input, { separator: DEFAULT_SEPARATOR })
|
|
43
|
+
expect(result).toBe(expected)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe("multiplication", () => {
|
|
48
|
+
it.each([
|
|
49
|
+
["5x5", `5${UNICODE_SYMBOLS.MULTIPLICATION}5`],
|
|
50
|
+
["10 x 20", `10 ${UNICODE_SYMBOLS.MULTIPLICATION} 20`],
|
|
51
|
+
["10x20", `10${UNICODE_SYMBOLS.MULTIPLICATION}20`],
|
|
52
|
+
["The room is 10x12 feet", `The room is 10${UNICODE_SYMBOLS.MULTIPLICATION}12 feet`],
|
|
53
|
+
["2x speed", `2${UNICODE_SYMBOLS.MULTIPLICATION} speed`],
|
|
54
|
+
["3x", `3${UNICODE_SYMBOLS.MULTIPLICATION}`],
|
|
55
|
+
["5*3", `5${UNICODE_SYMBOLS.MULTIPLICATION}3`],
|
|
56
|
+
["Resolution: 1920x1080", `Resolution: 1920${UNICODE_SYMBOLS.MULTIPLICATION}1080`],
|
|
57
|
+
// Test trailing multiplier before punctuation (word boundary cases)
|
|
58
|
+
['2x"', `2${UNICODE_SYMBOLS.MULTIPLICATION}"`],
|
|
59
|
+
["2x'", `2${UNICODE_SYMBOLS.MULTIPLICATION}'`],
|
|
60
|
+
["2x.", `2${UNICODE_SYMBOLS.MULTIPLICATION}.`],
|
|
61
|
+
["2x,", `2${UNICODE_SYMBOLS.MULTIPLICATION},`],
|
|
62
|
+
["2x!", `2${UNICODE_SYMBOLS.MULTIPLICATION}!`],
|
|
63
|
+
["2x?", `2${UNICODE_SYMBOLS.MULTIPLICATION}?`],
|
|
64
|
+
["extra", "extra"],
|
|
65
|
+
["complex", "complex"],
|
|
66
|
+
["x-axis", "x-axis"],
|
|
67
|
+
["5xword", "5xword"],
|
|
68
|
+
["10xbox", "10xbox"],
|
|
69
|
+
["2xLarge", "2xLarge"],
|
|
70
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
71
|
+
expect(multiplication(input)).toBe(expected)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("handles separator characters", () => {
|
|
75
|
+
const sep = "\uE000"
|
|
76
|
+
expect(multiplication(`5${sep}x${sep}5`, { separator: sep })).toBe(`5${sep}${UNICODE_SYMBOLS.MULTIPLICATION}${sep}5`)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe("marker robustness", () => {
|
|
80
|
+
// Tests that separators don't create false word boundaries
|
|
81
|
+
// e.g., "5x\uE000tra" should NOT match because the actual text is "5xtra"
|
|
82
|
+
const sep = DEFAULT_SEPARATOR
|
|
83
|
+
|
|
84
|
+
it.each([
|
|
85
|
+
// [description, input, expected]
|
|
86
|
+
["false boundary before word char", `5x${sep}tra`, `5x${sep}tra`], // "5xtra" - should NOT convert
|
|
87
|
+
["valid boundary before space", `5x${sep} done`, `5${UNICODE_SYMBOLS.MULTIPLICATION}${sep} done`], // should convert
|
|
88
|
+
["valid boundary before punctuation", `5x${sep}.`, `5${UNICODE_SYMBOLS.MULTIPLICATION}${sep}.`], // should convert
|
|
89
|
+
])("handles %s", (_desc, input, expected) => {
|
|
90
|
+
expect(multiplication(input, { separator: sep })).toBe(expected)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe("mathSymbols", () => {
|
|
96
|
+
it.each([
|
|
97
|
+
["x != y", `x ${UNICODE_SYMBOLS.NOT_EQUAL} y`],
|
|
98
|
+
["a!=b", `a${UNICODE_SYMBOLS.NOT_EQUAL}b`],
|
|
99
|
+
["1 != 2", `1 ${UNICODE_SYMBOLS.NOT_EQUAL} 2`],
|
|
100
|
+
["+-5", `${UNICODE_SYMBOLS.PLUS_MINUS}5`],
|
|
101
|
+
["+/-5", `${UNICODE_SYMBOLS.PLUS_MINUS}5`],
|
|
102
|
+
["The answer is +- 5%", `The answer is ${UNICODE_SYMBOLS.PLUS_MINUS} 5%`],
|
|
103
|
+
["tolerance: +/-0.5", `tolerance: ${UNICODE_SYMBOLS.PLUS_MINUS}0.5`],
|
|
104
|
+
["a <= b", `a ${UNICODE_SYMBOLS.LESS_EQUAL} b`],
|
|
105
|
+
["x >= y", `x ${UNICODE_SYMBOLS.GREATER_EQUAL} y`],
|
|
106
|
+
["~= 5", `${UNICODE_SYMBOLS.APPROXIMATE} 5`],
|
|
107
|
+
["=~ 5", `${UNICODE_SYMBOLS.APPROXIMATE} 5`],
|
|
108
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
109
|
+
expect(mathSymbols(input)).toBe(expected)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe("legalSymbols", () => {
|
|
114
|
+
it.each([
|
|
115
|
+
["Copyright (c) 2024", `Copyright ${UNICODE_SYMBOLS.COPYRIGHT} 2024`],
|
|
116
|
+
["(C) Acme Inc", `${UNICODE_SYMBOLS.COPYRIGHT} Acme Inc`],
|
|
117
|
+
["Brand(r)", `Brand${UNICODE_SYMBOLS.REGISTERED}`],
|
|
118
|
+
["Product (R)", `Product ${UNICODE_SYMBOLS.REGISTERED}`],
|
|
119
|
+
["Name(tm)", `Name${UNICODE_SYMBOLS.TRADEMARK}`],
|
|
120
|
+
["Name (TM)", `Name ${UNICODE_SYMBOLS.TRADEMARK}`],
|
|
121
|
+
["(c) 2024 (r) Brand(tm)", `${UNICODE_SYMBOLS.COPYRIGHT} 2024 ${UNICODE_SYMBOLS.REGISTERED} Brand${UNICODE_SYMBOLS.TRADEMARK}`],
|
|
122
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
123
|
+
expect(legalSymbols(input)).toBe(expected)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe("arrows", () => {
|
|
128
|
+
it.each([
|
|
129
|
+
["A -> B", `A ${UNICODE_SYMBOLS.ARROW_RIGHT} B`],
|
|
130
|
+
["A <- B", `A ${UNICODE_SYMBOLS.ARROW_LEFT} B`],
|
|
131
|
+
["A <-> B", `A ${UNICODE_SYMBOLS.ARROW_LEFT_RIGHT} B`],
|
|
132
|
+
["A --> B", `A ${UNICODE_SYMBOLS.ARROW_RIGHT} B`],
|
|
133
|
+
["A <-- B", `A ${UNICODE_SYMBOLS.ARROW_LEFT} B`],
|
|
134
|
+
["A <--> B", `A ${UNICODE_SYMBOLS.ARROW_LEFT_RIGHT} B`],
|
|
135
|
+
["start -> middle -> end", `start ${UNICODE_SYMBOLS.ARROW_RIGHT} middle ${UNICODE_SYMBOLS.ARROW_RIGHT} end`],
|
|
136
|
+
// Pointer-style arrows preserved (no spaces)
|
|
137
|
+
["function->call", "function->call"],
|
|
138
|
+
["array[0]->value", "array[0]->value"],
|
|
139
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
140
|
+
expect(arrows(input)).toBe(expected)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe("degrees", () => {
|
|
145
|
+
it.each([
|
|
146
|
+
["20 C", `20 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
147
|
+
["20C", `20 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
148
|
+
["68 F", `68 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
149
|
+
["68F", `68 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
150
|
+
["Water boils at 100 C", `Water boils at 100 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
151
|
+
["Room temperature: 72F", `Room temperature: 72 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
152
|
+
// Test degrees before punctuation (word boundary cases)
|
|
153
|
+
["20C.", `20 ${UNICODE_SYMBOLS.DEGREE}C.`],
|
|
154
|
+
["68F!", `68 ${UNICODE_SYMBOLS.DEGREE}F!`],
|
|
155
|
+
["100C,", `100 ${UNICODE_SYMBOLS.DEGREE}C,`],
|
|
156
|
+
["20 km", "20 km"],
|
|
157
|
+
["Section C", "Section C"],
|
|
158
|
+
["100 C", `100 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
159
|
+
["212F", `212 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
160
|
+
["-40 C", `-40 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
161
|
+
// Case-sensitive: lowercase c/f should NOT be converted
|
|
162
|
+
["20 c", "20 c"],
|
|
163
|
+
["68 f", "68 f"],
|
|
164
|
+
["20c", "20c"],
|
|
165
|
+
["68f", "68f"],
|
|
166
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
167
|
+
expect(degrees(input)).toBe(expected)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it("handles separator characters", () => {
|
|
171
|
+
const sep = "\uE000"
|
|
172
|
+
expect(degrees(`20${sep}C`, { separator: sep })).toBe(
|
|
173
|
+
`20${sep} ${UNICODE_SYMBOLS.DEGREE}C`
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe("marker robustness", () => {
|
|
178
|
+
// Tests that separators don't create false word boundaries
|
|
179
|
+
const sep = DEFAULT_SEPARATOR
|
|
180
|
+
|
|
181
|
+
it.each([
|
|
182
|
+
// [description, input, expected]
|
|
183
|
+
["false boundary before word char", `20C${sep}elsius`, `20C${sep}elsius`], // "20Celsius" - should NOT convert
|
|
184
|
+
["valid boundary before space", `20C${sep} today`, `20 ${UNICODE_SYMBOLS.DEGREE}C${sep} today`], // should convert
|
|
185
|
+
["valid boundary before punctuation", `68F${sep}.`, `68 ${UNICODE_SYMBOLS.DEGREE}F${sep}.`], // should convert
|
|
186
|
+
])("handles %s", (_desc, input, expected) => {
|
|
187
|
+
expect(degrees(input, { separator: sep })).toBe(expected)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe("primeMarks", () => {
|
|
193
|
+
it.each([
|
|
194
|
+
['5\'10"', `5${UNICODE_SYMBOLS.PRIME}10${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
195
|
+
['He is 6\'2" tall', `He is 6${UNICODE_SYMBOLS.PRIME}2${UNICODE_SYMBOLS.DOUBLE_PRIME} tall`],
|
|
196
|
+
["The board is 8' long", `The board is 8${UNICODE_SYMBOLS.PRIME} long`],
|
|
197
|
+
['12"', `12${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
198
|
+
["Location: 45° 30' 15\"", `Location: 45° 30${UNICODE_SYMBOLS.PRIME} 15${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
199
|
+
["don't", "don't"],
|
|
200
|
+
["it's", "it's"],
|
|
201
|
+
["'Twas the night", "'Twas the night"],
|
|
202
|
+
["'hello'", "'hello'"],
|
|
203
|
+
['"test"', '"test"'],
|
|
204
|
+
['The ceiling is 9\'6" high', `The ceiling is 9${UNICODE_SYMBOLS.PRIME}6${UNICODE_SYMBOLS.DOUBLE_PRIME} high`],
|
|
205
|
+
["40° 44' 54\" N", `40° 44${UNICODE_SYMBOLS.PRIME} 54${UNICODE_SYMBOLS.DOUBLE_PRIME} N`],
|
|
206
|
+
['The board is 12" wide', `The board is 12${UNICODE_SYMBOLS.DOUBLE_PRIME} wide`],
|
|
207
|
+
['12" long', `12${UNICODE_SYMBOLS.DOUBLE_PRIME} long`],
|
|
208
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
209
|
+
expect(primeMarks(input)).toBe(expected)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Quote balancing tests: simple (\d)['"]) patterns would incorrectly convert these
|
|
213
|
+
it.each([
|
|
214
|
+
// Double quote balancing
|
|
215
|
+
['"Term 1".', '"Term 1".'],
|
|
216
|
+
['"Number 5"', '"Number 5"'],
|
|
217
|
+
['"Item 3", "Item 4"', '"Item 3", "Item 4"'],
|
|
218
|
+
['She said "Chapter 5" was good', 'She said "Chapter 5" was good'],
|
|
219
|
+
['The file "test_v2" exists', 'The file "test_v2" exists'],
|
|
220
|
+
['"Room 101" is famous', '"Room 101" is famous'],
|
|
221
|
+
// Single quote balancing
|
|
222
|
+
["'Term 1'", "'Term 1'"],
|
|
223
|
+
["'Item 3'", "'Item 3'"],
|
|
224
|
+
["She said 'Chapter 5' was good", "She said 'Chapter 5' was good"],
|
|
225
|
+
])('preserves closing quotes via quote balancing: "%s"', (input, expected) => {
|
|
226
|
+
expect(primeMarks(input)).toBe(expected)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it("handles separator characters", () => {
|
|
230
|
+
const sep = "\uE000"
|
|
231
|
+
expect(primeMarks(`5${sep}'${sep}10${sep}"`, { separator: sep })).toBe(
|
|
232
|
+
`5${sep}${UNICODE_SYMBOLS.PRIME}${sep}10${sep}${UNICODE_SYMBOLS.DOUBLE_PRIME}`
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe("fractions", () => {
|
|
238
|
+
it.each([
|
|
239
|
+
["1/2", UNICODE_SYMBOLS.FRACTION_1_2],
|
|
240
|
+
["1/4", UNICODE_SYMBOLS.FRACTION_1_4],
|
|
241
|
+
["3/4", UNICODE_SYMBOLS.FRACTION_3_4],
|
|
242
|
+
["1/3", UNICODE_SYMBOLS.FRACTION_1_3],
|
|
243
|
+
["2/3", UNICODE_SYMBOLS.FRACTION_2_3],
|
|
244
|
+
["Add 1/2 cup", `Add ${UNICODE_SYMBOLS.FRACTION_1_2} cup`],
|
|
245
|
+
["About 3/4 done", `About ${UNICODE_SYMBOLS.FRACTION_3_4} done`],
|
|
246
|
+
["1/8 teaspoon", `${UNICODE_SYMBOLS.FRACTION_1_8} teaspoon`],
|
|
247
|
+
// Test fractions before punctuation
|
|
248
|
+
["1/2.", `${UNICODE_SYMBOLS.FRACTION_1_2}.`],
|
|
249
|
+
["3/4!", `${UNICODE_SYMBOLS.FRACTION_3_4}!`],
|
|
250
|
+
["1/4,", `${UNICODE_SYMBOLS.FRACTION_1_4},`],
|
|
251
|
+
["21/4", "21/4"],
|
|
252
|
+
["page 1/25", "page 1/25"],
|
|
253
|
+
["1/7", "1/7"],
|
|
254
|
+
["5/9", "5/9"],
|
|
255
|
+
// Edge cases: fractions in paths/URLs should not match
|
|
256
|
+
["a/1/4", "a/1/4"],
|
|
257
|
+
["1/4/b", "1/4/b"],
|
|
258
|
+
["x/1/2", "x/1/2"],
|
|
259
|
+
// Edge cases: fractions adjacent to decimals
|
|
260
|
+
["3.1/4", "3.1/4"],
|
|
261
|
+
["1/4.5", "1/4.5"],
|
|
262
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
263
|
+
expect(fractions(input)).toBe(expected)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it("handles separator characters", () => {
|
|
267
|
+
const sep = "\uE000"
|
|
268
|
+
expect(fractions(`1${sep}/${sep}2`, { separator: sep })).toBe(
|
|
269
|
+
`${sep}${UNICODE_SYMBOLS.FRACTION_1_2}${sep}`
|
|
270
|
+
)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe("collapseSpaces", () => {
|
|
275
|
+
const { NBSP } = UNICODE_SYMBOLS
|
|
276
|
+
|
|
277
|
+
it.each([
|
|
278
|
+
// Multiple regular spaces → single space
|
|
279
|
+
["hello world", "hello world"],
|
|
280
|
+
["a b", "a b"],
|
|
281
|
+
["x y", "x y"],
|
|
282
|
+
// Multiple nbsp → single nbsp
|
|
283
|
+
[`foo${NBSP}${NBSP}bar`, `foo${NBSP}bar`],
|
|
284
|
+
[`a${NBSP}${NBSP}${NBSP}b`, `a${NBSP}b`],
|
|
285
|
+
// Mixed: any nbsp present → prefer nbsp (more likely intentional)
|
|
286
|
+
[`a ${NBSP}b`, `a${NBSP}b`],
|
|
287
|
+
[`x ${NBSP}y`, `x${NBSP}y`],
|
|
288
|
+
[`a${NBSP} b`, `a${NBSP}b`],
|
|
289
|
+
[`x${NBSP} y`, `x${NBSP}y`],
|
|
290
|
+
[`a ${NBSP} b`, `a${NBSP}b`],
|
|
291
|
+
[`x${NBSP} ${NBSP}y`, `x${NBSP}y`],
|
|
292
|
+
// Single spaces unchanged
|
|
293
|
+
["hello world", "hello world"],
|
|
294
|
+
[`foo${NBSP}bar`, `foo${NBSP}bar`],
|
|
295
|
+
// Multiple groups of spaces
|
|
296
|
+
["a b c", "a b c"],
|
|
297
|
+
[`x${NBSP}${NBSP}y z`, `x${NBSP}y z`],
|
|
298
|
+
// Edge cases
|
|
299
|
+
["", ""],
|
|
300
|
+
[" ", " "],
|
|
301
|
+
[`${NBSP}${NBSP}`, NBSP],
|
|
302
|
+
["no spaces", "no spaces"],
|
|
303
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
304
|
+
expect(collapseSpaces(input)).toBe(expected)
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe("superscriptOrdinal", () => {
|
|
309
|
+
it.each([
|
|
310
|
+
// Basic ordinals
|
|
311
|
+
["1st", `1${UNICODE_SYMBOLS.SUPERSCRIPT_ST}`],
|
|
312
|
+
["2nd", `2${UNICODE_SYMBOLS.SUPERSCRIPT_ND}`],
|
|
313
|
+
["3rd", `3${UNICODE_SYMBOLS.SUPERSCRIPT_RD}`],
|
|
314
|
+
["4th", `4${UNICODE_SYMBOLS.SUPERSCRIPT_TH}`],
|
|
315
|
+
// Larger numbers
|
|
316
|
+
["21st", `21${UNICODE_SYMBOLS.SUPERSCRIPT_ST}`],
|
|
317
|
+
["22nd", `22${UNICODE_SYMBOLS.SUPERSCRIPT_ND}`],
|
|
318
|
+
["23rd", `23${UNICODE_SYMBOLS.SUPERSCRIPT_RD}`],
|
|
319
|
+
["30th", `30${UNICODE_SYMBOLS.SUPERSCRIPT_TH}`],
|
|
320
|
+
["100th", `100${UNICODE_SYMBOLS.SUPERSCRIPT_TH}`],
|
|
321
|
+
// In sentences
|
|
322
|
+
["The 1st place winner", `The 1${UNICODE_SYMBOLS.SUPERSCRIPT_ST} place winner`],
|
|
323
|
+
["Born on the 30th of June", `Born on the 30${UNICODE_SYMBOLS.SUPERSCRIPT_TH} of June`],
|
|
324
|
+
["The 21st century", `The 21${UNICODE_SYMBOLS.SUPERSCRIPT_ST} century`],
|
|
325
|
+
// Before punctuation
|
|
326
|
+
["1st.", `1${UNICODE_SYMBOLS.SUPERSCRIPT_ST}.`],
|
|
327
|
+
["2nd,", `2${UNICODE_SYMBOLS.SUPERSCRIPT_ND},`],
|
|
328
|
+
["3rd!", `3${UNICODE_SYMBOLS.SUPERSCRIPT_RD}!`],
|
|
329
|
+
// Case insensitive
|
|
330
|
+
["1ST", `1${UNICODE_SYMBOLS.SUPERSCRIPT_ST}`],
|
|
331
|
+
["2ND", `2${UNICODE_SYMBOLS.SUPERSCRIPT_ND}`],
|
|
332
|
+
["3RD", `3${UNICODE_SYMBOLS.SUPERSCRIPT_RD}`],
|
|
333
|
+
["4TH", `4${UNICODE_SYMBOLS.SUPERSCRIPT_TH}`],
|
|
334
|
+
// Should not match non-ordinal text
|
|
335
|
+
["first", "first"],
|
|
336
|
+
["stand", "stand"],
|
|
337
|
+
["thunder", "thunder"],
|
|
338
|
+
["rand", "rand"],
|
|
339
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
340
|
+
expect(superscriptOrdinal(input)).toBe(expected)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it("handles separator characters", () => {
|
|
344
|
+
const sep = "\uE000"
|
|
345
|
+
expect(superscriptOrdinal(`30${sep}th`, { separator: sep })).toBe(
|
|
346
|
+
`30${sep}${UNICODE_SYMBOLS.SUPERSCRIPT_TH}`
|
|
347
|
+
)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
describe("marker robustness", () => {
|
|
351
|
+
// Tests that separators don't create false word boundaries
|
|
352
|
+
const sep = DEFAULT_SEPARATOR
|
|
353
|
+
|
|
354
|
+
it.each([
|
|
355
|
+
// [description, input, expected]
|
|
356
|
+
["false boundary (1stly)", `1st${sep}ly`, `1st${sep}ly`], // "1stly" - should NOT convert
|
|
357
|
+
["false boundary (2ndary)", `2nd${sep}ary`, `2nd${sep}ary`], // "2ndary" - should NOT convert
|
|
358
|
+
["false boundary (3rdly)", `3rd${sep}ly`, `3rd${sep}ly`], // "3rdly" - should NOT convert
|
|
359
|
+
["valid boundary before space", `1st${sep} place`, `1${UNICODE_SYMBOLS.SUPERSCRIPT_ST}${sep} place`], // should convert
|
|
360
|
+
["valid boundary before punctuation", `2nd${sep}.`, `2${UNICODE_SYMBOLS.SUPERSCRIPT_ND}${sep}.`], // should convert
|
|
361
|
+
])("handles %s", (_desc, input, expected) => {
|
|
362
|
+
expect(superscriptOrdinal(input, { separator: sep })).toBe(expected)
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
describe("punctuationLigatures", () => {
|
|
368
|
+
it.each([
|
|
369
|
+
// Double/multiple question marks squashed to ligature
|
|
370
|
+
["What??", `What${UNICODE_SYMBOLS.DOUBLE_QUESTION}`],
|
|
371
|
+
["Really???", `Really${UNICODE_SYMBOLS.DOUBLE_QUESTION}`],
|
|
372
|
+
["Huh????", `Huh${UNICODE_SYMBOLS.DOUBLE_QUESTION}`],
|
|
373
|
+
["??", UNICODE_SYMBOLS.DOUBLE_QUESTION],
|
|
374
|
+
// Question + exclamation(s) → ⁈
|
|
375
|
+
["Really?!", `Really${UNICODE_SYMBOLS.QUESTION_EXCLAMATION}`],
|
|
376
|
+
["What?!!", `What${UNICODE_SYMBOLS.QUESTION_EXCLAMATION}`],
|
|
377
|
+
["?!", UNICODE_SYMBOLS.QUESTION_EXCLAMATION],
|
|
378
|
+
// Exclamation + question(s) → ⁉
|
|
379
|
+
["No way!?", `No way${UNICODE_SYMBOLS.EXCLAMATION_QUESTION}`],
|
|
380
|
+
["What!??", `What${UNICODE_SYMBOLS.EXCLAMATION_QUESTION}`],
|
|
381
|
+
["!?", UNICODE_SYMBOLS.EXCLAMATION_QUESTION],
|
|
382
|
+
// Double/multiple exclamation marks squashed to single
|
|
383
|
+
["Wow!!", "Wow!"],
|
|
384
|
+
["Amazing!!!", "Amazing!"],
|
|
385
|
+
["Yes!!!!", "Yes!"],
|
|
386
|
+
["!!", "!"],
|
|
387
|
+
// Single punctuation unchanged
|
|
388
|
+
["What?", "What?"],
|
|
389
|
+
["Wow!", "Wow!"],
|
|
390
|
+
// Text without punctuation unchanged
|
|
391
|
+
["Hello world", "Hello world"],
|
|
392
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
393
|
+
expect(punctuationLigatures(input)).toBe(expected)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it.each([
|
|
397
|
+
["??", `${UNICODE_SYMBOLS.DOUBLE_QUESTION}${DEFAULT_SEPARATOR}`],
|
|
398
|
+
["?!", `${UNICODE_SYMBOLS.QUESTION_EXCLAMATION}${DEFAULT_SEPARATOR}`],
|
|
399
|
+
["!?", `${UNICODE_SYMBOLS.EXCLAMATION_QUESTION}${DEFAULT_SEPARATOR}`],
|
|
400
|
+
["!!", `!${DEFAULT_SEPARATOR}`],
|
|
401
|
+
])("preserves separator in %s", (marks, expected) => {
|
|
402
|
+
const input = marks[0] + DEFAULT_SEPARATOR + marks[1]
|
|
403
|
+
expect(punctuationLigatures(input, { separator: DEFAULT_SEPARATOR })).toBe(expected)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it("handles multiple ligatures in same text", () => {
|
|
407
|
+
const input = "What?? Really?! No way!? Wow!!"
|
|
408
|
+
const expected = `What${UNICODE_SYMBOLS.DOUBLE_QUESTION} Really${UNICODE_SYMBOLS.QUESTION_EXCLAMATION} No way${UNICODE_SYMBOLS.EXCLAMATION_QUESTION} Wow!`
|
|
409
|
+
expect(punctuationLigatures(input)).toBe(expected)
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
describe("hexadecimal preservation", () => {
|
|
414
|
+
it.each([
|
|
415
|
+
"0x5F3759DF",
|
|
416
|
+
"0xff",
|
|
417
|
+
"0X1A2B",
|
|
418
|
+
"The magic number is 0x5F3759DF",
|
|
419
|
+
"test 0x",
|
|
420
|
+
"value: 0X",
|
|
421
|
+
])('preserves "%s"', (input) => {
|
|
422
|
+
expect(multiplication(input)).toBe(input)
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// Tests derived from competitor libraries and typography guidelines
|
|
427
|
+
describe("competitor-derived edge cases", () => {
|
|
428
|
+
// From retext-smartypants: ellipsis variations
|
|
429
|
+
describe("ellipsis patterns", () => {
|
|
430
|
+
it.each([
|
|
431
|
+
// Ellipsis at boundaries
|
|
432
|
+
["...start", `${UNICODE_SYMBOLS.ELLIPSIS} start`],
|
|
433
|
+
["end...", `end${UNICODE_SYMBOLS.ELLIPSIS}`],
|
|
434
|
+
// Four dots = ellipsis + period
|
|
435
|
+
["text....", `text${UNICODE_SYMBOLS.ELLIPSIS}.`],
|
|
436
|
+
// Six dots = two ellipses
|
|
437
|
+
["text......", `text${UNICODE_SYMBOLS.ELLIPSIS}${UNICODE_SYMBOLS.ELLIPSIS}`],
|
|
438
|
+
])('handles ellipsis pattern: "%s"', (input, expected) => {
|
|
439
|
+
expect(ellipsis(input)).toBe(expected)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it("converts spaced periods to ellipsis", () => {
|
|
443
|
+
expect(ellipsis("text. . . more")).toBe(`text${UNICODE_SYMBOLS.ELLIPSIS} more`)
|
|
444
|
+
expect(ellipsis(". . .")).toBe(UNICODE_SYMBOLS.ELLIPSIS)
|
|
445
|
+
// Also works with non-breaking spaces
|
|
446
|
+
expect(ellipsis(`.${UNICODE_SYMBOLS.NBSP}.${UNICODE_SYMBOLS.NBSP}.`)).toBe(UNICODE_SYMBOLS.ELLIPSIS)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it("preserves non-ellipsis spaced periods", () => {
|
|
450
|
+
// Only exactly three spaced dots are converted
|
|
451
|
+
expect(ellipsis("a . b . c")).toBe("a . b . c")
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// From smartquotes.js: prime marks for height
|
|
456
|
+
describe("height measurements", () => {
|
|
457
|
+
it.each([
|
|
458
|
+
['6\'2"', `6${UNICODE_SYMBOLS.PRIME}2${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
459
|
+
['5\'10"', `5${UNICODE_SYMBOLS.PRIME}10${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
460
|
+
["8' boards", `8${UNICODE_SYMBOLS.PRIME} boards`],
|
|
461
|
+
['cut 12" wide', `cut 12${UNICODE_SYMBOLS.DOUBLE_PRIME} wide`],
|
|
462
|
+
])('converts measurement: "%s"', (input, expected) => {
|
|
463
|
+
expect(primeMarks(input)).toBe(expected)
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
// From Standard Ebooks: coordinates
|
|
468
|
+
describe("coordinate notation", () => {
|
|
469
|
+
it.each([
|
|
470
|
+
["40° 44' 54\" N", `40° 44${UNICODE_SYMBOLS.PRIME} 54${UNICODE_SYMBOLS.DOUBLE_PRIME} N`],
|
|
471
|
+
["74° 0' 21\" W", `74° 0${UNICODE_SYMBOLS.PRIME} 21${UNICODE_SYMBOLS.DOUBLE_PRIME} W`],
|
|
472
|
+
["51° 30' N", `51° 30${UNICODE_SYMBOLS.PRIME} N`],
|
|
473
|
+
])('converts coordinates: "%s"', (input, expected) => {
|
|
474
|
+
expect(primeMarks(input)).toBe(expected)
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
// From typograf: math with spaces
|
|
479
|
+
describe("math symbol spacing", () => {
|
|
480
|
+
it.each([
|
|
481
|
+
["a != b", `a ${UNICODE_SYMBOLS.NOT_EQUAL} b`],
|
|
482
|
+
["x!=y", `x${UNICODE_SYMBOLS.NOT_EQUAL}y`],
|
|
483
|
+
["5 <= 10", `5 ${UNICODE_SYMBOLS.LESS_EQUAL} 10`],
|
|
484
|
+
["10 >= 5", `10 ${UNICODE_SYMBOLS.GREATER_EQUAL} 5`],
|
|
485
|
+
])('handles math symbols: "%s"', (input, expected) => {
|
|
486
|
+
expect(mathSymbols(input)).toBe(expected)
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
// From tipograph: arrow preservation in code
|
|
491
|
+
describe("arrow context sensitivity", () => {
|
|
492
|
+
it.each([
|
|
493
|
+
// Should convert (spaced)
|
|
494
|
+
["input -> output", `input ${UNICODE_SYMBOLS.ARROW_RIGHT} output`],
|
|
495
|
+
["a <-> b", `a ${UNICODE_SYMBOLS.ARROW_LEFT_RIGHT} b`],
|
|
496
|
+
// Should NOT convert (no spaces - code-like)
|
|
497
|
+
["obj->method", "obj->method"],
|
|
498
|
+
["ptr->value", "ptr->value"],
|
|
499
|
+
["this->that", "this->that"],
|
|
500
|
+
])('handles arrows contextually: "%s"', (input, expected) => {
|
|
501
|
+
expect(arrows(input)).toBe(expected)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
describe("complex real-world patterns", () => {
|
|
507
|
+
describe("mixed measurements", () => {
|
|
508
|
+
it.each([
|
|
509
|
+
// Simple foot measurements
|
|
510
|
+
["The board is 8' long", `The board is 8${UNICODE_SYMBOLS.PRIME} long`],
|
|
511
|
+
['12" wide', `12${UNICODE_SYMBOLS.DOUBLE_PRIME} wide`],
|
|
512
|
+
// Height notation
|
|
513
|
+
['6\'2"', `6${UNICODE_SYMBOLS.PRIME}2${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
514
|
+
])('handles measurement: "%s"', (input, expected) => {
|
|
515
|
+
expect(primeMarks(input)).toBe(expected)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
describe("temperatures in context", () => {
|
|
521
|
+
it.each([
|
|
522
|
+
["Set oven to 350 F", `Set oven to 350 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
523
|
+
["Water freezes at 0 C", `Water freezes at 0 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
524
|
+
["Room temperature: 72 F", `Room temperature: 72 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
525
|
+
])('handles temperature: "%s"', (input, expected) => {
|
|
526
|
+
expect(degrees(input)).toBe(expected)
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
describe("legal text", () => {
|
|
531
|
+
it.each([
|
|
532
|
+
["(c) 2024 Company", `${UNICODE_SYMBOLS.COPYRIGHT} 2024 Company`],
|
|
533
|
+
["Brand(r) is a registered trademark", `Brand${UNICODE_SYMBOLS.REGISTERED} is a registered trademark`],
|
|
534
|
+
["Product(tm) - buy now!", `Product${UNICODE_SYMBOLS.TRADEMARK} - buy now!`],
|
|
535
|
+
])('handles legal symbol: "%s"', (input, expected) => {
|
|
536
|
+
expect(legalSymbols(input)).toBe(expected)
|
|
537
|
+
})
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
describe("fractions in recipes", () => {
|
|
541
|
+
it.each([
|
|
542
|
+
["Add 1/2 cup flour", `Add ${UNICODE_SYMBOLS.FRACTION_1_2} cup flour`],
|
|
543
|
+
["1/4 teaspoon salt", `${UNICODE_SYMBOLS.FRACTION_1_4} teaspoon salt`],
|
|
544
|
+
["about 3/4 done", `about ${UNICODE_SYMBOLS.FRACTION_3_4} done`],
|
|
545
|
+
["1/3 of the mixture", `${UNICODE_SYMBOLS.FRACTION_1_3} of the mixture`],
|
|
546
|
+
])('handles fraction: "%s"', (input, expected) => {
|
|
547
|
+
expect(fractions(input)).toBe(expected)
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
describe("idempotency", () => {
|
|
553
|
+
it.each([
|
|
554
|
+
UNICODE_SYMBOLS.ELLIPSIS,
|
|
555
|
+
`5${UNICODE_SYMBOLS.MULTIPLICATION}5`,
|
|
556
|
+
`a ${UNICODE_SYMBOLS.NOT_EQUAL} b`,
|
|
557
|
+
UNICODE_SYMBOLS.COPYRIGHT,
|
|
558
|
+
`5${UNICODE_SYMBOLS.PRIME}10${UNICODE_SYMBOLS.DOUBLE_PRIME}`,
|
|
559
|
+
`20 ${UNICODE_SYMBOLS.DEGREE}C`,
|
|
560
|
+
UNICODE_SYMBOLS.FRACTION_1_2,
|
|
561
|
+
`1${UNICODE_SYMBOLS.SUPERSCRIPT_ST}`,
|
|
562
|
+
])('is idempotent for: "%s"', (input) => {
|
|
563
|
+
expect(ellipsis(input)).toBe(input)
|
|
564
|
+
expect(multiplication(input)).toBe(input)
|
|
565
|
+
})
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
describe("symbolTransform", () => {
|
|
569
|
+
it.each([
|
|
570
|
+
["Wait... 5x5 != 20 (c) 2024", `Wait${UNICODE_SYMBOLS.ELLIPSIS} 5${UNICODE_SYMBOLS.MULTIPLICATION}5 ${UNICODE_SYMBOLS.NOT_EQUAL} 20 ${UNICODE_SYMBOLS.COPYRIGHT} 2024`],
|
|
571
|
+
["-2 x 3", `-2 ${UNICODE_SYMBOLS.MULTIPLICATION} 3`],
|
|
572
|
+
["5 x 5", `5 ${UNICODE_SYMBOLS.MULTIPLICATION} 5`],
|
|
573
|
+
])('converts "%s" to "%s"', (input, expected) => {
|
|
574
|
+
expect(symbolTransform(input)).toBe(expected)
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
it("handles complex text with multiple symbols", () => {
|
|
578
|
+
const input = "Product(tm) v2.0 - Size: 10x20 cm, tolerance +- 5%"
|
|
579
|
+
const expected = `Product${UNICODE_SYMBOLS.TRADEMARK} v2.0 - Size: 10${UNICODE_SYMBOLS.MULTIPLICATION}20 cm, tolerance ${UNICODE_SYMBOLS.PLUS_MINUS} 5%`
|
|
580
|
+
expect(symbolTransform(input)).toEqual(expected)
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it("respects separator option", () => {
|
|
584
|
+
const sep = "\uE000"
|
|
585
|
+
const input = `5${sep}x${sep}5`
|
|
586
|
+
expect(symbolTransform(input, { separator: sep })).toEqual(`5${sep}${UNICODE_SYMBOLS.MULTIPLICATION}${sep}5`)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it("includes arrows by default", () => {
|
|
590
|
+
expect(symbolTransform("A -> B")).toBe(`A ${UNICODE_SYMBOLS.ARROW_RIGHT} B`)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it("can disable arrows with includeArrows: false", () => {
|
|
594
|
+
expect(symbolTransform("A -> B", { includeArrows: false })).toBe("A -> B")
|
|
595
|
+
})
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
describe("multiplication edge cases", () => {
|
|
599
|
+
it.each([
|
|
600
|
+
["1000000x2000000", `1000000${UNICODE_SYMBOLS.MULTIPLICATION}2000000`],
|
|
601
|
+
["01x02", `01${UNICODE_SYMBOLS.MULTIPLICATION}02`],
|
|
602
|
+
["2x", `2${UNICODE_SYMBOLS.MULTIPLICATION}`],
|
|
603
|
+
])('handles multiplication edge: "%s"', (input, expected) => {
|
|
604
|
+
expect(multiplication(input)).toBe(expected)
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
describe("multiplication - preserve words containing x", () => {
|
|
609
|
+
it.each([
|
|
610
|
+
["extra", "extra"],
|
|
611
|
+
["index", "index"],
|
|
612
|
+
["2xtra", "2xtra"],
|
|
613
|
+
["hex", "hex"],
|
|
614
|
+
["next", "next"],
|
|
615
|
+
["text", "text"],
|
|
616
|
+
["approximately", "approximately"],
|
|
617
|
+
])('does NOT transform word: "%s"', (input, expected) => {
|
|
618
|
+
expect(multiplication(input)).toBe(expected)
|
|
619
|
+
})
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
describe("ellipsis edge cases", () => {
|
|
623
|
+
it.each([
|
|
624
|
+
["One... Two... Three...", `One${UNICODE_SYMBOLS.ELLIPSIS} Two${UNICODE_SYMBOLS.ELLIPSIS} Three${UNICODE_SYMBOLS.ELLIPSIS}`],
|
|
625
|
+
["...5 items", `${UNICODE_SYMBOLS.ELLIPSIS} 5 items`],
|
|
626
|
+
["End of sentence....", `End of sentence${UNICODE_SYMBOLS.ELLIPSIS}.`],
|
|
627
|
+
['"Wait..."', `"Wait${UNICODE_SYMBOLS.ELLIPSIS}"`],
|
|
628
|
+
])('handles ellipsis edge: "%s"', (input, expected) => {
|
|
629
|
+
expect(ellipsis(input)).toBe(expected)
|
|
630
|
+
})
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
describe("ellipsis - preserve abbreviations", () => {
|
|
634
|
+
it.each([
|
|
635
|
+
["e.g.", "e.g."],
|
|
636
|
+
["i.e.", "i.e."],
|
|
637
|
+
["a.m.", "a.m."],
|
|
638
|
+
["p.m.", "p.m."],
|
|
639
|
+
["Dr.", "Dr."],
|
|
640
|
+
["Mr.", "Mr."],
|
|
641
|
+
["etc.", "etc."],
|
|
642
|
+
])('preserves abbreviation: "%s"', (input, expected) => {
|
|
643
|
+
expect(ellipsis(input)).toBe(expected)
|
|
644
|
+
})
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
describe("prime marks edge cases", () => {
|
|
648
|
+
it.each([
|
|
649
|
+
['100\'50"', `100${UNICODE_SYMBOLS.PRIME}50${UNICODE_SYMBOLS.DOUBLE_PRIME}`],
|
|
650
|
+
["5' boards", `5${UNICODE_SYMBOLS.PRIME} boards`],
|
|
651
|
+
['12" pipe', `12${UNICODE_SYMBOLS.DOUBLE_PRIME} pipe`],
|
|
652
|
+
["5', 10'", `5${UNICODE_SYMBOLS.PRIME}, 10'`],
|
|
653
|
+
])('handles prime mark edge: "%s"', (input, expected) => {
|
|
654
|
+
expect(primeMarks(input)).toBe(expected)
|
|
655
|
+
})
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
describe("degrees edge temperatures", () => {
|
|
659
|
+
it.each([
|
|
660
|
+
["-40 C", `-40 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
661
|
+
["-40 F", `-40 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
662
|
+
["0 C", `0 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
663
|
+
["0 F", `0 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
664
|
+
["1000 C", `1000 ${UNICODE_SYMBOLS.DEGREE}C`],
|
|
665
|
+
["1000 F", `1000 ${UNICODE_SYMBOLS.DEGREE}F`],
|
|
666
|
+
])('handles temperature edge: "%s"', (input, expected) => {
|
|
667
|
+
expect(degrees(input)).toBe(expected)
|
|
668
|
+
})
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
describe("arrow edge cases", () => {
|
|
672
|
+
it.each([
|
|
673
|
+
["A -> B -> C", `A ${UNICODE_SYMBOLS.ARROW_RIGHT} B ${UNICODE_SYMBOLS.ARROW_RIGHT} C`],
|
|
674
|
+
["A <- B <- C", `A ${UNICODE_SYMBOLS.ARROW_LEFT} B ${UNICODE_SYMBOLS.ARROW_LEFT} C`],
|
|
675
|
+
["A ---> B", `A ${UNICODE_SYMBOLS.ARROW_RIGHT} B`],
|
|
676
|
+
["A <--- B", `A ${UNICODE_SYMBOLS.ARROW_LEFT} B`],
|
|
677
|
+
["-> output", `${UNICODE_SYMBOLS.ARROW_RIGHT} output`],
|
|
678
|
+
["input ->", `input ${UNICODE_SYMBOLS.ARROW_RIGHT}`],
|
|
679
|
+
])('handles arrow edge: "%s"', (input, expected) => {
|
|
680
|
+
expect(arrows(input)).toBe(expected)
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
describe("fractions edge cases", () => {
|
|
685
|
+
it.each([
|
|
686
|
+
["1/2 and 1/4", `${UNICODE_SYMBOLS.FRACTION_1_2} and ${UNICODE_SYMBOLS.FRACTION_1_4}`],
|
|
687
|
+
["Add 1/2.", `Add ${UNICODE_SYMBOLS.FRACTION_1_2}.`],
|
|
688
|
+
["(1/2)", `(${UNICODE_SYMBOLS.FRACTION_1_2})`],
|
|
689
|
+
["5/7", "5/7"],
|
|
690
|
+
["11/12", "11/12"],
|
|
691
|
+
])('handles fraction edge: "%s"', (input, expected) => {
|
|
692
|
+
expect(fractions(input)).toBe(expected)
|
|
693
|
+
})
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
describe("chained multiplications", () => {
|
|
697
|
+
const M = UNICODE_SYMBOLS.MULTIPLICATION
|
|
698
|
+
it.each([
|
|
699
|
+
["5x5x5", `5${M}5${M}5`, "tight x"],
|
|
700
|
+
["5 x 5 x 5", `5 ${M} 5 ${M} 5`, "spaced x"],
|
|
701
|
+
["5*5*5", `5${M}5${M}5`, "tight *"],
|
|
702
|
+
["5 * 5 * 5", `5 ${M} 5 ${M} 5`, "spaced *"],
|
|
703
|
+
["10x10x10", `10${M}10${M}10`, "multi-digit"],
|
|
704
|
+
])("converts %s → %s (%s)", (input, expected) => {
|
|
705
|
+
expect(multiplication(input)).toBe(expected)
|
|
706
|
+
})
|
|
707
|
+
})
|
|
708
|
+
|