punctilio 4.3.1 → 5.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.
Files changed (65) hide show
  1. package/README.md +36 -22
  2. package/dist/constants.d.ts +0 -27
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +0 -40
  5. package/dist/constants.js.map +1 -1
  6. package/dist/dashes.d.ts +11 -7
  7. package/dist/dashes.d.ts.map +1 -1
  8. package/dist/dashes.js +423 -284
  9. package/dist/dashes.js.map +1 -1
  10. package/dist/index.d.ts +14 -118
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +65 -116
  13. package/dist/index.js.map +1 -1
  14. package/dist/markdown.js +1 -1
  15. package/dist/markdown.js.map +1 -1
  16. package/dist/nbsp.d.ts +19 -13
  17. package/dist/nbsp.d.ts.map +1 -1
  18. package/dist/nbsp.js +214 -69
  19. package/dist/nbsp.js.map +1 -1
  20. package/dist/prose-view.d.ts +48 -7
  21. package/dist/prose-view.d.ts.map +1 -1
  22. package/dist/prose-view.js +163 -27
  23. package/dist/prose-view.js.map +1 -1
  24. package/dist/quote-classifier.d.ts +5 -4
  25. package/dist/quote-classifier.d.ts.map +1 -1
  26. package/dist/quote-classifier.js +601 -88
  27. package/dist/quote-classifier.js.map +1 -1
  28. package/dist/quotes.d.ts +12 -4
  29. package/dist/quotes.d.ts.map +1 -1
  30. package/dist/quotes.js +24 -27
  31. package/dist/quotes.js.map +1 -1
  32. package/dist/rehype.d.ts +53 -8
  33. package/dist/rehype.d.ts.map +1 -1
  34. package/dist/rehype.js +105 -62
  35. package/dist/rehype.js.map +1 -1
  36. package/dist/remark.d.ts +1 -1
  37. package/dist/remark.d.ts.map +1 -1
  38. package/dist/remark.js +6 -9
  39. package/dist/remark.js.map +1 -1
  40. package/dist/symbols.d.ts +25 -14
  41. package/dist/symbols.d.ts.map +1 -1
  42. package/dist/symbols.js +812 -220
  43. package/dist/symbols.js.map +1 -1
  44. package/dist/transform-options.d.ts +97 -0
  45. package/dist/transform-options.d.ts.map +1 -0
  46. package/dist/transform-options.js +34 -0
  47. package/dist/transform-options.js.map +1 -0
  48. package/dist/utils.d.ts +0 -16
  49. package/dist/utils.d.ts.map +1 -1
  50. package/dist/utils.js +0 -56
  51. package/dist/utils.js.map +1 -1
  52. package/package.json +6 -2
  53. package/src/constants.ts +0 -46
  54. package/src/dashes.ts +450 -269
  55. package/src/index.ts +70 -291
  56. package/src/markdown.ts +1 -1
  57. package/src/nbsp.ts +255 -78
  58. package/src/prose-view.ts +234 -39
  59. package/src/quote-classifier.ts +618 -88
  60. package/src/quotes.ts +36 -19
  61. package/src/rehype.ts +169 -88
  62. package/src/remark.ts +6 -11
  63. package/src/symbols.ts +814 -251
  64. package/src/transform-options.ts +143 -0
  65. package/src/utils.ts +0 -82
@@ -0,0 +1,143 @@
1
+ import { DASH_STYLES, type DashStyle } from "./dashes.js"
2
+ import { PUNCTUATION_STYLES, type PunctuationStyle } from "./quotes.js"
3
+ import { assertKnownOptionKeys, filterUndefined } from "./utils.js"
4
+
5
+ export interface TransformOptions {
6
+ /**
7
+ * Whether to include symbol transforms (ellipsis, multiplication, etc.)
8
+ * Default: true
9
+ */
10
+ symbols?: boolean
11
+
12
+ /**
13
+ * Whether to collapse multiple consecutive spaces (including non-breaking
14
+ * spaces) into a single space. Keeps the first space in the sequence.
15
+ * Runs at the start of a line (after `\n` or start-of-string) are preserved
16
+ * so indented blocks (e.g. HN-style code) survive.
17
+ *
18
+ * - `true` (default): "hello world" → "hello world"; " indented" → " indented"
19
+ * - `false`: Preserve multiple spaces
20
+ *
21
+ * Default: true
22
+ */
23
+ collapseSpaces?: boolean
24
+
25
+ /**
26
+ * How to handle quotes and punctuation placement.
27
+ *
28
+ * - `"american"` (default): Chicago style. Converts straight quotes to smart
29
+ * quotes, converts prime marks, and places periods/commas inside quotes.
30
+ * Example: "Hello." and "Hello,"
31
+ * - `"british"`: Oxford style. Converts straight quotes to smart quotes,
32
+ * converts prime marks, and places periods/commas outside quotes.
33
+ * Example: "Hello". and "Hello",
34
+ * - `"german"`: German style. Uses low-9 quote characters: „..." and ‚...'
35
+ * (U+201E/U+201C double, U+201A/U+2018 single). Punctuation outside quotes.
36
+ * - `"french"`: French style. Uses guillemets with NNBSP padding: «\u202F...\u202F»
37
+ * Single quotes remain as curly quotes. Punctuation outside quotes.
38
+ * - `"none"`: Skip all quote and punctuation transforms entirely.
39
+ * Straight quotes, apostrophes, and prime marks are left unmodified.
40
+ *
41
+ * Default: "american"
42
+ */
43
+ punctuationStyle?: PunctuationStyle
44
+
45
+ /**
46
+ * How to style dashes.
47
+ *
48
+ * - `"american"` (default): Chicago style. Converts parenthetical dashes to
49
+ * unspaced em dashes (word—word), number ranges to en dashes (1–5),
50
+ * date ranges to en dashes (January–March), and hyphens to minus signs (−5).
51
+ * - `"british"`: Oxford style. Converts parenthetical dashes to spaced en
52
+ * dashes (word – word), with the same number range, date range, and minus
53
+ * sign conversions.
54
+ * - `"none"`: Skip all dash transforms entirely. Hyphens, number ranges,
55
+ * date ranges, and minus signs are left unmodified.
56
+ *
57
+ * Default: "american"
58
+ */
59
+ dashStyle?: DashStyle
60
+
61
+ /**
62
+ * Whether to include fraction transforms (1/2 → ½)
63
+ * Default: false (can be aggressive)
64
+ */
65
+ fractions?: boolean
66
+
67
+ /**
68
+ * Whether to include degree symbol transforms (20 C → 20 °C)
69
+ * Default: false (can be aggressive)
70
+ */
71
+ degrees?: boolean
72
+
73
+ /**
74
+ * Whether to convert ordinal suffixes to Unicode superscript characters.
75
+ * Transforms numbers like "1st", "2nd", "3rd", "4th" to "1ˢᵗ", "2ⁿᵈ", "3ʳᵈ", "4ᵗʰ".
76
+ * Default: false
77
+ */
78
+ superscript?: boolean
79
+
80
+ /**
81
+ * Whether to convert repeated punctuation marks to Unicode ligature characters.
82
+ * Squashes multiple marks: "???" → "⁇", "?!" → "⁈", "!?" → "⁉", "!!!" → "!"
83
+ * Default: false (poor font support)
84
+ */
85
+ ligatures?: boolean
86
+
87
+ /**
88
+ * Whether to include arrow transforms (-> → →, <- → ←, <-> → ↔).
89
+ * Only applies when `symbols` is true.
90
+ * Default: true
91
+ */
92
+ includeArrows?: boolean
93
+
94
+ /**
95
+ * Whether to insert non-breaking spaces in typographically appropriate
96
+ * locations (after short words, between numbers and units, before
97
+ * last words to prevent widows, after honorifics, etc.).
98
+ * Default: true
99
+ */
100
+ nbsp?: boolean
101
+ }
102
+
103
+ const defaultOpts: Required<TransformOptions> = {
104
+ symbols: true,
105
+ includeArrows: true,
106
+ fractions: false,
107
+ degrees: false,
108
+ superscript: false,
109
+ ligatures: false,
110
+ nbsp: true,
111
+ collapseSpaces: true,
112
+ punctuationStyle: "american",
113
+ dashStyle: "american",
114
+ }
115
+
116
+ /** Runtime list of valid `transform()` option keys, derived from the option
117
+ * defaults so it cannot drift from {@link TransformOptions}. */
118
+ export const TRANSFORM_OPTION_KEYS: readonly string[] = Object.keys(defaultOpts)
119
+
120
+ export type ResolvedTransformOptions = Required<TransformOptions>
121
+
122
+ /** Validates `options` and fills in the defaults. */
123
+ export function resolveTransformOptions(options: TransformOptions): ResolvedTransformOptions {
124
+ assertKnownOptionKeys(options, TRANSFORM_OPTION_KEYS, "transform")
125
+
126
+ const punctuationStyle = options.punctuationStyle ?? "american"
127
+ if (!PUNCTUATION_STYLES.includes(punctuationStyle as typeof PUNCTUATION_STYLES[number])) {
128
+ throw new Error(
129
+ `Invalid punctuationStyle: "${punctuationStyle}". ` +
130
+ `Must be one of: ${PUNCTUATION_STYLES.join(", ")}.`
131
+ )
132
+ }
133
+
134
+ const dashStyle = options.dashStyle ?? "american"
135
+ if (!DASH_STYLES.includes(dashStyle as typeof DASH_STYLES[number])) {
136
+ throw new Error(
137
+ `Invalid dashStyle: "${dashStyle}". ` +
138
+ `Must be one of: ${DASH_STYLES.join(", ")}.`
139
+ )
140
+ }
141
+
142
+ return { ...defaultOpts, ...filterUndefined(options) }
143
+ }
package/src/utils.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { DEFAULT_SEPARATOR, ISSUES_URL } from "./constants.js"
2
-
3
1
  const ERROR_STRING_THRESHOLD = 2000
4
2
 
5
3
  /**
@@ -28,86 +26,6 @@ export function formatErrorString(content: string, label: string): string {
28
26
  return `[${label}: ${JSON.stringify(truncated)}... (${content.length} chars total)]`
29
27
  }
30
28
 
31
- /** Throws if any text node contains the separator (would corrupt split/join). */
32
- export function assertSeparatorAbsent(textValues: string[], separator: string): void {
33
- for (const value of textValues) {
34
- if (!value.includes(separator)) continue
35
-
36
- const codePoints = [...separator]
37
- .map(ch => `U+${ch.codePointAt(0)!.toString(16).toUpperCase().padStart(4, "0")}`)
38
- .join(" ")
39
- throw new Error(
40
- `Text contains the separator sequence ${codePoints} which is used internally by punctilio ` +
41
- `to track element boundaries. Pass a different separator via the "separator" option.\n` +
42
- `Text: ${formatErrorString(value, "input")}`
43
- )
44
- }
45
- }
46
-
47
- export function countSeparators(text: string, separator: string = DEFAULT_SEPARATOR): number {
48
- if (separator.length === 0) return 0
49
- let count = 0
50
- let index = 0
51
- while ((index = text.indexOf(separator, index)) !== -1) {
52
- count++
53
- index += separator.length
54
- }
55
- return count
56
- }
57
-
58
- interface TextNode {
59
- value: string
60
- }
61
-
62
- /**
63
- * Applies a text transformation across an array of text nodes using the
64
- * separator-marking technique: appends the separator to each node's value,
65
- * concatenates, transforms, then splits back on the separator and writes
66
- * each fragment into the corresponding text node. Mutates `textNodes`.
67
- */
68
- export function transformTextNodes(
69
- textNodes: TextNode[],
70
- transformFn: (input: string) => string,
71
- separator: string,
72
- ): void {
73
- assertSeparatorAbsent(textNodes.map((n) => n.value), separator)
74
-
75
- const markedContent = textNodes.map((n) => n.value + separator).join("")
76
- const transformedContent = transformFn(markedContent)
77
- const transformedFragments = transformedContent.split(separator).slice(0, -1)
78
-
79
- /* istanbul ignore if -- defensive: transform should never consume separator chars */
80
- if (transformedFragments.length !== textNodes.length) {
81
- throw new Error(
82
- `Transformation altered the number of text nodes. ` +
83
- `Expected ${textNodes.length}, got ${transformedFragments.length}. ` +
84
- `Input: ${formatErrorString(markedContent, "input")}`
85
- )
86
- }
87
-
88
- textNodes.forEach((n, index) => {
89
- n.value = transformedFragments[index]
90
- })
91
- }
92
-
93
- /** Throws if a transformation changed the separator count. */
94
- export function assertSeparatorCountPreserved(
95
- original: string,
96
- transformed: string,
97
- separator: string = DEFAULT_SEPARATOR,
98
- transformName: string = "transform"
99
- ): void {
100
- const originalCount = countSeparators(original, separator)
101
- const transformedCount = countSeparators(transformed, separator)
102
- if (originalCount !== transformedCount) {
103
- throw new Error(
104
- `${transformName} altered separator count: expected ${originalCount}, got ${transformedCount}.\n` +
105
- `This is a bug in punctilio. Please file an issue at ${ISSUES_URL}\n` +
106
- `Include the input text that caused this error.`
107
- )
108
- }
109
- }
110
-
111
29
  /** Throws if `options` contains a key not present in `validKeys`. @internal */
112
30
  export function assertKnownOptionKeys(
113
31
  options: object,