punctilio 3.11.2 → 3.11.4

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.
Files changed (66) hide show
  1. package/dist/cli.d.ts +1 -12
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +4 -26
  4. package/dist/cli.js.map +1 -1
  5. package/dist/constants.d.ts +13 -61
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +12 -61
  8. package/dist/constants.js.map +1 -1
  9. package/dist/dashes.d.ts +0 -9
  10. package/dist/dashes.d.ts.map +1 -1
  11. package/dist/dashes.js +5 -27
  12. package/dist/dashes.js.map +1 -1
  13. package/dist/html.d.ts +1 -32
  14. package/dist/html.d.ts.map +1 -1
  15. package/dist/html.js +1 -29
  16. package/dist/html.js.map +1 -1
  17. package/dist/index.d.ts +5 -44
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -45
  20. package/dist/index.js.map +1 -1
  21. package/dist/markdown.d.ts +1 -34
  22. package/dist/markdown.d.ts.map +1 -1
  23. package/dist/markdown.js +1 -34
  24. package/dist/markdown.js.map +1 -1
  25. package/dist/nbsp.d.ts +1 -68
  26. package/dist/nbsp.d.ts.map +1 -1
  27. package/dist/nbsp.js +7 -87
  28. package/dist/nbsp.js.map +1 -1
  29. package/dist/prettier-plugin.d.ts +0 -14
  30. package/dist/prettier-plugin.d.ts.map +1 -1
  31. package/dist/prettier-plugin.js +0 -19
  32. package/dist/prettier-plugin.js.map +1 -1
  33. package/dist/quotes.d.ts +0 -3
  34. package/dist/quotes.d.ts.map +1 -1
  35. package/dist/quotes.js +11 -58
  36. package/dist/quotes.js.map +1 -1
  37. package/dist/rehype.d.ts +5 -142
  38. package/dist/rehype.d.ts.map +1 -1
  39. package/dist/rehype.js +9 -146
  40. package/dist/rehype.js.map +1 -1
  41. package/dist/remark.d.ts +0 -30
  42. package/dist/remark.d.ts.map +1 -1
  43. package/dist/remark.js +0 -41
  44. package/dist/remark.js.map +1 -1
  45. package/dist/symbols.d.ts +1 -7
  46. package/dist/symbols.d.ts.map +1 -1
  47. package/dist/symbols.js +10 -54
  48. package/dist/symbols.js.map +1 -1
  49. package/dist/utils.d.ts +10 -58
  50. package/dist/utils.d.ts.map +1 -1
  51. package/dist/utils.js +10 -58
  52. package/dist/utils.js.map +1 -1
  53. package/package.json +6 -5
  54. package/src/cli.ts +7 -33
  55. package/src/constants.ts +13 -62
  56. package/src/dashes.ts +5 -28
  57. package/src/html.ts +1 -33
  58. package/src/index.ts +30 -70
  59. package/src/markdown.ts +1 -38
  60. package/src/nbsp.ts +7 -88
  61. package/src/prettier-plugin.ts +0 -20
  62. package/src/quotes.ts +11 -59
  63. package/src/rehype.ts +11 -165
  64. package/src/remark.ts +1 -43
  65. package/src/symbols.ts +10 -67
  66. package/src/utils.ts +10 -60
package/src/index.ts CHANGED
@@ -1,55 +1,31 @@
1
- /**
2
- * punctilio - Smart typography transformations
3
- *
4
- * A library for converting plain ASCII punctuation into typographically
5
- * correct Unicode characters. Handles smart quotes, em-dashes, en-dashes,
6
- * minus signs, ellipses, multiplication signs, and more.
7
- *
8
- * @packageDocumentation
9
- */
10
-
11
- export { niceQuotes, classifyApostrophes, type QuoteOptions, type PunctuationStyle } from "./quotes.js"
1
+ export { DEFAULT_SEPARATOR, UNICODE_SYMBOLS } from "./constants.js"
12
2
  import type { PunctuationStyle } from "./quotes.js"
13
3
  export {
14
- hyphenReplace,
15
- enDashNumberRange,
4
+ type DashOptions,
5
+ type DashStyle,
16
6
  enDashDateRange,
7
+ enDashNumberRange,
8
+ hyphenReplace,
17
9
  minusReplace,
18
10
  numberRangeDisallowedPrefixes,
19
- type DashOptions,
20
- type DashStyle,
21
11
  } from "./dashes.js"
22
12
  import type { DashStyle } from "./dashes.js"
23
13
  export {
24
- ellipsis,
25
- multiplication,
26
- mathSymbols,
27
- legalSymbols,
28
- arrows,
29
- degrees,
30
- fractions,
31
- primeMarks,
32
- collapseSpaces,
33
- superscriptOrdinal,
34
- punctuationLigatures,
35
- symbolTransform,
36
- type SymbolOptions,
37
- } from "./symbols.js"
38
- export {
39
- nbspAfterShortWords,
40
- nbspBetweenNumberAndUnit,
41
- nbspBeforeLastWord,
14
+ HONORIFICS,
15
+ nbspAfterCopyrightSymbols,
16
+ nbspAfterHonorifics,
42
17
  nbspAfterReferenceAbbreviations,
43
18
  nbspAfterSectionSymbols,
44
- nbspAfterHonorifics,
45
- nbspAfterCopyrightSymbols,
19
+ nbspAfterShortWords,
20
+ nbspBeforeLastWord,
46
21
  nbspBetweenInitials,
22
+ nbspBetweenNumberAndUnit,
23
+ type NbspOptions,
47
24
  nbspTransform,
48
- UNITS,
49
- HONORIFICS,
50
25
  REFERENCE_ABBREVIATIONS,
51
- type NbspOptions,
26
+ UNITS,
52
27
  } from "./nbsp.js"
28
+ export { classifyApostrophes, niceQuotes, type PunctuationStyle, type QuoteOptions } from "./quotes.js"
53
29
 
54
30
  export interface TransformOptions {
55
31
  /**
@@ -171,13 +147,27 @@ export interface TransformOptions {
171
147
 
172
148
  import { niceQuotes } from "./quotes.js"
173
149
  import { hyphenReplace } from "./dashes.js"
174
- import { symbolTransform, fractions as fractionsTransform, degrees as degreesTransform, superscriptOrdinal as superscriptTransform, primeMarks, collapseSpaces as collapseSpacesTransform, punctuationLigatures as ligaturesTransform } from "./symbols.js"
150
+ import { collapseSpaces as collapseSpacesTransform, degrees as degreesTransform, fractions as fractionsTransform, punctuationLigatures as ligaturesTransform, primeMarks, superscriptOrdinal as superscriptTransform, symbolTransform } from "./symbols.js"
175
151
  import { nbspTransform as nbspTransformFn } from "./nbsp.js"
176
152
  import { assertSeparatorCountPreserved, filterUndefined, formatErrorString } from "./utils.js"
177
153
  import { DEFAULT_SEPARATOR, ISSUES_URL, UNICODE_SYMBOLS } from "./constants.js"
178
154
 
155
+ export {
156
+ arrows,
157
+ collapseSpaces,
158
+ degrees,
159
+ ellipsis,
160
+ fractions,
161
+ legalSymbols,
162
+ mathSymbols,
163
+ multiplication,
164
+ primeMarks,
165
+ punctuationLigatures,
166
+ superscriptOrdinal,
167
+ type SymbolOptions,
168
+ symbolTransform,
169
+ } from "./symbols.js"
179
170
  export { assertSeparatorAbsent, assertSeparatorCountPreserved, countSeparators, transformTextNodes } from "./utils.js"
180
- export { DEFAULT_SEPARATOR, UNICODE_SYMBOLS } from "./constants.js"
181
171
  export const MODIFIER_LETTER_APOSTROPHE = UNICODE_SYMBOLS.MODIFIER_LETTER_APOSTROPHE
182
172
 
183
173
  const defaultOpts: Required<Omit<TransformOptions, "separator">> = {
@@ -194,36 +184,6 @@ const defaultOpts: Required<Omit<TransformOptions, "separator">> = {
194
184
  dashStyle: "american",
195
185
  }
196
186
 
197
- /**
198
- * Applies all typography transformations: smart quotes, proper dashes,
199
- * and symbol improvements.
200
- *
201
- * This is a convenience function that applies transformations in sequence:
202
- * 1. hyphenReplace (em-dashes, en-dashes, minus signs)
203
- * 2. primeMarks (feet/inches, arcminutes/arcseconds)
204
- * 3. niceQuotes (smart quotes)
205
- * 4. symbolTransform (ellipses, multiplication, math symbols, legal symbols, arrows)
206
- * 5. fractions (disabled by default)
207
- * 6. degrees (disabled by default)
208
- * 7. superscript (disabled by default)
209
- * 8. ligatures (disabled by default)
210
- * 9. collapseSpaces (collapses multiple spaces into one)
211
- * 10. nbsp (non-breaking spaces, enabled by default)
212
- *
213
- * @example
214
- * ```ts
215
- * import { transform } from 'punctilio'
216
- *
217
- * transform('"Hello," she said - "it\'s pages 1-5."')
218
- * // → '"Hello," she said—"it's pages 1–5."'
219
- *
220
- * transform('Wait... 5x5 != 25 (c) 2024')
221
- * // → 'Wait… 5×5 ≠ 25 © 2024'
222
- *
223
- * transform('Add 1/2 cup', { fractions: true })
224
- * // → 'Add ½ cup'
225
- * ```
226
- */
227
187
  export function transform(text: string, options: TransformOptions = {}): string {
228
188
  const separator = options.separator ?? DEFAULT_SEPARATOR
229
189
 
package/src/markdown.ts CHANGED
@@ -1,14 +1,3 @@
1
- /**
2
- * Convenience module for Markdown-to-Markdown typography transformations.
3
- *
4
- * Provides a single function that takes Markdown input and returns
5
- * typographically improved Markdown output.
6
- *
7
- * Requires `unified`, `remark-parse`, and `remark-stringify` to be installed.
8
- *
9
- * @packageDocumentation
10
- */
11
-
12
1
  import { unified } from "unified"
13
2
  import remarkParse from "remark-parse"
14
3
  import remarkGfm from "remark-gfm"
@@ -18,9 +7,6 @@ import QuickLRU from "quick-lru"
18
7
  import { remarkPunctilio, type RemarkPunctilioOptions } from "./remark.js"
19
8
  import { stableStringify } from "./utils.js"
20
9
 
21
- /**
22
- * Options for the Markdown transform pipeline.
23
- */
24
10
  export interface MarkdownOptions extends RemarkPunctilioOptions {
25
11
  /**
26
12
  * Character for emphasis (`*text*` vs `_text_`) and strong (`**text**` vs `__text__`).
@@ -41,9 +27,6 @@ export interface MarkdownOptions extends RemarkPunctilioOptions {
41
27
  ruleMarker?: "-" | "*" | "_"
42
28
  }
43
29
 
44
- /**
45
- * Creates a unified processor pipeline for Markdown typography transformations.
46
- */
47
30
  function createProcessor(options: MarkdownOptions) {
48
31
  const {
49
32
  emphasisMarker,
@@ -78,31 +61,11 @@ const processorCache = new QuickLRU<string, ReturnType<typeof createProcessor>>(
78
61
  maxSize: MAX_PROCESSOR_CACHE_SIZE,
79
62
  })
80
63
 
81
- /**
82
- * Clears the processor cache. Exported for test isolation only.
83
- * @internal
84
- */
64
+ /** @internal */
85
65
  export function clearProcessorCache(): void {
86
66
  processorCache.clear()
87
67
  }
88
68
 
89
- /**
90
- * Transforms Markdown text with typographic improvements.
91
- *
92
- * Parses the input as Markdown, applies punctilio typography transformations
93
- * (smart quotes, em-dashes, ellipses, etc.), and serializes back to Markdown.
94
- *
95
- * The unified processor pipeline is cached and reused across calls with
96
- * identical options, avoiding redundant pipeline construction.
97
- *
98
- * @example
99
- * ```ts
100
- * import { transformMarkdown } from 'punctilio/markdown'
101
- *
102
- * await transformMarkdown('"Hello" -- world')
103
- * // → '\u201CHello\u201D\u2014world'
104
- * ```
105
- */
106
69
  export async function transformMarkdown(
107
70
  input: string,
108
71
  options: MarkdownOptions = {}
package/src/nbsp.ts CHANGED
@@ -1,14 +1,4 @@
1
- /**
2
- * Non-breaking space transformations for improved typography.
3
- *
4
- * Inserts non-breaking spaces (U+00A0) in positions where line breaks
5
- * would be typographically undesirable: after short words, between
6
- * numbers and units, before orphaned last words, etc.
7
- *
8
- * @module nbsp
9
- */
10
-
11
- import { UNICODE_SYMBOLS, LATIN_LETTERS, SPACE_CHARS, NBSP_CHARS, wordBoundaryEnd, getEscapedSeparator, cachedRegExp } from "./constants.js"
1
+ import { cachedRegExp, getEscapedSeparator, LATIN_LETTERS, NBSP_CHARS, SPACE_CHARS, UNICODE_SYMBOLS, wordBoundaryEnd } from "./constants.js"
12
2
 
13
3
  const {
14
4
  NBSP,
@@ -26,15 +16,11 @@ export interface NbspOptions {
26
16
  separator?: string
27
17
  }
28
18
 
29
- /** Space pattern matching regular space, tab, and nbsp. */
30
19
  const SPACE = `[${SPACE_CHARS}]`
31
20
 
32
21
  const UNICODE_UPPERCASE = "\\p{Lu}"
33
22
 
34
- /**
35
- * Common units for the number-unit nbsp pattern. Only matches these specific
36
- * abbreviations after a number to avoid false positives like "chapter 3 above".
37
- */
23
+ /** Only specific abbreviations are matched to avoid false positives. */
38
24
  export const UNITS: readonly string[] = [
39
25
  // Length
40
26
  "km", "cm", "mm", "mi", "ft", "yd", "nm", "pm", "m",
@@ -111,17 +97,8 @@ const ABBREVIATION_PATTERN = REFERENCE_ABBREVIATIONS.map((a) => `${a}\\.`).join(
111
97
  const PUNCTUATION_OR_QUOTE = `[.,!?:;)(${LEFT_DOUBLE_QUOTE}${RIGHT_DOUBLE_QUOTE}\u00AB\u00BB${LEFT_SINGLE_QUOTE}${RIGHT_SINGLE_QUOTE}"]`
112
98
  const COPYRIGHT_SYMBOLS = `[${COPYRIGHT}${REGISTERED}${TRADEMARK}]`
113
99
 
114
- /**
115
- * Adds non-breaking space after short words (1-2 letters) to prevent them from
116
- * being left alone at the end of a line.
117
- *
118
- * Skips when the short word is already preceded by an NBSP from an earlier
119
- * transform, or when this match immediately follows the previous one
120
- * (back-to-back short words in the same pass). Either case would bind three
121
- * or more words into a single line-break atom — defeating widow protection.
122
- *
123
- * @example "a cat" → "a\u00A0cat", "I am" → "I\u00A0am"
124
- */
100
+ // Skips when preceded by NBSP or back-to-back with the previous match,
101
+ // preventing 3+ words from binding into a single line-break atom.
125
102
  export function nbspAfterShortWords(text: string, options: NbspOptions = {}): string {
126
103
  const sep = getEscapedSeparator(options)
127
104
  const pattern = cachedRegExp(
@@ -137,11 +114,6 @@ export function nbspAfterShortWords(text: string, options: NbspOptions = {}): st
137
114
  })
138
115
  }
139
116
 
140
- /**
141
- * Adds non-breaking space between numbers and common units.
142
- *
143
- * @example "100 km" → "100\u00A0km", "5 kg" → "5\u00A0kg"
144
- */
145
117
  export function nbspBetweenNumberAndUnit(text: string, options: NbspOptions = {}): string {
146
118
  const sep = getEscapedSeparator(options)
147
119
  const wbe = wordBoundaryEnd(sep)
@@ -152,30 +124,12 @@ export function nbspBetweenNumberAndUnit(text: string, options: NbspOptions = {}
152
124
  return text.replace(pattern, `$<digit>$<marker1>${NBSP}$<marker2>$<unit>`)
153
125
  }
154
126
 
155
- /**
156
- * Maximum length, in characters, of the "last word" that gets an NBSP
157
- * glued to its preceding space. Short enough that a typical English word
158
- * qualifies but an entire trailing URL or long token does not. Only affects
159
- * widow prevention at end-of-string and paragraph breaks.
160
- */
161
127
  const MAX_LAST_WORD_LENGTH = 10
162
128
 
163
- /**
164
- * Maximum word length, in characters, considered for the cascade check.
165
- * Bounded so the lookbehind in `nbspBeforeLastWord` stays ReDoS-safe.
166
- */
129
+ // Bounded so the lookbehind stays ReDoS-safe.
167
130
  const MAX_MIDDLE_WORD_LENGTH = 15
168
131
 
169
- /**
170
- * Adds non-breaking space before the last word to prevent widows.
171
- *
172
- * Only applies to final words of 1 to {@link MAX_LAST_WORD_LENGTH} characters,
173
- * at end of string or paragraph break (\n\n). Uses non-multiline mode so $
174
- * matches only the true end of string.
175
- *
176
- * Skips when the second-to-last word is already glued backwards via NBSP,
177
- * so the phrase doesn't become a 3-word non-breaking atom.
178
- */
132
+ // Skips when the second-to-last word is already glued backwards via NBSP.
179
133
  export function nbspBeforeLastWord(text: string, options: NbspOptions = {}): string {
180
134
  const sep = getEscapedSeparator(options)
181
135
  const cascadeBlock = `(?<![${NBSP_CHARS}][${LATIN_LETTERS}]{1,${MAX_MIDDLE_WORD_LENGTH}})`
@@ -187,12 +141,6 @@ export function nbspBeforeLastWord(text: string, options: NbspOptions = {}): str
187
141
  return text.replace(pattern, `${NBSP}$<lastWord>$<ending>`)
188
142
  }
189
143
 
190
- /**
191
- * Adds non-breaking space after reference abbreviations (Fig., Vol., p., etc.)
192
- * when followed by a number.
193
- *
194
- * @example "Fig. 1" → "Fig.\u00A01", "p. 42" → "p.\u00A042"
195
- */
196
144
  export function nbspAfterReferenceAbbreviations(text: string, options: NbspOptions = {}): string {
197
145
  const sep = getEscapedSeparator(options)
198
146
  const pattern = cachedRegExp(
@@ -202,12 +150,6 @@ export function nbspAfterReferenceAbbreviations(text: string, options: NbspOptio
202
150
  return text.replace(pattern, `$<abbrev>$<marker>${NBSP}`)
203
151
  }
204
152
 
205
- /**
206
- * Adds non-breaking space after section (§) and paragraph (¶) symbols
207
- * when followed by a number.
208
- *
209
- * @example "§ 5" → "§\u00A05"
210
- */
211
153
  export function nbspAfterSectionSymbols(text: string, options: NbspOptions = {}): string {
212
154
  const sep = getEscapedSeparator(options)
213
155
  const pattern = cachedRegExp(
@@ -217,11 +159,6 @@ export function nbspAfterSectionSymbols(text: string, options: NbspOptions = {})
217
159
  return text.replace(pattern, `$<symbol>$<marker>${NBSP}`)
218
160
  }
219
161
 
220
- /**
221
- * Adds non-breaking space after honorific titles when followed by a capitalized name.
222
- *
223
- * @example "Dr. Smith" → "Dr.\u00A0Smith", "Mr. Jones" → "Mr.\u00A0Jones"
224
- */
225
162
  export function nbspAfterHonorifics(text: string, options: NbspOptions = {}): string {
226
163
  const sep = getEscapedSeparator(options)
227
164
  const pattern = cachedRegExp(
@@ -231,12 +168,6 @@ export function nbspAfterHonorifics(text: string, options: NbspOptions = {}): st
231
168
  return text.replace(pattern, `$<honorific>$<marker>${NBSP}`)
232
169
  }
233
170
 
234
- /**
235
- * Adds non-breaking space after copyright, registered, and trademark
236
- * symbols when followed by a year or capitalized name.
237
- *
238
- * @example "© 2024" → "©\u00A02024", "® Brand" → "®\u00A0Brand"
239
- */
240
171
  export function nbspAfterCopyrightSymbols(text: string, options: NbspOptions = {}): string {
241
172
  const sep = getEscapedSeparator(options)
242
173
  const pattern = cachedRegExp(
@@ -246,11 +177,6 @@ export function nbspAfterCopyrightSymbols(text: string, options: NbspOptions = {
246
177
  return text.replace(pattern, `$<symbol>$<marker>${NBSP}`)
247
178
  }
248
179
 
249
- /**
250
- * Adds non-breaking space between initials (e.g., "J. K. Rowling").
251
- *
252
- * @example "J. K. Rowling" → "J.\u00A0K.\u00A0Rowling"
253
- */
254
180
  export function nbspBetweenInitials(text: string, options: NbspOptions = {}): string {
255
181
  const sep = getEscapedSeparator(options)
256
182
  const pattern = cachedRegExp(
@@ -262,13 +188,7 @@ export function nbspBetweenInitials(text: string, options: NbspOptions = {}): st
262
188
 
263
189
  type NbspFn = (text: string, options: NbspOptions) => string
264
190
 
265
- /**
266
- * Ordered list of nbsp transforms. Specific patterns (honorifics, abbreviations,
267
- * initials) come first so they claim their matches before the generic
268
- * `nbspAfterShortWords`. For example, "No. 5" is matched by
269
- * `nbspAfterReferenceAbbreviations` first, preventing `nbspAfterShortWords`
270
- * from also matching "No" as a 2-letter short word.
271
- */
191
+ // Specific patterns first so they claim matches before generic nbspAfterShortWords.
272
192
  const NBSP_TRANSFORMS: NbspFn[] = [
273
193
  nbspAfterHonorifics,
274
194
  nbspAfterReferenceAbbreviations,
@@ -280,7 +200,6 @@ const NBSP_TRANSFORMS: NbspFn[] = [
280
200
  nbspBeforeLastWord,
281
201
  ]
282
202
 
283
- /** Apply all non-breaking space transformations in sequence. */
284
203
  export function nbspTransform(text: string, options: NbspOptions = {}): string {
285
204
  // All nbsp patterns require a space, tab, or existing nbsp to match.
286
205
  // Short-circuit if the text contains none of these.
@@ -1,18 +1,3 @@
1
- /**
2
- * Prettier plugin applying punctilio's typographic transforms to Markdown.
3
- *
4
- * @example
5
- * ```jsonc
6
- * // .prettierrc
7
- * { "plugins": ["punctilio/prettier-plugin"] }
8
- *
9
- * // .punctiliorc.json (optional)
10
- * { "punctuationStyle": "british", "skipTags": ["pre", "code"] }
11
- * ```
12
- *
13
- * @packageDocumentation
14
- */
15
-
16
1
  import { dirname } from "node:path"
17
2
 
18
3
  import * as prettierMarkdown from "prettier/plugins/markdown"
@@ -30,11 +15,6 @@ async function loadPunctilioConfig(searchFrom: string): Promise<RemarkPunctilioO
30
15
  return !result || result.isEmpty ? {} : (result.config as RemarkPunctilioOptions)
31
16
  }
32
17
 
33
- /**
34
- * Runs `remarkPunctilio`'s transformer against an mdast tree. The plugin
35
- * mutates the tree in place; awaiting handles any future async variant
36
- * without churning callers.
37
- */
38
18
  async function runRemarkPunctilio(opts: RemarkPunctilioOptions, ast: Root): Promise<void> {
39
19
  // The unified `Transformer` signature requires a VFile and a callback,
40
20
  // but `remarkPunctilio` only ever reads the tree. Narrow to the actual
package/src/quotes.ts CHANGED
@@ -1,9 +1,4 @@
1
- /**
2
- * Smart quote transformation: straight quotes → curly quotes.
3
- */
4
-
5
- import escapeStringRegexp from "escape-string-regexp"
6
- import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR, LATIN_LETTERS, TERMINAL_PUNCTUATION, getEscapedSeparator, cachedRegExp } from "./constants.js"
1
+ import { cachedRegExp, DEFAULT_SEPARATOR, escapeStringRegexp, getEscapedSeparator, LATIN_LETTERS, TERMINAL_PUNCTUATION, UNICODE_SYMBOLS } from "./constants.js"
7
2
 
8
3
  const {
9
4
  EM_DASH,
@@ -14,15 +9,9 @@ const {
14
9
  MODIFIER_LETTER_APOSTROPHE,
15
10
  } = UNICODE_SYMBOLS
16
11
 
17
- /** Joined string of all terminal punctuation characters for use in regex character classes. */
18
12
  const TERMINAL_PUNCTUATION_CLASS = TERMINAL_PUNCTUATION.join("")
19
13
 
20
- /**
21
- * Maximum consecutive closing quotes the punctuation-placement regexes will
22
- * traverse in a single match (e.g., '". for two-deep nesting). Bounded to keep
23
- * the regex linear on pathological inputs that contain many closing quotes
24
- * without any following period or comma.
25
- */
14
+ // Bounded to keep punctuation-placement regexes linear on pathological inputs.
26
15
  const MAX_NESTED_QUOTES = 4
27
16
 
28
17
  export const PUNCTUATION_STYLES = ["american", "british", "german", "french", "none"] as const
@@ -35,7 +24,6 @@ export interface QuoteOptions {
35
24
  punctuationStyle?: PunctuationStyle
36
25
  }
37
26
 
38
- /** Convert straight single quotes to curly quotes and apostrophes */
39
27
  function convertSingleQuotes(text: string, sep: string): string {
40
28
  const escapedSep = getEscapedSeparator({ separator: sep })
41
29
 
@@ -86,11 +74,8 @@ function convertSingleQuotes(text: string, sep: string): string {
86
74
  return text
87
75
  }
88
76
 
89
- /**
90
- * Convert unmatched RSQ after s/S to MLA (plural possessives like dogs', Bayes').
91
- * Tracks LSQ/RSQ balance left-to-right so that paired closing quotes
92
- * (e.g., 'yes' where s precedes RSQ) remain RSQ.
93
- */
77
+ // Converts unmatched RSQ after s/S to MLA (plural possessives).
78
+ // Tracks LSQ/RSQ balance so paired closing quotes remain RSQ.
94
79
  function convertUnmatchedPluralPossessives(text: string, sep: string): string {
95
80
  let singleQuoteBalance = 0
96
81
  return text.replace(
@@ -116,9 +101,6 @@ function convertUnmatchedPluralPossessives(text: string, sep: string): string {
116
101
  )
117
102
  }
118
103
 
119
- /**
120
- * Build the beginning-double-quote regex pattern from named fragments.
121
- */
122
104
  function buildBeginningDoublePattern(escapedSep: string, rawEscSep: string): string {
123
105
  // Consuming boundary (not a lookbehind): variable-width lookbehinds
124
106
  // with alternation cause ReDoS. Re-emitted via $<boundary> in the replacement.
@@ -140,7 +122,6 @@ function buildBeginningDoublePattern(escapedSep: string, rawEscSep: string): str
140
122
  return `${boundary}${beforeCapture}["](?:${afterConditions.join("|")})`
141
123
  }
142
124
 
143
- /** Convert straight double quotes to curly quotes */
144
125
  function convertDoubleQuotes(text: string, sep: string): string {
145
126
  const rawEscSep = escapeStringRegexp(sep)
146
127
  const escapedSep = getEscapedSeparator({ separator: sep })
@@ -166,18 +147,8 @@ function convertDoubleQuotes(text: string, sep: string): string {
166
147
  }
167
148
 
168
149
  /**
169
- * Build a regex that matches a punctuation mark sitting OUTSIDE one or more
170
- * consecutive closing quotes, for American-style inside-the-quotes placement.
171
- *
172
- * Shape: `<non-terminal>(sepBefore)(quotes)(sepAfter)<punctuation>`
173
- *
174
- * - The lookbehind ensures the char just before the match isn't already terminal
175
- * punctuation (so we don't rewrite, e.g., "Stop!".).
176
- * - `quotes` matches 1 to MAX_NESTED_QUOTES closing quotes (possibly separated
177
- * by element-boundary markers). The bound keeps the regex linear on
178
- * pathological inputs — unbounded `*` causes catastrophic backtracking when
179
- * many closing quotes appear with no following punctuation.
180
- * - `punctuationPattern` is the trailing pattern (e.g. `,` or `(?!\.\.\.)\.`).
150
+ * Matches punctuation OUTSIDE consecutive closing quotes (American style).
151
+ * `quotes` is bounded to MAX_NESTED_QUOTES to prevent catastrophic backtracking.
181
152
  */
182
153
  function buildOutsidePunctuationRegex(escapedSep: string, punctuationPattern: string): RegExp {
183
154
  const closingQuoteClass = `[${RIGHT_SINGLE_QUOTE}${RIGHT_DOUBLE_QUOTE}]`
@@ -192,10 +163,6 @@ function buildOutsidePunctuationRegex(escapedSep: string, punctuationPattern: st
192
163
  return cachedRegExp(pattern, "g")
193
164
  }
194
165
 
195
- /**
196
- * Move a punctuation mark sitting outside one or more consecutive closing
197
- * quotes to the inside (American style): `Hello'".` → `Hello.'"`.
198
- */
199
166
  function moveOutsideToInside(
200
167
  text: string,
201
168
  escapedSep: string,
@@ -206,7 +173,6 @@ function moveOutsideToInside(
206
173
  return text.replace(regex, `$<sepBefore>${replacementChar}$<quotes>$<sepAfter>`)
207
174
  }
208
175
 
209
- /** Apply American or British punctuation style */
210
176
  function applyPunctuationStyle(text: string, sep: string, style: PunctuationStyle): string {
211
177
  const escapedSep = getEscapedSeparator({ separator: sep })
212
178
 
@@ -238,11 +204,8 @@ function applyPunctuationStyle(text: string, sep: string, style: PunctuationStyl
238
204
  return text
239
205
  }
240
206
 
241
- /**
242
- * Normalize German quotes back to American for idempotent re-processing.
243
- * Tracks „..." (U+201E/U+201C) and ‚...' (U+201A/U+2018) pairs so that
244
- * the ambiguous closer characters are mapped to the correct American equivalents.
245
- */
207
+ // Normalize German quotes back to American for idempotent re-processing.
208
+ // Tracks „..." and ‚...' pairs so ambiguous closers map correctly.
246
209
  function normalizeGermanQuotes(text: string): string {
247
210
  const DLQ = UNICODE_SYMBOLS.DOUBLE_LOW_9_QUOTE
248
211
  const SLQ = UNICODE_SYMBOLS.SINGLE_LOW_9_QUOTE
@@ -280,7 +243,6 @@ function normalizeGermanQuotes(text: string): string {
280
243
  return chars.join("")
281
244
  }
282
245
 
283
- /** Remap American curly quotes to German low-9 style. Safe because apostrophes are still MLA at this stage. */
284
246
  function applyGermanQuotes(text: string): string {
285
247
  return text
286
248
  .replaceAll(LEFT_DOUBLE_QUOTE, UNICODE_SYMBOLS.DOUBLE_LOW_9_QUOTE)
@@ -289,11 +251,8 @@ function applyGermanQuotes(text: string): string {
289
251
  .replaceAll(RIGHT_SINGLE_QUOTE, LEFT_SINGLE_QUOTE)
290
252
  }
291
253
 
292
- /**
293
- * Normalize French guillemets back to American for idempotent re-processing.
294
- * Strips either NBSP or NNBSP padding — older outputs used NBSP before the
295
- * fix to the Unicode CLDR / Imprimerie nationale prescription of NNBSP.
296
- */
254
+ // Normalize French guillemets back to American for idempotent re-processing.
255
+ // Strips either NBSP or NNBSP padding (older outputs used NBSP).
297
256
  function normalizeFrenchQuotes(text: string): string {
298
257
  const innerSpace = `[${UNICODE_SYMBOLS.NBSP}${UNICODE_SYMBOLS.NNBSP}]`
299
258
  return text
@@ -301,12 +260,7 @@ function normalizeFrenchQuotes(text: string): string {
301
260
  .replace(cachedRegExp(`${innerSpace}?${UNICODE_SYMBOLS.RIGHT_GUILLEMET}`, "g"), RIGHT_DOUBLE_QUOTE)
302
261
  }
303
262
 
304
- /**
305
- * Remap American curly double quotes to French guillemets with NNBSP padding.
306
- * Uses U+202F (NARROW NO-BREAK SPACE) per Unicode CLDR fr locale and
307
- * Imprimerie nationale's "Lexique des règles typographiques" (6th ed., 2002),
308
- * which prescribe a narrow non-breaking space — not U+00A0 — inside `« … »`.
309
- */
263
+ // Uses U+202F (NARROW NO-BREAK SPACE) per Unicode CLDR and Imprimerie nationale.
310
264
  function applyFrenchQuotes(text: string): string {
311
265
  return text
312
266
  .replaceAll(LEFT_DOUBLE_QUOTE, `${UNICODE_SYMBOLS.LEFT_GUILLEMET}${UNICODE_SYMBOLS.NNBSP}`)
@@ -318,13 +272,11 @@ interface LocaleQuoteTransform {
318
272
  apply: (text: string) => string
319
273
  }
320
274
 
321
- /** Locale-specific normalize (pre-pipeline) and apply (post-pipeline) functions. */
322
275
  const localeQuoteTransforms: Partial<Record<PunctuationStyle, LocaleQuoteTransform>> = {
323
276
  german: { normalize: normalizeGermanQuotes, apply: applyGermanQuotes },
324
277
  french: { normalize: normalizeFrenchQuotes, apply: applyFrenchQuotes },
325
278
  }
326
279
 
327
- /** Shared quote-processing pipeline. Returns text with MLA for apostrophes. */
328
280
  function processQuotes(text: string, options: QuoteOptions): string {
329
281
  const sep = options.separator ?? DEFAULT_SEPARATOR
330
282
  const punctuationStyle = options.punctuationStyle ?? "american"