punctilio 2.3.0 → 3.0.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 +1 -2
- package/dist/index.d.ts +1 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -14
- package/dist/index.js.map +1 -1
- package/dist/quotes.d.ts +11 -11
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +21 -7
- package/dist/quotes.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -26
- package/src/quotes.ts +22 -18
- package/src/tests/quotes.test.ts +46 -49
- package/src/tests/remark.test.ts +1 -1
package/README.md
CHANGED
|
@@ -137,7 +137,6 @@ transform(text, {
|
|
|
137
137
|
ligatures: false, // ??? → ⁇, ?! → ⁈, !? → ⁉, !!! → !
|
|
138
138
|
nbsp: true, // non-breaking spaces (after honorifics, between numbers and units, etc.)
|
|
139
139
|
checkIdempotency: true, // verify transform(transform(x)) === transform(x)
|
|
140
|
-
useModifierLetterApostrophe: false, // output distinct codepoints for apostrophes vs end-single-quotes
|
|
141
140
|
})
|
|
142
141
|
```
|
|
143
142
|
|
|
@@ -150,4 +149,4 @@ transform(text, {
|
|
|
150
149
|
- Spaced en-dashes between words (word – word)
|
|
151
150
|
- Setting either style to `none` skips the entire transform category: `punctuationStyle: 'none'` preserves straight quotes, apostrophes, and prime marks; `dashStyle: 'none'` preserves all hyphens, number ranges, date ranges, and minus signs.
|
|
152
151
|
- `punctilio` is idempotent by design: `transform(transform(text))` always equals `transform(text)`. If performance is critical, set `checkIdempotency: false` to skip the verification pass.
|
|
153
|
-
-
|
|
152
|
+
- Use `classifyApostrophes(text)` to distinguish apostrophes from closing single quotes. It returns text with apostrophes as U+02BC (MODIFIER LETTER APOSTROPHE) and closing quotes as U+2019 (RIGHT SINGLE QUOTATION MARK). Per the [Unicode Standard](https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-6/#G30602), `transform()` and `niceQuotes()` use U+2019 for both in their output.
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
|
-
export { niceQuotes, type QuoteOptions, type PunctuationStyle } from "./quotes.js";
|
|
10
|
+
export { niceQuotes, classifyApostrophes, type QuoteOptions, type PunctuationStyle } from "./quotes.js";
|
|
11
11
|
import type { PunctuationStyle } from "./quotes.js";
|
|
12
12
|
export { hyphenReplace, enDashNumberRange, enDashDateRange, minusReplace, months, numberRangeDisallowedPrefixes, type DashOptions, type DashStyle, } from "./dashes.js";
|
|
13
13
|
import type { DashStyle } from "./dashes.js";
|
|
@@ -105,17 +105,6 @@ export interface TransformOptions {
|
|
|
105
105
|
* Default: true
|
|
106
106
|
*/
|
|
107
107
|
checkIdempotency?: boolean;
|
|
108
|
-
/**
|
|
109
|
-
* Use modifier letter apostrophe (U+02BC) instead of right single quote
|
|
110
|
-
* (U+2019) for contractions and possessives. This distinguishes apostrophes
|
|
111
|
-
* from closing quotes in most cases, though some ambiguous patterns (e.g.,
|
|
112
|
-
* `dogs'` vs a closing single quote) cannot be resolved without semantic
|
|
113
|
-
* understanding. May also break downstream regex patterns that expect
|
|
114
|
-
* standard quote characters like /don\u2019t/ or /O\u2019Brien/.
|
|
115
|
-
*
|
|
116
|
-
* Default: false
|
|
117
|
-
*/
|
|
118
|
-
useModifierLetterApostrophe?: boolean;
|
|
119
108
|
}
|
|
120
109
|
export { assertSeparatorCountPreserved, countSeparators } from "./utils.js";
|
|
121
110
|
export { DEFAULT_SEPARATOR, UNICODE_SYMBOLS } from "./constants.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACvG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,MAAM,EACN,6BAA6B,EAC7B,KAAK,WAAW,EAChB,KAAK,SAAS,GACf,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,YAAY,EACZ,MAAM,EACN,OAAO,EACP,SAAS,EACT,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,KAAK,aAAa,GACnB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,+BAA+B,EAC/B,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,EACnB,aAAa,EACb,KAAK,EACL,UAAU,EACV,uBAAuB,EACvB,KAAK,WAAW,GACjB,MAAM,WAAW,CAAA;AAElB,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IAExB;;;;;;;;;;;;;OAaG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IAEnC;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;IAErB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IAEnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IAEnB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAE3B;AASD,OAAO,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AACnE,eAAO,MAAM,0BAA0B,KAA6C,CAAA;AAiDpF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAmE9E"}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
|
-
export { niceQuotes } from "./quotes.js";
|
|
10
|
+
export { niceQuotes, classifyApostrophes } from "./quotes.js";
|
|
11
11
|
export { hyphenReplace, enDashNumberRange, enDashDateRange, minusReplace, months, numberRangeDisallowedPrefixes, } from "./dashes.js";
|
|
12
12
|
export { ellipsis, multiplication, mathSymbols, legalSymbols, arrows, degrees, fractions, primeMarks, collapseSpaces, superscriptOrdinal, punctuationLigatures, symbolTransform, } from "./symbols.js";
|
|
13
13
|
export { nbspAfterShortWords, nbspBetweenNumberAndUnit, nbspBeforeLastWord, nbspAfterReferenceAbbreviations, nbspAfterSectionSymbols, nbspAfterHonorifics, nbspAfterCopyrightSymbols, nbspBetweenInitials, nbspTransform, UNITS, HONORIFICS, REFERENCE_ABBREVIATIONS, } from "./nbsp.js";
|
|
@@ -65,7 +65,6 @@ const defaultOpts = {
|
|
|
65
65
|
checkIdempotency: true,
|
|
66
66
|
punctuationStyle: "american",
|
|
67
67
|
dashStyle: "american",
|
|
68
|
-
useModifierLetterApostrophe: false,
|
|
69
68
|
};
|
|
70
69
|
export function transform(text, options = {}) {
|
|
71
70
|
const separator = options.separator ?? DEFAULT_SEPARATOR;
|
|
@@ -77,17 +76,12 @@ export function transform(text, options = {}) {
|
|
|
77
76
|
`Use a single-codepoint character like the default "\\uE000".`);
|
|
78
77
|
}
|
|
79
78
|
const original = text;
|
|
80
|
-
const { symbols, fractions, degrees, superscript, ligatures, nbsp, collapseSpaces, checkIdempotency,
|
|
79
|
+
const { symbols, fractions, degrees, superscript, ligatures, nbsp, collapseSpaces, checkIdempotency, ...separatorOpts } = { ...defaultOpts, ...options };
|
|
81
80
|
text = hyphenReplace(text, separatorOpts);
|
|
82
81
|
if (separatorOpts.punctuationStyle !== "none") {
|
|
83
82
|
text = primeMarks(text, separatorOpts);
|
|
84
83
|
}
|
|
85
|
-
|
|
86
|
-
// because NBSP's PUNCTUATION_OR_QUOTE class includes RSQ but not MLA.
|
|
87
|
-
// Without this, apostrophes in "don't" would act as word boundaries
|
|
88
|
-
// during NBSP insertion. The final MLA→RSQ conversion at line 286
|
|
89
|
-
// runs after all transforms complete.
|
|
90
|
-
text = niceQuotes(text, { ...separatorOpts, useModifierLetterApostrophe: true });
|
|
84
|
+
text = niceQuotes(text, separatorOpts);
|
|
91
85
|
if (symbols) {
|
|
92
86
|
text = symbolTransform(text, separatorOpts);
|
|
93
87
|
}
|
|
@@ -110,11 +104,6 @@ export function transform(text, options = {}) {
|
|
|
110
104
|
text = nbspTransformFn(text, separatorOpts);
|
|
111
105
|
}
|
|
112
106
|
assertSeparatorCountPreserved(original, text, separator, "transform");
|
|
113
|
-
// Convert MLA→RSQ for regex-friendly output, after all transforms
|
|
114
|
-
// (especially NBSP which relies on MLA being letter-like).
|
|
115
|
-
if (!useModifierLetterApostrophe) {
|
|
116
|
-
text = text.replaceAll(UNICODE_SYMBOLS.MODIFIER_LETTER_APOSTROPHE, UNICODE_SYMBOLS.RIGHT_SINGLE_QUOTE);
|
|
117
|
-
}
|
|
118
107
|
if (checkIdempotency) {
|
|
119
108
|
const secondPass = transform(text, { ...options, checkIdempotency: false });
|
|
120
109
|
/* istanbul ignore if -- defensive check that should never trigger */
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAA4C,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/quotes.d.ts
CHANGED
|
@@ -7,18 +7,18 @@ export interface QuoteOptions {
|
|
|
7
7
|
separator?: string;
|
|
8
8
|
/** "american" (inside), "british" (outside), "none". Default: "american" */
|
|
9
9
|
punctuationStyle?: PunctuationStyle;
|
|
10
|
-
/**
|
|
11
|
-
* Use modifier letter apostrophe (U+02BC) instead of right single quote
|
|
12
|
-
* (U+2019) for contractions and possessives. This distinguishes apostrophes
|
|
13
|
-
* from closing quotes in most cases, though some ambiguous patterns (e.g.,
|
|
14
|
-
* `dogs'` vs a closing single quote) cannot be resolved without semantic
|
|
15
|
-
* understanding. May also break downstream regex patterns that expect
|
|
16
|
-
* standard quote characters like /don\u2019t/ or /O\u2019Brien/.
|
|
17
|
-
*
|
|
18
|
-
* Default: false
|
|
19
|
-
*/
|
|
20
|
-
useModifierLetterApostrophe?: boolean;
|
|
21
10
|
}
|
|
22
11
|
/** Convert straight quotes to smart quotes. */
|
|
23
12
|
export declare function niceQuotes(text: string, options?: QuoteOptions): string;
|
|
13
|
+
/**
|
|
14
|
+
* Classify apostrophes vs. closing single quotes.
|
|
15
|
+
*
|
|
16
|
+
* Returns the text with smart quotes applied, where apostrophes are
|
|
17
|
+
* U+02BC (MODIFIER LETTER APOSTROPHE) and closing single quotes are
|
|
18
|
+
* U+2019 (RIGHT SINGLE QUOTATION MARK).
|
|
19
|
+
*
|
|
20
|
+
* For display output, use {@link niceQuotes} or `transform()` instead,
|
|
21
|
+
* which use U+2019 for both per the Unicode Standard.
|
|
22
|
+
*/
|
|
23
|
+
export declare function classifyApostrophes(text: string, options?: QuoteOptions): string;
|
|
24
24
|
//# sourceMappingURL=quotes.d.ts.map
|
package/dist/quotes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quotes.d.ts","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"AAAA;;GAEG;AAeH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAA;AAE9D,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"quotes.d.ts","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"AAAA;;GAEG;AAeH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAA;AAE9D,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;CACpC;AA6JD,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAK3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAEpF"}
|
package/dist/quotes.js
CHANGED
|
@@ -104,8 +104,8 @@ function applyPunctuationStyle(text, sep, style) {
|
|
|
104
104
|
}
|
|
105
105
|
return text;
|
|
106
106
|
}
|
|
107
|
-
/**
|
|
108
|
-
|
|
107
|
+
/** Shared quote-processing pipeline. Returns text with MLA for apostrophes. */
|
|
108
|
+
function processQuotes(text, options) {
|
|
109
109
|
const sep = options.separator ?? DEFAULT_SEPARATOR;
|
|
110
110
|
const punctuationStyle = options.punctuationStyle ?? "american";
|
|
111
111
|
if (punctuationStyle === "none")
|
|
@@ -113,12 +113,26 @@ export function niceQuotes(text, options = {}) {
|
|
|
113
113
|
text = convertSingleQuotes(text, sep);
|
|
114
114
|
text = convertDoubleQuotes(text, sep);
|
|
115
115
|
text = applyPunctuationStyle(text, sep, punctuationStyle);
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
/** Convert straight quotes to smart quotes. */
|
|
119
|
+
export function niceQuotes(text, options = {}) {
|
|
116
120
|
// MLA is used internally so applyPunctuationStyle can distinguish
|
|
117
121
|
// apostrophes (MLA, don't move) from closing quotes (RSQ, do move).
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
// Always convert back to RSQ for standard output per Unicode.
|
|
123
|
+
return processQuotes(text, options).replaceAll(MODIFIER_LETTER_APOSTROPHE, RIGHT_SINGLE_QUOTE);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Classify apostrophes vs. closing single quotes.
|
|
127
|
+
*
|
|
128
|
+
* Returns the text with smart quotes applied, where apostrophes are
|
|
129
|
+
* U+02BC (MODIFIER LETTER APOSTROPHE) and closing single quotes are
|
|
130
|
+
* U+2019 (RIGHT SINGLE QUOTATION MARK).
|
|
131
|
+
*
|
|
132
|
+
* For display output, use {@link niceQuotes} or `transform()` instead,
|
|
133
|
+
* which use U+2019 for both per the Unicode Standard.
|
|
134
|
+
*/
|
|
135
|
+
export function classifyApostrophes(text, options = {}) {
|
|
136
|
+
return processQuotes(text, options);
|
|
123
137
|
}
|
|
124
138
|
//# sourceMappingURL=quotes.js.map
|
package/dist/quotes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quotes.js","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,kBAAkB,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAElF,MAAM,EACJ,OAAO,EACP,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,QAAQ,GACT,GAAG,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"quotes.js","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export { niceQuotes, type QuoteOptions, type PunctuationStyle } from "./quotes.js"
|
|
11
|
+
export { niceQuotes, classifyApostrophes, type QuoteOptions, type PunctuationStyle } from "./quotes.js"
|
|
12
12
|
import type { PunctuationStyle } from "./quotes.js"
|
|
13
13
|
export {
|
|
14
14
|
hyphenReplace,
|
|
@@ -155,17 +155,6 @@ export interface TransformOptions {
|
|
|
155
155
|
*/
|
|
156
156
|
checkIdempotency?: boolean
|
|
157
157
|
|
|
158
|
-
/**
|
|
159
|
-
* Use modifier letter apostrophe (U+02BC) instead of right single quote
|
|
160
|
-
* (U+2019) for contractions and possessives. This distinguishes apostrophes
|
|
161
|
-
* from closing quotes in most cases, though some ambiguous patterns (e.g.,
|
|
162
|
-
* `dogs'` vs a closing single quote) cannot be resolved without semantic
|
|
163
|
-
* understanding. May also break downstream regex patterns that expect
|
|
164
|
-
* standard quote characters like /don\u2019t/ or /O\u2019Brien/.
|
|
165
|
-
*
|
|
166
|
-
* Default: false
|
|
167
|
-
*/
|
|
168
|
-
useModifierLetterApostrophe?: boolean
|
|
169
158
|
}
|
|
170
159
|
|
|
171
160
|
import { niceQuotes } from "./quotes.js"
|
|
@@ -224,7 +213,6 @@ const defaultOpts: Required<Omit<TransformOptions, "separator">> = {
|
|
|
224
213
|
checkIdempotency: true,
|
|
225
214
|
punctuationStyle: "american",
|
|
226
215
|
dashStyle: "american",
|
|
227
|
-
useModifierLetterApostrophe: false,
|
|
228
216
|
}
|
|
229
217
|
|
|
230
218
|
export function transform(text: string, options: TransformOptions = {}): string {
|
|
@@ -241,18 +229,13 @@ export function transform(text: string, options: TransformOptions = {}): string
|
|
|
241
229
|
}
|
|
242
230
|
|
|
243
231
|
const original = text
|
|
244
|
-
const { symbols, fractions, degrees, superscript, ligatures, nbsp, collapseSpaces, checkIdempotency,
|
|
232
|
+
const { symbols, fractions, degrees, superscript, ligatures, nbsp, collapseSpaces, checkIdempotency, ...separatorOpts } = { ...defaultOpts, ...options }
|
|
245
233
|
|
|
246
234
|
text = hyphenReplace(text, separatorOpts)
|
|
247
235
|
if (separatorOpts.punctuationStyle !== "none") {
|
|
248
236
|
text = primeMarks(text, separatorOpts)
|
|
249
237
|
}
|
|
250
|
-
|
|
251
|
-
// because NBSP's PUNCTUATION_OR_QUOTE class includes RSQ but not MLA.
|
|
252
|
-
// Without this, apostrophes in "don't" would act as word boundaries
|
|
253
|
-
// during NBSP insertion. The final MLA→RSQ conversion at line 286
|
|
254
|
-
// runs after all transforms complete.
|
|
255
|
-
text = niceQuotes(text, { ...separatorOpts, useModifierLetterApostrophe: true })
|
|
238
|
+
text = niceQuotes(text, separatorOpts)
|
|
256
239
|
|
|
257
240
|
if (symbols) {
|
|
258
241
|
text = symbolTransform(text, separatorOpts)
|
|
@@ -284,12 +267,6 @@ export function transform(text: string, options: TransformOptions = {}): string
|
|
|
284
267
|
|
|
285
268
|
assertSeparatorCountPreserved(original, text, separator, "transform")
|
|
286
269
|
|
|
287
|
-
// Convert MLA→RSQ for regex-friendly output, after all transforms
|
|
288
|
-
// (especially NBSP which relies on MLA being letter-like).
|
|
289
|
-
if (!useModifierLetterApostrophe) {
|
|
290
|
-
text = text.replaceAll(UNICODE_SYMBOLS.MODIFIER_LETTER_APOSTROPHE, UNICODE_SYMBOLS.RIGHT_SINGLE_QUOTE)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
270
|
if (checkIdempotency) {
|
|
294
271
|
const secondPass = transform(text, { ...options, checkIdempotency: false })
|
|
295
272
|
/* istanbul ignore if -- defensive check that should never trigger */
|
package/src/quotes.ts
CHANGED
|
@@ -22,17 +22,6 @@ export interface QuoteOptions {
|
|
|
22
22
|
separator?: string
|
|
23
23
|
/** "american" (inside), "british" (outside), "none". Default: "american" */
|
|
24
24
|
punctuationStyle?: PunctuationStyle
|
|
25
|
-
/**
|
|
26
|
-
* Use modifier letter apostrophe (U+02BC) instead of right single quote
|
|
27
|
-
* (U+2019) for contractions and possessives. This distinguishes apostrophes
|
|
28
|
-
* from closing quotes in most cases, though some ambiguous patterns (e.g.,
|
|
29
|
-
* `dogs'` vs a closing single quote) cannot be resolved without semantic
|
|
30
|
-
* understanding. May also break downstream regex patterns that expect
|
|
31
|
-
* standard quote characters like /don\u2019t/ or /O\u2019Brien/.
|
|
32
|
-
*
|
|
33
|
-
* Default: false
|
|
34
|
-
*/
|
|
35
|
-
useModifierLetterApostrophe?: boolean
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
/** Convert straight single quotes to curly quotes and apostrophes */
|
|
@@ -177,8 +166,8 @@ function applyPunctuationStyle(text: string, sep: string, style: PunctuationStyl
|
|
|
177
166
|
return text
|
|
178
167
|
}
|
|
179
168
|
|
|
180
|
-
/**
|
|
181
|
-
|
|
169
|
+
/** Shared quote-processing pipeline. Returns text with MLA for apostrophes. */
|
|
170
|
+
function processQuotes(text: string, options: QuoteOptions): string {
|
|
182
171
|
const sep = options.separator ?? DEFAULT_SEPARATOR
|
|
183
172
|
const punctuationStyle = options.punctuationStyle ?? "american"
|
|
184
173
|
if (punctuationStyle === "none") return text
|
|
@@ -187,12 +176,27 @@ export function niceQuotes(text: string, options: QuoteOptions = {}): string {
|
|
|
187
176
|
text = convertDoubleQuotes(text, sep)
|
|
188
177
|
text = applyPunctuationStyle(text, sep, punctuationStyle)
|
|
189
178
|
|
|
179
|
+
return text
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Convert straight quotes to smart quotes. */
|
|
183
|
+
export function niceQuotes(text: string, options: QuoteOptions = {}): string {
|
|
190
184
|
// MLA is used internally so applyPunctuationStyle can distinguish
|
|
191
185
|
// apostrophes (MLA, don't move) from closing quotes (RSQ, do move).
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
186
|
+
// Always convert back to RSQ for standard output per Unicode.
|
|
187
|
+
return processQuotes(text, options).replaceAll(MODIFIER_LETTER_APOSTROPHE, RIGHT_SINGLE_QUOTE)
|
|
188
|
+
}
|
|
196
189
|
|
|
197
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Classify apostrophes vs. closing single quotes.
|
|
192
|
+
*
|
|
193
|
+
* Returns the text with smart quotes applied, where apostrophes are
|
|
194
|
+
* U+02BC (MODIFIER LETTER APOSTROPHE) and closing single quotes are
|
|
195
|
+
* U+2019 (RIGHT SINGLE QUOTATION MARK).
|
|
196
|
+
*
|
|
197
|
+
* For display output, use {@link niceQuotes} or `transform()` instead,
|
|
198
|
+
* which use U+2019 for both per the Unicode Standard.
|
|
199
|
+
*/
|
|
200
|
+
export function classifyApostrophes(text: string, options: QuoteOptions = {}): string {
|
|
201
|
+
return processQuotes(text, options)
|
|
198
202
|
}
|
package/src/tests/quotes.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { niceQuotes } from "../quotes.js"
|
|
1
|
+
import { niceQuotes, classifyApostrophes } from "../quotes.js"
|
|
2
2
|
import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR } from "../constants.js"
|
|
3
3
|
|
|
4
4
|
const {
|
|
@@ -11,9 +11,6 @@ const {
|
|
|
11
11
|
ELLIPSIS,
|
|
12
12
|
} = UNICODE_SYMBOLS
|
|
13
13
|
|
|
14
|
-
/** Enable MLA output for tests that assert the apostrophe codepoint. */
|
|
15
|
-
const MLA = { useModifierLetterApostrophe: true } as const
|
|
16
|
-
|
|
17
14
|
describe("niceQuotes", () => {
|
|
18
15
|
describe("double quotes", () => {
|
|
19
16
|
it.each([
|
|
@@ -52,7 +49,7 @@ describe("niceQuotes", () => {
|
|
|
52
49
|
// End-of-line quote becomes RIGHT quote
|
|
53
50
|
['not confident in that plan - "', `not confident in that plan - ${RIGHT_DOUBLE_QUOTE}`],
|
|
54
51
|
])('should convert double quotes in "%s"', (input, expected) => {
|
|
55
|
-
expect(
|
|
52
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
56
53
|
})
|
|
57
54
|
})
|
|
58
55
|
|
|
@@ -117,7 +114,7 @@ describe("niceQuotes", () => {
|
|
|
117
114
|
// Closing RSQ then opening LDQ
|
|
118
115
|
[`'hello' "world"`, `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} ${LEFT_DOUBLE_QUOTE}world${RIGHT_DOUBLE_QUOTE}`],
|
|
119
116
|
])('should handle single quotes/apostrophes in "%s"', (input, expected) => {
|
|
120
|
-
expect(
|
|
117
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
121
118
|
})
|
|
122
119
|
})
|
|
123
120
|
|
|
@@ -139,7 +136,7 @@ describe("niceQuotes", () => {
|
|
|
139
136
|
["'dogs'", `${LEFT_SINGLE_QUOTE}dogs${RIGHT_SINGLE_QUOTE}`],
|
|
140
137
|
["'he tells' stories", `${LEFT_SINGLE_QUOTE}he tells${RIGHT_SINGLE_QUOTE} stories`],
|
|
141
138
|
])('handles plural possessive: "%s"', (input, expected) => {
|
|
142
|
-
expect(
|
|
139
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
143
140
|
})
|
|
144
141
|
|
|
145
142
|
it.each([
|
|
@@ -147,8 +144,8 @@ describe("niceQuotes", () => {
|
|
|
147
144
|
`Bayes${MODIFIER_LETTER_APOSTROPHE} rule`,
|
|
148
145
|
`dogs${MODIFIER_LETTER_APOSTROPHE} and ${LEFT_SINGLE_QUOTE}cats${RIGHT_SINGLE_QUOTE}`,
|
|
149
146
|
])('plural possessive is idempotent: "%s"', (input) => {
|
|
150
|
-
expect(
|
|
151
|
-
expect(
|
|
147
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
148
|
+
expect(classifyApostrophes(classifyApostrophes(input))).toBe(input)
|
|
152
149
|
})
|
|
153
150
|
|
|
154
151
|
it("stress: many possessives interleaved with quotes", () => {
|
|
@@ -157,7 +154,7 @@ describe("niceQuotes", () => {
|
|
|
157
154
|
i % 2 === 0 ? `dogs'` : `'yes'`
|
|
158
155
|
)
|
|
159
156
|
const input = pairs.join(" ")
|
|
160
|
-
const result =
|
|
157
|
+
const result = classifyApostrophes(input)
|
|
161
158
|
// Every dogs' → MLA, every 'yes' → LSQ+yes+RSQ
|
|
162
159
|
const expectedParts = Array.from({ length: 50 }, (_, i) =>
|
|
163
160
|
i % 2 === 0
|
|
@@ -165,19 +162,19 @@ describe("niceQuotes", () => {
|
|
|
165
162
|
: `${LEFT_SINGLE_QUOTE}yes${RIGHT_SINGLE_QUOTE}`
|
|
166
163
|
)
|
|
167
164
|
expect(result).toBe(expectedParts.join(" "))
|
|
168
|
-
expect(
|
|
165
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
169
166
|
})
|
|
170
167
|
|
|
171
168
|
it("stress: 200 consecutive plural possessives", () => {
|
|
172
169
|
const words = ["dogs'", "cats'", "Bayes'", "Thomas'", "PLAYERS'"]
|
|
173
170
|
const input = Array.from({ length: 200 }, (_, i) => words[i % words.length]).join(" ")
|
|
174
171
|
const start = performance.now()
|
|
175
|
-
const result =
|
|
172
|
+
const result = classifyApostrophes(input)
|
|
176
173
|
expect(performance.now() - start).toBeLessThan(1000)
|
|
177
174
|
// All should be MLA (no LSQ to pair with)
|
|
178
175
|
expect(result).not.toContain(RIGHT_SINGLE_QUOTE)
|
|
179
176
|
expect(result).not.toContain("'")
|
|
180
|
-
expect(
|
|
177
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
181
178
|
})
|
|
182
179
|
|
|
183
180
|
it("stress: deeply nested alternating pattern", () => {
|
|
@@ -186,7 +183,7 @@ describe("niceQuotes", () => {
|
|
|
186
183
|
const input = Array.from({ length: 30 }, (_, i) =>
|
|
187
184
|
i % 2 === 0 ? `'word${i}'` : `${possessives[i % possessives.length]}'`
|
|
188
185
|
).join(" ")
|
|
189
|
-
const result =
|
|
186
|
+
const result = classifyApostrophes(input)
|
|
190
187
|
// Quoted words get LSQ/RSQ, possessives get MLA
|
|
191
188
|
for (let i = 0; i < 30; i++) {
|
|
192
189
|
if (i % 2 === 0) {
|
|
@@ -195,7 +192,7 @@ describe("niceQuotes", () => {
|
|
|
195
192
|
expect(result).toContain(`${possessives[i % possessives.length]}${MODIFIER_LETTER_APOSTROPHE}`)
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
|
-
expect(
|
|
195
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
199
196
|
})
|
|
200
197
|
})
|
|
201
198
|
|
|
@@ -214,11 +211,11 @@ describe("niceQuotes", () => {
|
|
|
214
211
|
["'twas the night, 'twas indeed", `${MODIFIER_LETTER_APOSTROPHE}twas the night, ${MODIFIER_LETTER_APOSTROPHE}twas indeed`],
|
|
215
212
|
[`"'twas the night"`, `${LEFT_DOUBLE_QUOTE}${MODIFIER_LETTER_APOSTROPHE}twas the night${RIGHT_DOUBLE_QUOTE}`],
|
|
216
213
|
])('handles leading apostrophe in "%s"', (input, expected) => {
|
|
217
|
-
expect(
|
|
214
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
218
215
|
})
|
|
219
216
|
|
|
220
217
|
it("multiple decades in one sentence", () => {
|
|
221
|
-
expect(
|
|
218
|
+
expect(classifyApostrophes("The '60s, '70s, and '80s were different.")).toBe(
|
|
222
219
|
`The ${MODIFIER_LETTER_APOSTROPHE}60s, ${MODIFIER_LETTER_APOSTROPHE}70s, and ${MODIFIER_LETTER_APOSTROPHE}80s were different.`
|
|
223
220
|
)
|
|
224
221
|
})
|
|
@@ -231,7 +228,7 @@ describe("niceQuotes", () => {
|
|
|
231
228
|
["digit neighbors (\\w includes digits)", "1 'n' 2", `1 ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} 2`],
|
|
232
229
|
["known false positive: quoted letter", "the letter 'n' is common", `the letter ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} is common`],
|
|
233
230
|
])('%s', (_label, input, expected) => {
|
|
234
|
-
expect(
|
|
231
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
235
232
|
})
|
|
236
233
|
|
|
237
234
|
it.each([
|
|
@@ -240,7 +237,7 @@ describe("niceQuotes", () => {
|
|
|
240
237
|
["punctuation precedes first apostrophe", "said, 'n' is good", `said, ${LEFT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE} is good`],
|
|
241
238
|
["newline replaces space", "Rock 'n'\nRoll", `Rock ${LEFT_SINGLE_QUOTE}n${RIGHT_SINGLE_QUOTE}\nRoll`],
|
|
242
239
|
])('does not produce MLA-n-MLA: %s', (_label, input, expected) => {
|
|
243
|
-
expect(
|
|
240
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
244
241
|
})
|
|
245
242
|
})
|
|
246
243
|
|
|
@@ -277,7 +274,7 @@ describe("niceQuotes", () => {
|
|
|
277
274
|
[`O'Brien's idea`, `O${MODIFIER_LETTER_APOSTROPHE}Brien${MODIFIER_LETTER_APOSTROPHE}s idea`],
|
|
278
275
|
[`The O'Connors`, `The O${MODIFIER_LETTER_APOSTROPHE}Connors`],
|
|
279
276
|
])('handles Irish/Scottish name apostrophes: "%s"', (input, expected) => {
|
|
280
|
-
expect(
|
|
277
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
281
278
|
})
|
|
282
279
|
|
|
283
280
|
// From smartquotes.js: complex nested quotes
|
|
@@ -311,7 +308,7 @@ describe("niceQuotes", () => {
|
|
|
311
308
|
// Quote with slash separator
|
|
312
309
|
['"option1"/"option2"', `${LEFT_DOUBLE_QUOTE}option1${RIGHT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}option2${RIGHT_DOUBLE_QUOTE}`],
|
|
313
310
|
])('handles complex pattern: "%s"', (input, expected) => {
|
|
314
|
-
expect(
|
|
311
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
315
312
|
})
|
|
316
313
|
|
|
317
314
|
it("handles multi-line dialogue", () => {
|
|
@@ -337,8 +334,8 @@ describe("niceQuotes", () => {
|
|
|
337
334
|
// Mixed quote types
|
|
338
335
|
`${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} ${LEFT_DOUBLE_QUOTE}world${RIGHT_DOUBLE_QUOTE}`,
|
|
339
336
|
])('is idempotent for: "%s"', (input) => {
|
|
340
|
-
expect(
|
|
341
|
-
expect(
|
|
337
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
338
|
+
expect(classifyApostrophes(classifyApostrophes(input))).toBe(input)
|
|
342
339
|
})
|
|
343
340
|
})
|
|
344
341
|
|
|
@@ -357,21 +354,21 @@ describe("niceQuotes", () => {
|
|
|
357
354
|
// Closing after separator
|
|
358
355
|
[`'hello${sep}'`, `${LEFT_SINGLE_QUOTE}hello${sep}${RIGHT_SINGLE_QUOTE}`, "closing quote after separator"],
|
|
359
356
|
])("%s → %s (%s)", (input, expected) => {
|
|
360
|
-
expect(
|
|
357
|
+
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
361
358
|
})
|
|
362
359
|
|
|
363
360
|
it("plural possessive across separator", () => {
|
|
364
361
|
const input = `dogs${sep}' food`
|
|
365
362
|
const expected = `dogs${sep}${MODIFIER_LETTER_APOSTROPHE} food`
|
|
366
|
-
expect(
|
|
367
|
-
expect(
|
|
363
|
+
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
364
|
+
expect(classifyApostrophes(expected, { separator: sep })).toBe(expected)
|
|
368
365
|
})
|
|
369
366
|
|
|
370
367
|
it("'n' with separators around word boundaries", () => {
|
|
371
368
|
const input = `Rock${sep} 'n' ${sep}Roll`
|
|
372
369
|
const expected = `Rock${sep} ${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE} ${sep}Roll`
|
|
373
|
-
expect(
|
|
374
|
-
expect(
|
|
370
|
+
expect(classifyApostrophes(input, { separator: sep })).toBe(expected)
|
|
371
|
+
expect(classifyApostrophes(expected, { separator: sep })).toBe(expected)
|
|
375
372
|
})
|
|
376
373
|
})
|
|
377
374
|
|
|
@@ -387,7 +384,7 @@ describe("niceQuotes", () => {
|
|
|
387
384
|
["'a", `${MODIFIER_LETTER_APOSTROPHE}a`],
|
|
388
385
|
["a'", `a${RIGHT_SINGLE_QUOTE}`],
|
|
389
386
|
])('handles edge quote pattern: "%s"', (input, expected) => {
|
|
390
|
-
expect(
|
|
387
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
391
388
|
})
|
|
392
389
|
})
|
|
393
390
|
|
|
@@ -411,7 +408,7 @@ describe("niceQuotes", () => {
|
|
|
411
408
|
["wouldn't've", `wouldn${MODIFIER_LETTER_APOSTROPHE}t${MODIFIER_LETTER_APOSTROPHE}ve`],
|
|
412
409
|
["couldn't've", `couldn${MODIFIER_LETTER_APOSTROPHE}t${MODIFIER_LETTER_APOSTROPHE}ve`],
|
|
413
410
|
])('handles double contraction: "%s"', (input, expected) => {
|
|
414
|
-
expect(
|
|
411
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
415
412
|
})
|
|
416
413
|
})
|
|
417
414
|
|
|
@@ -424,7 +421,7 @@ describe("niceQuotes", () => {
|
|
|
424
421
|
// Newline stops lookahead → MLA (not LSQ) since no closing quote visible
|
|
425
422
|
["'hello\nworld'", `${MODIFIER_LETTER_APOSTROPHE}hello\nworld${RIGHT_SINGLE_QUOTE}`],
|
|
426
423
|
])('handles multiline: "%s"', (input, expected) => {
|
|
427
|
-
expect(
|
|
424
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
428
425
|
})
|
|
429
426
|
})
|
|
430
427
|
|
|
@@ -490,7 +487,7 @@ describe("niceQuotes", () => {
|
|
|
490
487
|
// British: RSQ period moves outside (contrast)
|
|
491
488
|
[`${LEFT_SINGLE_QUOTE}hello.${RIGHT_SINGLE_QUOTE}`, `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE}.`, "british"],
|
|
492
489
|
])('%s → %s (%s)', (input, expected, style) => {
|
|
493
|
-
expect(
|
|
490
|
+
expect(classifyApostrophes(input, { punctuationStyle: style as "american" | "british" })).toBe(expected)
|
|
494
491
|
})
|
|
495
492
|
})
|
|
496
493
|
|
|
@@ -518,7 +515,7 @@ describe("niceQuotes", () => {
|
|
|
518
515
|
})
|
|
519
516
|
})
|
|
520
517
|
|
|
521
|
-
describe("
|
|
518
|
+
describe("niceQuotes vs classifyApostrophes", () => {
|
|
522
519
|
const RSQ = RIGHT_SINGLE_QUOTE
|
|
523
520
|
|
|
524
521
|
it.each([
|
|
@@ -533,8 +530,8 @@ describe("niceQuotes", () => {
|
|
|
533
530
|
expect(niceQuotes(input)).toBe(expected)
|
|
534
531
|
})
|
|
535
532
|
|
|
536
|
-
it("uses MLA
|
|
537
|
-
expect(
|
|
533
|
+
it("classifyApostrophes uses MLA for apostrophes", () => {
|
|
534
|
+
expect(classifyApostrophes("don't")).toBe(`don${MODIFIER_LETTER_APOSTROPHE}t`)
|
|
538
535
|
})
|
|
539
536
|
|
|
540
537
|
it("RSQ output is idempotent", () => {
|
|
@@ -545,11 +542,11 @@ describe("niceQuotes", () => {
|
|
|
545
542
|
}
|
|
546
543
|
})
|
|
547
544
|
|
|
548
|
-
it("
|
|
545
|
+
it("classifyApostrophes output is idempotent", () => {
|
|
549
546
|
const inputs = ["don't", "the dog's", "the dogs'", "O'Brien", "Rock 'n' Roll", "'90s"]
|
|
550
547
|
for (const input of inputs) {
|
|
551
|
-
const first =
|
|
552
|
-
expect(
|
|
548
|
+
const first = classifyApostrophes(input)
|
|
549
|
+
expect(classifyApostrophes(first)).toBe(first)
|
|
553
550
|
}
|
|
554
551
|
})
|
|
555
552
|
|
|
@@ -563,12 +560,12 @@ describe("niceQuotes", () => {
|
|
|
563
560
|
// Mixed MLA + RSQ in same text
|
|
564
561
|
[`I${MODIFIER_LETTER_APOSTROPHE}m fine and you${RIGHT_SINGLE_QUOTE}re great`, `I${MODIFIER_LETTER_APOSTROPHE}m fine and you${MODIFIER_LETTER_APOSTROPHE}re great`],
|
|
565
562
|
])('normalizes RSQ contraction "%s"', (input, expected) => {
|
|
566
|
-
expect(
|
|
563
|
+
expect(classifyApostrophes(input)).toBe(expected)
|
|
567
564
|
})
|
|
568
565
|
|
|
569
566
|
it("does NOT normalize RSQ when it is a closing quote", () => {
|
|
570
567
|
const input = `${LEFT_SINGLE_QUOTE}hello${RIGHT_SINGLE_QUOTE} world`
|
|
571
|
-
expect(
|
|
568
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
572
569
|
})
|
|
573
570
|
|
|
574
571
|
it.each([
|
|
@@ -578,20 +575,20 @@ describe("niceQuotes", () => {
|
|
|
578
575
|
[`${LEFT_SINGLE_QUOTE}Stop!${RIGHT_SINGLE_QUOTE}`, "exclamation"],
|
|
579
576
|
[`${LEFT_SINGLE_QUOTE}test,${RIGHT_SINGLE_QUOTE} more`, "comma"],
|
|
580
577
|
])("preserves closing RSQ preceded by non-word char (%s)", (input) => {
|
|
581
|
-
expect(
|
|
578
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
582
579
|
})
|
|
583
580
|
|
|
584
581
|
it("normalizes RSQ possessive with digit prefix", () => {
|
|
585
|
-
expect(
|
|
582
|
+
expect(classifyApostrophes(`GPT-2${RIGHT_SINGLE_QUOTE}s`)).toBe(`GPT-2${MODIFIER_LETTER_APOSTROPHE}s`)
|
|
586
583
|
})
|
|
587
584
|
|
|
588
585
|
it("normalizes RSQ plural possessive to MLA", () => {
|
|
589
|
-
expect(
|
|
586
|
+
expect(classifyApostrophes(`the dogs${RIGHT_SINGLE_QUOTE} owner`)).toBe(`the dogs${MODIFIER_LETTER_APOSTROPHE} owner`)
|
|
590
587
|
})
|
|
591
588
|
|
|
592
589
|
it("preserves RSQ plural possessive when matched with LSQ", () => {
|
|
593
590
|
const input = `${LEFT_SINGLE_QUOTE}dogs${RIGHT_SINGLE_QUOTE} and more`
|
|
594
|
-
expect(
|
|
591
|
+
expect(classifyApostrophes(input)).toBe(input)
|
|
595
592
|
})
|
|
596
593
|
})
|
|
597
594
|
})
|
|
@@ -600,24 +597,24 @@ describe("niceQuotes", () => {
|
|
|
600
597
|
it("handles 1500-char input without closing quote", () => {
|
|
601
598
|
const input = `'${"a".repeat(1500)}`
|
|
602
599
|
const start = performance.now()
|
|
603
|
-
const result =
|
|
600
|
+
const result = classifyApostrophes(input)
|
|
604
601
|
expect(performance.now() - start).toBeLessThan(1000)
|
|
605
|
-
expect(
|
|
602
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
606
603
|
})
|
|
607
604
|
|
|
608
605
|
it("handles 16 rapid apostrophes", () => {
|
|
609
606
|
const input = "a'b'c'd'e'f'g'h'i'j'k'l'm'n'o'p"
|
|
610
607
|
const expected = `a${MODIFIER_LETTER_APOSTROPHE}b${MODIFIER_LETTER_APOSTROPHE}c${MODIFIER_LETTER_APOSTROPHE}d${MODIFIER_LETTER_APOSTROPHE}e${MODIFIER_LETTER_APOSTROPHE}f${MODIFIER_LETTER_APOSTROPHE}g${MODIFIER_LETTER_APOSTROPHE}h${MODIFIER_LETTER_APOSTROPHE}i${MODIFIER_LETTER_APOSTROPHE}j${MODIFIER_LETTER_APOSTROPHE}k${MODIFIER_LETTER_APOSTROPHE}l${MODIFIER_LETTER_APOSTROPHE}m${MODIFIER_LETTER_APOSTROPHE}n${MODIFIER_LETTER_APOSTROPHE}o${MODIFIER_LETTER_APOSTROPHE}p`
|
|
611
608
|
const start = performance.now()
|
|
612
|
-
const result =
|
|
609
|
+
const result = classifyApostrophes(input)
|
|
613
610
|
expect(performance.now() - start).toBeLessThan(1000)
|
|
614
611
|
expect(result).toBe(expected)
|
|
615
|
-
expect(
|
|
612
|
+
expect(classifyApostrophes(result)).toBe(result)
|
|
616
613
|
})
|
|
617
614
|
|
|
618
615
|
it("handles 500-char word + possessive", () => {
|
|
619
616
|
const input = `${"a".repeat(500)}'s thing`
|
|
620
|
-
expect(
|
|
617
|
+
expect(classifyApostrophes(input)).toBe(`${"a".repeat(500)}${MODIFIER_LETTER_APOSTROPHE}s thing`)
|
|
621
618
|
})
|
|
622
619
|
})
|
|
623
620
|
|
package/src/tests/remark.test.ts
CHANGED
|
@@ -48,7 +48,7 @@ describe("remarkPunctilio", () => {
|
|
|
48
48
|
describe("basic transformations (nbsp enabled)", () => {
|
|
49
49
|
it.each([
|
|
50
50
|
["quotes", '"Hello," she said.', `${LDQ}Hello,${RDQ} she${NBSP}said.`],
|
|
51
|
-
["apostrophes", "It's a test.", `It${RSQ}s
|
|
51
|
+
["apostrophes", "It's a test.", `It${RSQ}s${NBSP}a${NBSP}test.`],
|
|
52
52
|
["em dashes", "Wait -- here it comes.", `Wait${EM_DASH}here it${NBSP}comes.`],
|
|
53
53
|
["en dashes (number range)", "Pages 1-5", `Pages${NBSP}1${EN_DASH}5`],
|
|
54
54
|
["ellipses", "Wait...", `Wait${ELLIPSIS}`],
|