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/rehype.ts CHANGED
@@ -1,44 +1,22 @@
1
- /**
2
- * Rehype plugin for applying punctilio typography transformations to HTML.
3
- *
4
- * This plugin integrates punctilio with the unified/rehype ecosystem,
5
- * allowing you to apply smart typography transformations to HTML ASTs.
6
- *
7
- * @packageDocumentation
8
- */
9
-
10
- import type { Root, Element, Text, ElementContent, Parent, RootContent } from "hast"
1
+ import type { Element, ElementContent, Parent, Root, RootContent, Text } from "hast"
11
2
  import type { Transformer } from "unified"
12
3
 
13
- import { visitParents, SKIP } from "unist-util-visit-parents"
4
+ import { SKIP, visitParents } from "unist-util-visit-parents"
14
5
 
15
6
  import { transform, type TransformOptions } from "./index.js"
16
7
  import { DEFAULT_SEPARATOR, MAX_RECURSION_DEPTH } from "./constants.js"
17
- import { transformTextNodes, formatErrorString } from "./utils.js"
8
+ import { formatErrorString, transformTextNodes } from "./utils.js"
18
9
 
19
- /** Predicate that decides whether an HTML element should be skipped during transformation. */
20
10
  type ElementPredicate = (node: Element) => boolean
21
11
 
22
- /** Function that transforms a plain-text string (e.g. smart quotes, dashes). */
23
12
  type TextTransformer = (input: string) => string
24
13
 
25
- /**
26
- * Predicate that decides whether an individual text node should be excluded
27
- * from transformation. Called after element-level `shouldSkip`, so it is
28
- * never invoked for text inside elements that are already skipped.
29
- *
30
- * `ancestors` is the chain of Element parents for the text node, ordered
31
- * root first, nearest last.
32
- */
14
+ /** Per-text-node skip predicate, called after element-level `shouldSkip`. */
33
15
  export type TextNodeSkipPredicate = (
34
16
  textNode: Text,
35
17
  ancestors: readonly Element[],
36
18
  ) => boolean
37
19
 
38
- /**
39
- * Options shared by functions that flatten and transform text nodes inside
40
- * an element tree.
41
- */
42
20
  export interface ElementTransformOptions {
43
21
  /**
44
22
  * Optional per-text-node skip predicate. When it returns `true` for a
@@ -49,9 +27,6 @@ export interface ElementTransformOptions {
49
27
  shouldSkipText?: TextNodeSkipPredicate
50
28
  }
51
29
 
52
- /**
53
- * Options for the rehype-punctilio plugin.
54
- */
55
30
  export interface RehypePunctilioOptions
56
31
  extends TransformOptions,
57
32
  ElementTransformOptions {
@@ -75,18 +50,6 @@ export interface RehypePunctilioOptions
75
50
 
76
51
  const DEFAULT_SKIP_TAGS = ["code", "pre", "script", "style", "kbd", "var", "samp", "template", "math", "svg"]
77
52
 
78
- /**
79
- * Flattens text nodes from an element tree into a single array.
80
- *
81
- * Pass `options.shouldSkipText` to exclude individual text nodes from the
82
- * result. Applied after element-level `shouldSkip`, so the hook is never
83
- * invoked for text inside an already-skipped element.
84
- *
85
- * @example
86
- * ```ts
87
- * const textNodes = flattenTextNodes(paragraphElement, (el) => el.tagName === 'code')
88
- * ```
89
- */
90
53
  export function flattenTextNodes(
91
54
  node: Element | ElementContent,
92
55
  shouldSkip: ElementPredicate,
@@ -136,15 +99,6 @@ function flattenTextNodesImpl(
136
99
  return []
137
100
  }
138
101
 
139
- /**
140
- * Extracts concatenated text content from an element.
141
- *
142
- * @example
143
- * ```ts
144
- * const text = getTextContent(paragraphElement)
145
- * // Returns "Hello, world!" for <p>Hello, <em>world!</em></p>
146
- * ```
147
- */
148
102
  export function getTextContent(
149
103
  node: Element,
150
104
  shouldSkip: ElementPredicate = () => false
@@ -154,16 +108,6 @@ export function getTextContent(
154
108
  .join("")
155
109
  }
156
110
 
157
- /**
158
- * Recursively finds the first text node in a tree of HTML elements.
159
- * Returns null if no text nodes exist.
160
- *
161
- * @example
162
- * ```ts
163
- * const textNode = getFirstTextNode(divElement)
164
- * if (textNode) console.log(textNode.value)
165
- * ```
166
- */
167
111
  export function getFirstTextNode(
168
112
  node: Parent | RootContent,
169
113
  depth: number = 0
@@ -187,21 +131,8 @@ export function getFirstTextNode(
187
131
  const QUOTE_OPENERS = new Set(["\u201C"])
188
132
  const CLOSER_TO_OPENER: Record<string, string> = { "\u201D": "\u201C" }
189
133
 
190
- /**
191
- * Validates that smart double quotes in a text string are properly matched.
192
- *
193
- * Only checks double quotes (\u201C/\u201D). Single quotes are intentionally
194
- * excluded because \u2019 (right single quote) doubles as an apostrophe,
195
- * making balance-checking unreliable.
196
- *
197
- * @throws Error if double quotes are mismatched
198
- *
199
- * @example
200
- * ```ts
201
- * assertSmartQuotesMatch('\u201CHello\u201D') // OK
202
- * assertSmartQuotesMatch('\u201CHello') // throws Error
203
- * ```
204
- */
134
+ // Only checks double quotes \u2014 single quotes are excluded because
135
+ // U+2019 doubles as an apostrophe, making balance-checking unreliable.
205
136
  export function assertSmartQuotesMatch(input: string): void {
206
137
  if (!input) return
207
138
 
@@ -225,38 +156,9 @@ export function assertSmartQuotesMatch(input: string): void {
225
156
  }
226
157
 
227
158
  /**
228
- * Applies a transformation to element text content while preserving HTML structure.
229
- *
230
- * This function uses a marker technique to handle text that spans multiple
231
- * HTML elements. It:
232
- * 1. Appends a private-use Unicode character to each child's text content
233
- * 2. Concatenates and transforms the whole paragraph
234
- * 3. Splits the result back into the original text nodes
235
- *
236
- * When `checkInvariance` is true, also verifies that the transform produces
237
- * the same result with and without markers — i.e.,
238
- * `stripMarkers(transform(textWithMarkers)) === transform(stripMarkers(text))`.
239
- * Useful for debugging transforms that accidentally interact with markers.
240
- *
241
- * Pass `options.shouldSkipText` to opt individual text nodes out of
242
- * transformation without skipping their enclosing element; skipped text
243
- * nodes keep their original `.value`.
244
- *
245
- * @throws Error if transformation alters the number of text nodes
246
- * @throws Error if `checkInvariance` is true and the invariance check fails
247
- *
248
- * @example
249
- * ```ts
250
- * import { transformElement, DEFAULT_SEPARATOR } from 'punctilio/rehype'
251
- *
252
- * // Apply a custom transform to an element
253
- * transformElement(
254
- * paragraphElement,
255
- * (text) => text.replace(/eg\b/g, 'e.g.'),
256
- * (el) => el.tagName === 'code',
257
- * DEFAULT_SEPARATOR
258
- * )
259
- * ```
159
+ * Transforms element text using the separator-marking technique. When
160
+ * `checkInvariance` is true, verifies `stripMarkers(transform(marked)) ===
161
+ * transform(stripMarkers(text))` for debugging marker interactions.
260
162
  */
261
163
  export function transformElement(
262
164
  node: Element,
@@ -299,12 +201,8 @@ export function transformElement(
299
201
  }
300
202
  }
301
203
 
302
- /**
303
- * HTML block-level elements. When a transformable element has a block-level
304
- * child, its children are processed independently rather than as a single
305
- * unit. This prevents merging text across semantically independent blocks
306
- * (e.g., separate paragraphs inside a div).
307
- */
204
+ // Block children cause per-block processing to avoid merging text across
205
+ // semantically independent blocks (e.g., separate <p>s inside a <div>).
308
206
  const BLOCK_ELEMENTS = new Set([
309
207
  "address", "article", "aside", "blockquote", "details", "dialog",
310
208
  "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure",
@@ -313,10 +211,6 @@ const BLOCK_ELEMENTS = new Set([
313
211
  "p", "pre", "section", "table", "ul",
314
212
  ])
315
213
 
316
- /**
317
- * HTML elements that can contain transformable text content.
318
- * We traverse into these to find text nodes to transform.
319
- */
320
214
  const TRANSFORMABLE_ELEMENTS = new Set([
321
215
  "p",
322
216
  "em",
@@ -374,11 +268,6 @@ const TRANSFORMABLE_ELEMENTS = new Set([
374
268
  "rp",
375
269
  ])
376
270
 
377
- /**
378
- * Checks whether an element has any text node descendants (skipping elements
379
- * matched by shouldSkip). Returns as soon as one is found — avoids allocating
380
- * an array of all text nodes just to check existence.
381
- */
382
271
  function hasTextDescendant(
383
272
  node: Element | ElementContent,
384
273
  shouldSkip: ElementPredicate,
@@ -403,20 +292,6 @@ function hasTextDescendant(
403
292
  return false
404
293
  }
405
294
 
406
- /**
407
- * Collects elements that should have text transformations applied.
408
- *
409
- * An element is collected as a single transformation unit when it:
410
- * 1. Has direct text children (original behavior), OR
411
- * 2. Has text descendants and NO block-level children — meaning all its
412
- * content is inline (phrasing) and should share transformation context
413
- * (e.g., `<p><em>"Hello</em><span>, world"</span></p>`).
414
- *
415
- * When an element has block-level children, we recurse so each block
416
- * is processed independently (e.g., `<div><p>A</p><p>B</p></div>`).
417
- *
418
- * @param alreadyTransformed - Elements already processed; skipped during traversal to avoid redundant work
419
- */
420
295
  export function collectTransformableElements(
421
296
  node: Element,
422
297
  shouldSkip: ElementPredicate,
@@ -462,11 +337,6 @@ export function collectTransformableElements(
462
337
  return results
463
338
  }
464
339
 
465
- /**
466
- * Recursively marks all element descendants as transformed.
467
- * Used to prevent redundant processing of nested elements whose text
468
- * was already processed as part of a parent element.
469
- */
470
340
  function markDescendants(node: Element, set: Set<Element>, depth: number = 0): void {
471
341
  /* istanbul ignore if -- defensive: prevents stack overflow from malicious HTML */
472
342
  if (depth > MAX_RECURSION_DEPTH) {
@@ -481,30 +351,6 @@ function markDescendants(node: Element, set: Set<Element>, depth: number = 0): v
481
351
  }
482
352
  }
483
353
 
484
- /**
485
- * Rehype plugin that applies punctilio typography transformations to HTML.
486
- *
487
- * @example
488
- * ```ts
489
- * import { unified } from 'unified'
490
- * import remarkParse from 'remark-parse'
491
- * import remarkRehype from 'remark-rehype'
492
- * import rehypeStringify from 'rehype-stringify'
493
- * import { rehypePunctilio } from 'punctilio/rehype'
494
- *
495
- * const result = await unified()
496
- * .use(remarkParse)
497
- * .use(remarkRehype)
498
- * .use(rehypePunctilio, {
499
- * punctuationStyle: 'american',
500
- * dashStyle: 'american',
501
- * })
502
- * .use(rehypeStringify)
503
- * .process('"Hello," she said -- "it\'s nice."')
504
- *
505
- * // Output HTML will have smart quotes and em-dashes
506
- * ```
507
- */
508
354
  export function rehypePunctilio(
509
355
  options: RehypePunctilioOptions = {}
510
356
  ): Transformer<Root, Root> {
package/src/remark.ts CHANGED
@@ -1,13 +1,4 @@
1
- /**
2
- * Remark plugin for applying punctilio typography transformations to Markdown.
3
- *
4
- * This plugin integrates punctilio with the unified/remark ecosystem,
5
- * allowing you to apply smart typography transformations to Markdown ASTs.
6
- *
7
- * @packageDocumentation
8
- */
9
-
10
- import type { Root, PhrasingContent, Text } from "mdast"
1
+ import type { PhrasingContent, Root, Text } from "mdast"
11
2
  import type { Transformer } from "unified"
12
3
 
13
4
  import { visitParents } from "unist-util-visit-parents"
@@ -18,21 +9,10 @@ import { transformTextNodes } from "./utils.js"
18
9
 
19
10
  export type RemarkPunctilioOptions = TransformOptions
20
11
 
21
- /**
22
- * MDAST node types that contain phrasing (inline) content and should
23
- * have their text transformed as a single unit.
24
- */
25
12
  const PHRASING_CONTAINERS = new Set(["paragraph", "heading", "tableCell"])
26
13
 
27
- /**
28
- * MDAST node types whose text content should not be transformed.
29
- */
30
14
  const SKIP_TYPES = new Set(["inlineCode", "html"])
31
15
 
32
- /**
33
- * Recursively collects text nodes from a phrasing content tree,
34
- * skipping code and HTML nodes.
35
- */
36
16
  function flattenTextNodes(
37
17
  node: PhrasingContent,
38
18
  depth: number = 0
@@ -59,28 +39,6 @@ function flattenTextNodes(
59
39
  return []
60
40
  }
61
41
 
62
- /**
63
- * Remark plugin that applies punctilio typography transformations to Markdown.
64
- *
65
- * @example
66
- * ```ts
67
- * import { unified } from 'unified'
68
- * import remarkParse from 'remark-parse'
69
- * import remarkStringify from 'remark-stringify'
70
- * import remarkPunctilio from 'punctilio/remark'
71
- *
72
- * const result = await unified()
73
- * .use(remarkParse)
74
- * .use(remarkPunctilio, {
75
- * punctuationStyle: 'american',
76
- * dashStyle: 'american',
77
- * })
78
- * .use(remarkStringify)
79
- * .process('"Hello," she said -- "it\'s nice."')
80
- *
81
- * // Output Markdown will have smart quotes and em-dashes
82
- * ```
83
- */
84
42
  export function remarkPunctilio(
85
43
  options: RemarkPunctilioOptions = {}
86
44
  ): Transformer<Root, Root> {
package/src/symbols.ts CHANGED
@@ -1,8 +1,4 @@
1
- /**
2
- * Symbol transformations: ellipses, multiplication, math symbols, arrows.
3
- */
4
-
5
- import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR, LATIN_LETTERS, wordBoundaryEnd, SPACE_CHARS, cachedRegExp, getEscapedSeparator } from "./constants.js"
1
+ import { cachedRegExp, DEFAULT_SEPARATOR, getEscapedSeparator, LATIN_LETTERS, SPACE_CHARS, UNICODE_SYMBOLS, wordBoundaryEnd } from "./constants.js"
6
2
  import { namedGroups } from "./utils.js"
7
3
 
8
4
  export interface SymbolOptions {
@@ -138,19 +134,10 @@ export function multiplication(text: string, options: SymbolOptions = {}): strin
138
134
  return text
139
135
  }
140
136
 
141
- /**
142
- * Builds a negative lookahead that must see through an optional separator
143
- * so e.g. `!<SEP>=` is correctly recognized as the start of `!==`.
144
- */
145
137
  type MathLookaheadBuilder = (escapedSeparator: string) => string
146
138
 
147
- /** [leftChars, rightChars, negativeLookaheadBuilder, replacement] */
148
139
  type MathSymbolRule = [string, string, MathLookaheadBuilder, string]
149
140
 
150
- /**
151
- * Math symbol replacement map.
152
- * The separator is inserted between left and right when building the regex.
153
- */
154
141
  const MATH_SYMBOL_MAP: MathSymbolRule[] = [
155
142
  ["!", "=", (chr) => `(?!${chr}?=)`, NOT_EQUAL],
156
143
  ["\\+/", "-", () => "", PLUS_MINUS],
@@ -178,18 +165,11 @@ export function mathSymbols(text: string, options: SymbolOptions = {}): string {
178
165
  return text
179
166
  }
180
167
 
181
- /** Predicate that decides whether a legal symbol should be converted based on surrounding text. */
182
168
  type ContextPredicate = (before: string, after: string) => boolean
183
169
 
184
- /**
185
- * Context window, in characters, examined on each side of a legal-symbol
186
- * candidate. Large enough to fit a 4-digit year plus surrounding whitespace
187
- * or the word "copyright" plus a space, but small enough that the slice
188
- * stays cheap on long inputs.
189
- */
170
+ // 25 chars: fits "copyright " or a 4-digit year with padding, without making slicing expensive.
190
171
  const LEGAL_SYMBOL_CONTEXT_WINDOW = 25
191
172
 
192
- /** Returns true when the context window ends with a path-like fragment (slash + non-whitespace). */
193
173
  const isPathContext = (before: string): boolean => {
194
174
  const parts = before.split(/\s+/)
195
175
  const trailing = parts[parts.length - 1]
@@ -197,11 +177,6 @@ const isPathContext = (before: string): boolean => {
197
177
  return slashIdx >= 0 && slashIdx < trailing.length - 1
198
178
  }
199
179
 
200
- /**
201
- * Context-aware replacement for legal symbols like (c), (r), (tm).
202
- * Extracts surrounding text, strips separator characters from the context
203
- * windows, and delegates the convert/skip decision to a predicate.
204
- */
205
180
  function contextAwareLegalReplace(
206
181
  text: string,
207
182
  pattern: RegExp,
@@ -249,19 +224,9 @@ export function legalSymbols(text: string, options: SymbolOptions = {}): string
249
224
  return text
250
225
  }
251
226
 
252
- /**
253
- * Builds an arrow-shape regex fragment from the escaped separator pattern.
254
- * The separator may appear between the halves of a bidirectional `<->`.
255
- */
256
227
  type ArrowPatternBuilder = (escapedSeparator: string) => string
257
228
 
258
- /**
259
- * Arrow pattern map: shape builder → Unicode symbol.
260
- *
261
- * The leading `<` on the bidi pattern anchors its repeated-dash-run group
262
- * so it stays linear-time; the right/left arrows can't anchor that way, so
263
- * they only admit one optional separator at the un-anchored end.
264
- */
229
+ // Leading `<` on bidi anchors the dash-run group for linear time.
265
230
  const ARROW_RULES: readonly [ArrowPatternBuilder, string][] = [
266
231
  [(sep) => `<${sep}?-+(?:${sep}-+)*${sep}?>`, ARROW_LEFT_RIGHT],
267
232
  [(sep) => `-+${sep}?>`, ARROW_RIGHT],
@@ -292,7 +257,6 @@ export function arrows(text: string, options: SymbolOptions = {}): string {
292
257
  return text
293
258
  }
294
259
 
295
- /** Convert "20 C" or "20 F" to "20 °C" or "20 °F". Only matches uppercase C/F. */
296
260
  export function degrees(text: string, options: SymbolOptions = {}): string {
297
261
  const chr = getEscapedSeparator(options)
298
262
 
@@ -309,7 +273,7 @@ export function degrees(text: string, options: SymbolOptions = {}): string {
309
273
  }
310
274
 
311
275
  /**
312
- * Build a regex that matches all quote characters in priority order:
276
+ * Matches quote characters in priority order:
313
277
  * 1. Prime candidate: digit + quote + non-letter (e.g., 5', 12")
314
278
  * 2. Contraction: letter + quote + letter (e.g., it's, don't, O'Brien)
315
279
  * 3. Trailing apostrophe: letter + quote + non-letter (e.g., dogs')
@@ -329,19 +293,9 @@ function buildQuoteClassificationPattern(
329
293
  )
330
294
  }
331
295
 
332
- /**
333
- * Replace callback that converts prime candidates while respecting quote balance.
334
- * Contractions and trailing apostrophes are recognized so they don't poison the
335
- * balance tracker. A prime candidate converts only when no unmatched opening
336
- * quote precedes it; otherwise it acts as a closing quote.
337
- *
338
- * For double primes (″): since ' and " are processed in separate passes,
339
- * 5'10" becomes 5′10" after the single-quote pass. Inside balanced double
340
- * quotes (e.g., "He is 5′10" tall"), the balance tracker would normally treat
341
- * the " after 10 as a closing quote. To handle this, we check whether the "
342
- * is preceded by ′ + digits — if so, it's inches notation and converts
343
- * regardless of balance.
344
- */
296
+ // Converts prime candidates while tracking quote balance. A prime after a
297
+ // digit converts only when no unmatched opener precedes it. Inside balanced
298
+ // quotes, a ′+digits context (feet-inches like 5′10") still converts.
345
299
  function balancedPrimeReplacer(primeChar: string, escapedSeparator: string) {
346
300
  // Pre-compile the feet-inches context pattern for the double-prime pass.
347
301
  // Matches when the text before a prime candidate ends with ′ followed by
@@ -421,13 +375,8 @@ export function primeMarks(text: string, options: SymbolOptions = {}): string {
421
375
  return text
422
376
  }
423
377
 
424
- /** [numerator, denominator, unicodeChar] */
425
378
  type FractionRule = [string, string, string]
426
379
 
427
- /**
428
- * Supported Unicode fractions.
429
- * The regex alternation and lookup map are both derived from this list.
430
- */
431
380
  const FRACTION_TUPLES: FractionRule[] = [
432
381
  ["1", "4", UNICODE_SYMBOLS.FRACTION_1_4],
433
382
  ["1", "2", UNICODE_SYMBOLS.FRACTION_1_2],
@@ -504,7 +453,7 @@ export function superscriptOrdinal(text: string, options: SymbolOptions = {}): s
504
453
  })
505
454
  }
506
455
 
507
- /** Collapse multiple spaces (including tabs) to single space. Preserves the highest-priority space type present in the run: NBSP > NNBSP > regular. Leading whitespace at the start of a line (after `\n` or start-of-string) is preserved so indented blocks (e.g. HN-style code) survive. */
456
+ // Preserves highest-priority space type (NBSP > NNBSP > regular) and leading indentation.
508
457
  export function collapseSpaces(text: string): string {
509
458
  return text.replace(cachedRegExp(`(?<=[^\\n${SPACE_CHARS}])[${SPACE_CHARS}]{2,}`, "g"), (match) => {
510
459
  if (match.includes(NBSP)) return NBSP
@@ -513,14 +462,9 @@ export function collapseSpaces(text: string): string {
513
462
  })
514
463
  }
515
464
 
516
- /** [firstChar, repeatedChar, replacement] */
517
465
  type LigatureRule = [string, string, string]
518
466
 
519
- /**
520
- * Punctuation ligature map.
521
- * Pattern: first(sep)?repeated(?:sep?repeated)* → replacement
522
- * Order matters: handle mixed punctuation first, then repeated
523
- */
467
+ // Order matters: mixed punctuation first, then repeated.
524
468
  const PUNCTUATION_LIGATURE_MAP: LigatureRule[] = [
525
469
  ["\\?", "!", QUESTION_EXCLAMATION], // ?!+ → ⁈
526
470
  ["!", "\\?", EXCLAMATION_QUESTION], // !?+ → ⁉
@@ -528,7 +472,7 @@ const PUNCTUATION_LIGATURE_MAP: LigatureRule[] = [
528
472
  ["!", "!", "!"], // !!+ → ! (normalize)
529
473
  ]
530
474
 
531
- /** Convert ?? to ⁇, ?! to ⁈, !? to ⁉. Poor font support, disabled by default. */
475
+ /** Convert ?? to ⁇, ?! to ⁈, !? to ⁉. Disabled by default (poor font support). */
532
476
  export function punctuationLigatures(text: string, options: SymbolOptions = {}): string {
533
477
  const chr = getEscapedSeparator(options)
534
478
  const sepPattern = cachedRegExp(chr, "g")
@@ -548,7 +492,6 @@ export function punctuationLigatures(text: string, options: SymbolOptions = {}):
548
492
  return text
549
493
  }
550
494
 
551
- /** Apply all symbol transforms. degrees/fractions excluded (too aggressive). */
552
495
  export function symbolTransform(text: string, options: SymbolOptions = {}): string {
553
496
  text = ellipsis(text, options)
554
497
  text = multiplication(text, options)
package/src/utils.ts CHANGED
@@ -1,20 +1,10 @@
1
- /**
2
- * Utility functions for text transformation validation.
3
- *
4
- * @module utils
5
- */
6
-
7
1
  import { DEFAULT_SEPARATOR, ISSUES_URL } from "./constants.js"
8
2
 
9
- /** Threshold above which strings are truncated in error messages. */
10
3
  const ERROR_STRING_THRESHOLD = 2000
11
4
 
12
5
  /**
13
- * Formats a string for error messages. If the string exceeds the threshold,
14
- * truncates it and shows the total length.
15
- *
16
- * In Node.js environments, also writes the full content to stderr
17
- * so it's available in build logs for debugging.
6
+ * Formats a string for error messages, truncating above the threshold.
7
+ * Writes full content to stderr in Node.js so build logs capture it.
18
8
  */
19
9
  export function formatErrorString(content: string, label: string): string {
20
10
  if (content.length <= ERROR_STRING_THRESHOLD) {
@@ -36,12 +26,7 @@ export function formatErrorString(content: string, label: string): string {
36
26
  return `[${label}: ${JSON.stringify(truncated)}... (${content.length} chars total)]`
37
27
  }
38
28
 
39
- /**
40
- * Throws if any text node contains the separator character, which would
41
- * corrupt the split/join mechanism used by the rehype and remark plugins.
42
- *
43
- * @throws Error if the separator is found in any text value
44
- */
29
+ /** Throws if any text node contains the separator (would corrupt split/join). */
45
30
  export function assertSeparatorAbsent(textValues: string[], separator: string): void {
46
31
  for (const value of textValues) {
47
32
  if (!value.includes(separator)) continue
@@ -57,9 +42,6 @@ export function assertSeparatorAbsent(textValues: string[], separator: string):
57
42
  }
58
43
  }
59
44
 
60
- /**
61
- * Counts non-overlapping occurrences of the separator string in the text.
62
- */
63
45
  export function countSeparators(text: string, separator: string = DEFAULT_SEPARATOR): number {
64
46
  if (separator.length === 0) return 0
65
47
  let count = 0
@@ -71,24 +53,15 @@ export function countSeparators(text: string, separator: string = DEFAULT_SEPARA
71
53
  return count
72
54
  }
73
55
 
74
- /** Minimal text-node interface shared by mdast.Text and hast.Text. */
75
56
  interface TextNode {
76
57
  value: string
77
58
  }
78
59
 
79
60
  /**
80
61
  * Applies a text transformation across an array of text nodes using the
81
- * separator-marking technique.
82
- *
83
- * 1. Validates no text node already contains the separator.
84
- * 2. Appends the separator to each node's value, concatenates into one string.
85
- * 3. Runs the transform function on the concatenated string.
86
- * 4. Splits the result back on the separator and writes each fragment
87
- * back into the corresponding text node.
88
- *
89
- * `textNodes` is mutated in place.
90
- *
91
- * @throws Error if the transformation alters the number of text nodes
62
+ * separator-marking technique: appends the separator to each node's value,
63
+ * concatenates, transforms, then splits back on the separator and writes
64
+ * each fragment into the corresponding text node. Mutates `textNodes`.
92
65
  */
93
66
  export function transformTextNodes(
94
67
  textNodes: TextNode[],
@@ -115,12 +88,7 @@ export function transformTextNodes(
115
88
  })
116
89
  }
117
90
 
118
- /**
119
- * Validates that a transformation preserved the separator count.
120
- * Throws an error if separators were added or removed.
121
- *
122
- * @throws Error if separator count changed
123
- */
91
+ /** Throws if a transformation changed the separator count. */
124
92
  export function assertSeparatorCountPreserved(
125
93
  original: string,
126
94
  transformed: string,
@@ -138,12 +106,7 @@ export function assertSeparatorCountPreserved(
138
106
  }
139
107
  }
140
108
 
141
- /**
142
- * Returns a copy of `obj` with `undefined`-valued keys removed.
143
- * Used so caller-supplied `{ foo: undefined }` doesn't override a default.
144
- *
145
- * @internal
146
- */
109
+ /** Returns a copy of `obj` with `undefined`-valued keys removed. @internal */
147
110
  export function filterUndefined<T extends object>(obj: T): Partial<T> {
148
111
  const result: Record<string, unknown> = {}
149
112
  for (const [k, v] of Object.entries(obj)) {
@@ -152,13 +115,7 @@ export function filterUndefined<T extends object>(obj: T): Partial<T> {
152
115
  return result as Partial<T>
153
116
  }
154
117
 
155
- /**
156
- * Returns a deterministic JSON-style string for an options object: drops
157
- * `undefined` values and sorts keys, so equivalent option sets always
158
- * produce the same string (suitable as a cache or LRU key).
159
- *
160
- * @internal
161
- */
118
+ /** Deterministic JSON string for an options object, suitable as cache key. @internal */
162
119
  export function stableStringify(obj: object): string {
163
120
  const entries = Object.entries(obj)
164
121
  .filter(([, v]) => v !== undefined)
@@ -166,14 +123,7 @@ export function stableStringify(obj: object): string {
166
123
  return JSON.stringify(entries)
167
124
  }
168
125
 
169
- /**
170
- * Extracts the named-groups object from a `String.prototype.replace`
171
- * callback's argument tuple. The last element is always the groups
172
- * object when the regex has named captures. Callers supply the group
173
- * shape via the type parameter to localise the unavoidable type cast.
174
- *
175
- * @internal
176
- */
126
+ /** Extracts named groups from a `.replace()` callback's arguments. @internal */
177
127
  export function namedGroups<G>(args: unknown[]): G {
178
128
  return args[args.length - 1] as G
179
129
  }