punctilio 4.3.1 → 5.0.1
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 +37 -23
- package/dist/constants.d.ts +0 -27
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -40
- package/dist/constants.js.map +1 -1
- package/dist/dashes.d.ts +11 -7
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +423 -284
- package/dist/dashes.js.map +1 -1
- package/dist/index.d.ts +14 -118
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -116
- package/dist/index.js.map +1 -1
- package/dist/markdown.js +1 -1
- package/dist/markdown.js.map +1 -1
- package/dist/nbsp.d.ts +19 -13
- package/dist/nbsp.d.ts.map +1 -1
- package/dist/nbsp.js +214 -69
- package/dist/nbsp.js.map +1 -1
- package/dist/prose-view.d.ts +48 -7
- package/dist/prose-view.d.ts.map +1 -1
- package/dist/prose-view.js +163 -27
- package/dist/prose-view.js.map +1 -1
- package/dist/quote-classifier.d.ts +5 -4
- package/dist/quote-classifier.d.ts.map +1 -1
- package/dist/quote-classifier.js +601 -88
- package/dist/quote-classifier.js.map +1 -1
- package/dist/quotes.d.ts +12 -4
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +24 -27
- package/dist/quotes.js.map +1 -1
- package/dist/rehype.d.ts +53 -8
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +105 -62
- package/dist/rehype.js.map +1 -1
- package/dist/remark.d.ts +1 -1
- package/dist/remark.d.ts.map +1 -1
- package/dist/remark.js +6 -9
- package/dist/remark.js.map +1 -1
- package/dist/symbols.d.ts +25 -14
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +812 -220
- package/dist/symbols.js.map +1 -1
- package/dist/transform-options.d.ts +97 -0
- package/dist/transform-options.d.ts.map +1 -0
- package/dist/transform-options.js +34 -0
- package/dist/transform-options.js.map +1 -0
- package/dist/utils.d.ts +0 -16
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +0 -56
- package/dist/utils.js.map +1 -1
- package/package.json +8 -3
- package/src/constants.ts +0 -46
- package/src/dashes.ts +450 -269
- package/src/index.ts +70 -291
- package/src/markdown.ts +1 -1
- package/src/nbsp.ts +255 -78
- package/src/prose-view.ts +234 -39
- package/src/quote-classifier.ts +618 -88
- package/src/quotes.ts +36 -19
- package/src/rehype.ts +169 -88
- package/src/remark.ts +6 -11
- package/src/symbols.ts +814 -251
- package/src/transform-options.ts +143 -0
- 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,
|