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 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
- - When `useModifierLetterApostrophe` is enabled, apostrophes use Modifier Letter Apostrophe (U+02BC) while closing single quotes use Right Single Quotation Mark (U+2019), producing semantically distinct codepoints in most cases. Contractions (don't), possessives (dog's), and leading abbreviations ('twas, '99) all output U+02BC. Bare trailing possessives like `dogs'` remain ambiguous. This option is off by default since U+02BC may not be matched by downstream regex patterns that expect standard quote characters.
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";
@@ -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;AAClF,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;IAE1B;;;;;;;;;OASG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAA;CACtC;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;AAkDpF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA8E9E"}
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, useModifierLetterApostrophe, ...separatorOpts } = { ...defaultOpts, ...options };
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
- // niceQuotes must output MLA here regardless of the user's option,
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":"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"}
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
@@ -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;IACnC;;;;;;;;;OASG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAA;CACtC;AAgJD,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAiB3E"}
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
- /** Convert straight quotes to smart quotes. */
108
- export function niceQuotes(text, options = {}) {
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
- // Convert back to RSQ unless the caller opts into the distinction.
119
- if (!options.useModifierLetterApostrophe) {
120
- text = text.replaceAll(MODIFIER_LETTER_APOSTROPHE, RIGHT_SINGLE_QUOTE);
121
- }
122
- return text;
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
@@ -1 +1 @@
1
- {"version":3,"file":"quotes.js","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"quotes.js","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "punctilio",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "description": "Smart typography transformations: curly quotes, em-dashes, en-dashes, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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, useModifierLetterApostrophe, ...separatorOpts } = { ...defaultOpts, ...options }
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
- // niceQuotes must output MLA here regardless of the user's option,
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
- /** Convert straight quotes to smart quotes. */
181
- export function niceQuotes(text: string, options: QuoteOptions = {}): string {
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
- // Convert back to RSQ unless the caller opts into the distinction.
193
- if (!options.useModifierLetterApostrophe) {
194
- text = text.replaceAll(MODIFIER_LETTER_APOSTROPHE, RIGHT_SINGLE_QUOTE)
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
- return text
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
  }
@@ -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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(input)
151
- expect(niceQuotes(niceQuotes(input, MLA), MLA)).toBe(input)
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 = niceQuotes(input, MLA)
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(niceQuotes(result, MLA)).toBe(result)
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 = niceQuotes(input, MLA)
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(niceQuotes(result, MLA)).toBe(result)
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 = niceQuotes(input, MLA)
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(niceQuotes(result, MLA)).toBe(result)
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(niceQuotes(input, MLA)).toBe(expected)
214
+ expect(classifyApostrophes(input)).toBe(expected)
218
215
  })
219
216
 
220
217
  it("multiple decades in one sentence", () => {
221
- expect(niceQuotes("The '60s, '70s, and '80s were different.", MLA)).toBe(
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(input)
341
- expect(niceQuotes(niceQuotes(input, MLA), MLA)).toBe(input)
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(niceQuotes(input, { separator: sep, ...MLA })).toBe(expected)
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(niceQuotes(input, { separator: sep, ...MLA })).toBe(expected)
367
- expect(niceQuotes(expected, { separator: sep, ...MLA })).toBe(expected)
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(niceQuotes(input, { separator: sep, ...MLA })).toBe(expected)
374
- expect(niceQuotes(expected, { separator: sep, ...MLA })).toBe(expected)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, { punctuationStyle: style as "american" | "british", ...MLA })).toBe(expected)
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("useModifierLetterApostrophe option", () => {
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 when option is true", () => {
537
- expect(niceQuotes("don't", MLA)).toBe(`don${MODIFIER_LETTER_APOSTROPHE}t`)
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("MLA output is idempotent", () => {
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 = niceQuotes(input, MLA)
552
- expect(niceQuotes(first, MLA)).toBe(first)
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(niceQuotes(input, MLA)).toBe(expected)
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(niceQuotes(input, MLA)).toBe(input)
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(niceQuotes(input, MLA)).toBe(input)
578
+ expect(classifyApostrophes(input)).toBe(input)
582
579
  })
583
580
 
584
581
  it("normalizes RSQ possessive with digit prefix", () => {
585
- expect(niceQuotes(`GPT-2${RIGHT_SINGLE_QUOTE}s`, MLA)).toBe(`GPT-2${MODIFIER_LETTER_APOSTROPHE}s`)
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(niceQuotes(`the dogs${RIGHT_SINGLE_QUOTE} owner`, MLA)).toBe(`the dogs${MODIFIER_LETTER_APOSTROPHE} owner`)
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(niceQuotes(input, MLA)).toBe(input)
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 = niceQuotes(input, MLA)
600
+ const result = classifyApostrophes(input)
604
601
  expect(performance.now() - start).toBeLessThan(1000)
605
- expect(niceQuotes(result, MLA)).toBe(result)
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 = niceQuotes(input, MLA)
609
+ const result = classifyApostrophes(input)
613
610
  expect(performance.now() - start).toBeLessThan(1000)
614
611
  expect(result).toBe(expected)
615
- expect(niceQuotes(result, MLA)).toBe(result)
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(niceQuotes(input, MLA)).toBe(`${"a".repeat(500)}${MODIFIER_LETTER_APOSTROPHE}s thing`)
617
+ expect(classifyApostrophes(input)).toBe(`${"a".repeat(500)}${MODIFIER_LETTER_APOSTROPHE}s thing`)
621
618
  })
622
619
  })
623
620
 
@@ -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 a${NBSP}test.`],
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}`],