punctilio 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alexander Turner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # punctilio
2
+
3
+ > *punctilio* (n.): a fine point of conduct or procedure
4
+
5
+ Smart typography transformations for JavaScript/TypeScript. Converts ASCII punctuation to typographically correct Unicode characters. Originally built for [my personal website](https://turntrout.com/design).
6
+
7
+ ## Features
8
+
9
+ - **Smart quotes**: `"straight"` → `"curly"` and `'apostrophes'` → `'apostrophes'`
10
+ - **Em dashes**: `word - word` or `word--word` → `word—word`
11
+ - **En dashes**: `1-5` → `1–5` (number ranges), `January-March` → `January–March` (date ranges)
12
+ - **Minus signs**: `-5` → `−5` (proper Unicode minus)
13
+ - **Handles edge cases**: contractions, possessives, nested quotes, year abbreviations ('99), "rock 'n' roll"
14
+
15
+ ## Why another typography library?
16
+
17
+ Existing solutions like [SmartyPants](https://daringfireball.net/projects/smartypants/) struggle with:
18
+
19
+ - **Apostrophe ambiguity**: Is `'Twas` an opening quote or apostrophe? (It's an apostrophe)
20
+ - **Cross-element text**: When quotes span `<em>"Hello</em> world"`, most libraries fail
21
+ - **Context sensitivity**: `'99` (year) vs `'hello'` (quoted) vs `don't` (contraction)
22
+
23
+ `punctilio` handles these through thorough regex patterns and an optional separator character for processing text that spans HTML elements.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install punctilio
29
+ # or
30
+ pnpm add punctilio
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Basic
36
+
37
+ ```typescript
38
+ import { transform, niceQuotes, hyphenReplace } from 'punctilio'
39
+
40
+ // Apply all transformations
41
+ transform('"Hello," she said - "it\'s pages 1-5."')
42
+ // → "Hello," she said—"it's pages 1–5."
43
+
44
+ // Or use individual functions
45
+ niceQuotes('"Hello", she said.')
46
+ // → "Hello", she said.
47
+
48
+ hyphenReplace('word - word')
49
+ // → word—word
50
+ ```
51
+
52
+ ### With HTML Element Boundaries
53
+
54
+ When processing text that spans multiple HTML elements, use a separator character to mark boundaries:
55
+
56
+ ```typescript
57
+ import { transform, DEFAULT_SEPARATOR } from 'punctilio'
58
+
59
+ // Your HTML: <p>"Hello <em>world</em>"</p>
60
+ // Extract text with separator between elements:
61
+ const text = `"Hello ${DEFAULT_SEPARATOR}world${DEFAULT_SEPARATOR}"`
62
+
63
+ const result = transform(text, { separator: DEFAULT_SEPARATOR })
64
+ // → "Hello \uE000world\uE000"
65
+ // The separator is preserved; split on it to restore to your elements
66
+ ```
67
+
68
+ For a complete implementation showing how to use this with a HAST (HTML AST) tree, see the [`transformElement` function in TurnTrout.com](https://github.com/alexander-turner/TurnTrout.com/blob/main/quartz/plugins/transformers/formatting_improvement_html.ts). I explain the philosophy behind this algorithm.
69
+
70
+ ## API
71
+
72
+ ### `transform(text, options?)`
73
+
74
+ Applies all typography transformations (quotes + dashes).
75
+
76
+ ### `niceQuotes(text, options?)`
77
+
78
+ Converts straight quotes to curly quotes. Handles:
79
+ - Opening/closing double quotes: `"` → `"` or `"`
80
+ - Opening/closing single quotes: `'` → `'` or `'`
81
+ - Contractions: `don't` → `don't`
82
+ - Possessives: `dog's` → `dog's`
83
+ - Year abbreviations: `'99` → `'99`
84
+ - Special cases: `'n'` in "rock 'n' roll"
85
+
86
+ ### `hyphenReplace(text, options?)`
87
+
88
+ Converts hyphens to proper dashes. Handles:
89
+ - Em dashes: `word - word` → `word—word`
90
+ - En dashes for number ranges: `1-5` → `1–5`
91
+ - En dashes for date ranges: `Jan-Mar` → `Jan–Mar`
92
+ - Minus signs: `-5` → `−5`
93
+ - Preserves: horizontal rules (`---`), compound words (`well-known`)
94
+
95
+ ### `enDashNumberRange(text, options?)`
96
+
97
+ Converts number ranges only: `pages 10-20` → `pages 10–20`
98
+
99
+ ### `enDashDateRange(text, options?)`
100
+
101
+ Converts month ranges only: `January-March` → `January–March`
102
+
103
+ ### `minusReplace(text, options?)`
104
+
105
+ Converts hyphens to minus signs in numerical contexts: `-5` → `−5`
106
+
107
+ ## License
108
+
109
+ MIT © Alexander Turner
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Dash and hyphen transformation
3
+ *
4
+ * Converts hyphens and dashes to typographically correct em-dashes,
5
+ * en-dashes, and minus signs based on context.
6
+ */
7
+ export interface DashOptions {
8
+ /**
9
+ * A boundary marker character used when transforming text that spans
10
+ * multiple HTML elements. This character is treated as "transparent"
11
+ * in the regex patterns.
12
+ *
13
+ * Should be a character that doesn't appear in your text.
14
+ * Default: "\uE000" (Unicode Private Use Area)
15
+ */
16
+ separator?: string;
17
+ }
18
+ /**
19
+ * List of month names (full and abbreviated) for date range detection
20
+ */
21
+ export declare const months: string;
22
+ /**
23
+ * Replaces hyphens with en-dashes in number ranges.
24
+ *
25
+ * Handles:
26
+ * - Simple ranges: "1-5" → "1–5"
27
+ * - Page numbers: "p.206-207" → "p.206–207"
28
+ * - Dollar amounts: "$100-$200" → "$100–$200"
29
+ * - Comma-formatted numbers: "1,000-2,000" → "1,000–2,000"
30
+ *
31
+ * Does NOT replace:
32
+ * - Spaced ranges: "1 - 5" (ambiguous, could be subtraction)
33
+ * - Version numbers with decimals: "1.5-1.8"
34
+ *
35
+ * @param text - The text to transform
36
+ * @param options - Configuration options
37
+ * @returns The text with en-dashes in number ranges
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * enDashNumberRange("pages 10-15")
42
+ * // → "pages 10–15"
43
+ * ```
44
+ */
45
+ export declare function enDashNumberRange(text: string, options?: DashOptions): string;
46
+ /**
47
+ * Replaces hyphens with en-dashes in month/date ranges.
48
+ *
49
+ * Handles full and abbreviated month names:
50
+ * - "January-March" → "January–March"
51
+ * - "Jan-Mar" → "Jan–Mar"
52
+ *
53
+ * @param text - The text to transform
54
+ * @param options - Configuration options
55
+ * @returns The text with en-dashes in date ranges
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * enDashDateRange("January-March 2024")
60
+ * // → "January–March 2024"
61
+ * ```
62
+ */
63
+ export declare function enDashDateRange(text: string, options?: DashOptions): string;
64
+ /**
65
+ * Replaces hyphens with proper minus signs (−) in numerical contexts.
66
+ *
67
+ * Handles negative numbers at:
68
+ * - Start of string/line
69
+ * - After whitespace
70
+ * - After opening parenthesis
71
+ * - After opening quote
72
+ *
73
+ * @param text - The text to transform
74
+ * @param options - Configuration options
75
+ * @returns The text with minus signs
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * minusReplace("The temperature was -5 degrees")
80
+ * // → "The temperature was −5 degrees"
81
+ * ```
82
+ */
83
+ export declare function minusReplace(text: string, options?: DashOptions): string;
84
+ /**
85
+ * Comprehensive dash replacement for typographic correctness.
86
+ *
87
+ * Applies multiple transformations:
88
+ * 1. Converts hyphens to minus signs in numerical contexts
89
+ * 2. Converts surrounded dashes (- or --) to em-dashes (—)
90
+ * 3. Removes spaces around em-dashes (per modern style)
91
+ * 4. Preserves space after em-dash at start of line
92
+ * 5. Adds space after em-dash following quotation marks
93
+ * 6. Converts number ranges to en-dashes (1-5 → 1–5)
94
+ * 7. Converts date ranges to en-dashes (Jan-Mar → Jan–Mar)
95
+ *
96
+ * Does NOT modify:
97
+ * - Horizontal rules (---)
98
+ * - Compound modifiers (well-known, browser-specific)
99
+ * - Hyphens in quoted blockquotes ("> - item")
100
+ *
101
+ * @param text - The text to transform
102
+ * @param options - Configuration options
103
+ * @returns The text with proper dashes
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * hyphenReplace("This is a - test")
108
+ * // → "This is a—test"
109
+ *
110
+ * hyphenReplace("Since--as you know")
111
+ * // → "Since—as you know"
112
+ *
113
+ * hyphenReplace("Pages 1-5")
114
+ * // → "Pages 1–5"
115
+ * ```
116
+ */
117
+ export declare function hyphenReplace(text: string, options?: DashOptions): string;
118
+ //# sourceMappingURL=dashes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashes.d.ts","sourceRoot":"","sources":["../src/dashes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,WAAW;IAC1B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAID;;GAEG;AACH,eAAO,MAAM,MAAM,QAyBR,CAAA;AAEX;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CASjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CAM/E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CAI5E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CAgD7E"}
package/dist/dashes.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Dash and hyphen transformation
3
+ *
4
+ * Converts hyphens and dashes to typographically correct em-dashes,
5
+ * en-dashes, and minus signs based on context.
6
+ */
7
+ const DEFAULT_SEPARATOR = "\uE000";
8
+ /**
9
+ * List of month names (full and abbreviated) for date range detection
10
+ */
11
+ export const months = [
12
+ "January",
13
+ "February",
14
+ "March",
15
+ "April",
16
+ "May",
17
+ "June",
18
+ "July",
19
+ "August",
20
+ "September",
21
+ "October",
22
+ "November",
23
+ "December",
24
+ "Jan",
25
+ "Feb",
26
+ "Mar",
27
+ "Apr",
28
+ "May",
29
+ "Jun",
30
+ "Jul",
31
+ "Aug",
32
+ "Sep",
33
+ "Oct",
34
+ "Nov",
35
+ "Dec",
36
+ ].join("|");
37
+ /**
38
+ * Replaces hyphens with en-dashes in number ranges.
39
+ *
40
+ * Handles:
41
+ * - Simple ranges: "1-5" → "1–5"
42
+ * - Page numbers: "p.206-207" → "p.206–207"
43
+ * - Dollar amounts: "$100-$200" → "$100–$200"
44
+ * - Comma-formatted numbers: "1,000-2,000" → "1,000–2,000"
45
+ *
46
+ * Does NOT replace:
47
+ * - Spaced ranges: "1 - 5" (ambiguous, could be subtraction)
48
+ * - Version numbers with decimals: "1.5-1.8"
49
+ *
50
+ * @param text - The text to transform
51
+ * @param options - Configuration options
52
+ * @returns The text with en-dashes in number ranges
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * enDashNumberRange("pages 10-15")
57
+ * // → "pages 10–15"
58
+ * ```
59
+ */
60
+ export function enDashNumberRange(text, options = {}) {
61
+ const chr = options.separator ?? DEFAULT_SEPARATOR;
62
+ return text.replace(new RegExp(`\\b(?<![a-zA-Z.])((?:p\\.?|\\$)?\\d[\\d.,]*${chr}?)-(${chr}?\\$?\\d[\\d.,]*)(?!\\.\\d)\\b`, "g"), "$1–$2");
63
+ }
64
+ /**
65
+ * Replaces hyphens with en-dashes in month/date ranges.
66
+ *
67
+ * Handles full and abbreviated month names:
68
+ * - "January-March" → "January–March"
69
+ * - "Jan-Mar" → "Jan–Mar"
70
+ *
71
+ * @param text - The text to transform
72
+ * @param options - Configuration options
73
+ * @returns The text with en-dashes in date ranges
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * enDashDateRange("January-March 2024")
78
+ * // → "January–March 2024"
79
+ * ```
80
+ */
81
+ export function enDashDateRange(text, options = {}) {
82
+ const chr = options.separator ?? DEFAULT_SEPARATOR;
83
+ return text.replace(new RegExp(`\\b(${months}${chr}?)-(${chr}?(?:${months}))\\b`, "g"), "$1–$2");
84
+ }
85
+ /**
86
+ * Replaces hyphens with proper minus signs (−) in numerical contexts.
87
+ *
88
+ * Handles negative numbers at:
89
+ * - Start of string/line
90
+ * - After whitespace
91
+ * - After opening parenthesis
92
+ * - After opening quote
93
+ *
94
+ * @param text - The text to transform
95
+ * @param options - Configuration options
96
+ * @returns The text with minus signs
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * minusReplace("The temperature was -5 degrees")
101
+ * // → "The temperature was −5 degrees"
102
+ * ```
103
+ */
104
+ export function minusReplace(text, options = {}) {
105
+ const chr = options.separator ?? DEFAULT_SEPARATOR;
106
+ const minusRegex = new RegExp(`(^|[\\s\\(${chr}""])-(\\s?\\d*\\.?\\d+)`, "gm");
107
+ return text.replaceAll(minusRegex, "$1−$2");
108
+ }
109
+ /**
110
+ * Comprehensive dash replacement for typographic correctness.
111
+ *
112
+ * Applies multiple transformations:
113
+ * 1. Converts hyphens to minus signs in numerical contexts
114
+ * 2. Converts surrounded dashes (- or --) to em-dashes (—)
115
+ * 3. Removes spaces around em-dashes (per modern style)
116
+ * 4. Preserves space after em-dash at start of line
117
+ * 5. Adds space after em-dash following quotation marks
118
+ * 6. Converts number ranges to en-dashes (1-5 → 1–5)
119
+ * 7. Converts date ranges to en-dashes (Jan-Mar → Jan–Mar)
120
+ *
121
+ * Does NOT modify:
122
+ * - Horizontal rules (---)
123
+ * - Compound modifiers (well-known, browser-specific)
124
+ * - Hyphens in quoted blockquotes ("> - item")
125
+ *
126
+ * @param text - The text to transform
127
+ * @param options - Configuration options
128
+ * @returns The text with proper dashes
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * hyphenReplace("This is a - test")
133
+ * // → "This is a—test"
134
+ *
135
+ * hyphenReplace("Since--as you know")
136
+ * // → "Since—as you know"
137
+ *
138
+ * hyphenReplace("Pages 1-5")
139
+ * // → "Pages 1–5"
140
+ * ```
141
+ */
142
+ export function hyphenReplace(text, options = {}) {
143
+ const chr = options.separator ?? DEFAULT_SEPARATOR;
144
+ text = minusReplace(text, options);
145
+ // Handle dashes with potential spaces and optional marker character
146
+ // Being right after chr is a sufficient condition for being an em
147
+ // dash, as it indicates the start of a new line
148
+ const preDash = new RegExp(`((?<markerBeforeTwo>${chr}?)[ ]+|(?<markerBeforeThree>${chr}))`);
149
+ // Want eg " - " to be replaced with "—"
150
+ const surroundedDash = new RegExp(`(?<=[^\\s>]|^)${preDash.source}[~–—-]+[ ]*(?<markerAfter>${chr}?)([ ]+|$)`, "g");
151
+ // Replace surrounded dashes with em dash
152
+ text = text.replace(surroundedDash, "$<markerBeforeTwo>$<markerBeforeThree>—$<markerAfter>");
153
+ // "Since--as you know" should be "Since—as you know"
154
+ const multipleDashInWords = new RegExp(`(?<=[A-Za-z\\d])(?<markerBefore>${chr}?)[~–—-]{2,}(?<markerAfter>${chr}?)(?=[A-Za-z\\d ])`, "g");
155
+ text = text.replace(multipleDashInWords, "$<markerBefore>—$<markerAfter>");
156
+ // Handle dashes at the start of a line
157
+ text = text.replace(new RegExp(`^(${chr})?[-]+ `, "gm"), "$1— ");
158
+ // Create a regex for spaces around em dashes, allowing for optional spaces around the em dash
159
+ const spacesAroundEM = new RegExp(`(?<markerBefore>${chr}?)[ ]*—[ ]*(?<markerAfter>${chr}?)[ ]*`, "g");
160
+ // Remove spaces around em dashes
161
+ text = text.replace(spacesAroundEM, "$<markerBefore>—$<markerAfter>");
162
+ // Handle special case after quotation marks
163
+ const postQuote = new RegExp(`(?<quote>[.!?]${chr}?['"'"]${chr}?|…)${spacesAroundEM.source}`, "g");
164
+ text = text.replace(postQuote, "$<quote> $<markerBefore>—$<markerAfter> ");
165
+ // Handle em dashes at the start of a line
166
+ const startOfLine = new RegExp(`^${spacesAroundEM.source}(?<after>[A-Z0-9])`, "gm");
167
+ text = text.replace(startOfLine, "$<markerBefore>—$<markerAfter> $<after>");
168
+ text = enDashNumberRange(text, options);
169
+ text = enDashDateRange(text, options);
170
+ return text;
171
+ }
172
+ //# sourceMappingURL=dashes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashes.js","sourceRoot":"","sources":["../src/dashes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
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, and more.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export { niceQuotes, type QuoteOptions } from "./quotes.js";
11
+ export { hyphenReplace, enDashNumberRange, enDashDateRange, minusReplace, months, type DashOptions, } from "./dashes.js";
12
+ export interface TransformOptions {
13
+ /**
14
+ * A boundary marker character used when transforming text that spans
15
+ * multiple HTML elements. This character is treated as "transparent"
16
+ * in the regex patterns.
17
+ *
18
+ * Should be a character that doesn't appear in your text.
19
+ * Default: "\uE000" (Unicode Private Use Area)
20
+ */
21
+ separator?: string;
22
+ }
23
+ /**
24
+ * Applies all typography transformations: smart quotes and proper dashes.
25
+ *
26
+ * This is a convenience function that applies both `niceQuotes` and
27
+ * `hyphenReplace` in sequence.
28
+ *
29
+ * @param text - The text to transform
30
+ * @param options - Configuration options
31
+ * @returns The text with all typography improvements applied
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { transform } from '@alexander-turner/punctilio'
36
+ *
37
+ * transform('"Hello," she said - "it\'s pages 1-5."')
38
+ * // → '"Hello," she said—"it's pages 1–5."'
39
+ * ```
40
+ */
41
+ export declare function transform(text: string, options?: TransformOptions): string;
42
+ /**
43
+ * Default separator character for boundary marking.
44
+ * Uses Unicode Private Use Area character U+E000.
45
+ */
46
+ export declare const DEFAULT_SEPARATOR = "\uE000";
47
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,MAAM,aAAa,CAAA;AAC3D,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,MAAM,EACN,KAAK,WAAW,GACjB,MAAM,aAAa,CAAA;AAEpB,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAI9E;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
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, and more.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export { niceQuotes } from "./quotes.js";
11
+ export { hyphenReplace, enDashNumberRange, enDashDateRange, minusReplace, months, } from "./dashes.js";
12
+ import { niceQuotes } from "./quotes.js";
13
+ import { hyphenReplace } from "./dashes.js";
14
+ /**
15
+ * Applies all typography transformations: smart quotes and proper dashes.
16
+ *
17
+ * This is a convenience function that applies both `niceQuotes` and
18
+ * `hyphenReplace` in sequence.
19
+ *
20
+ * @param text - The text to transform
21
+ * @param options - Configuration options
22
+ * @returns The text with all typography improvements applied
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { transform } from '@alexander-turner/punctilio'
27
+ *
28
+ * transform('"Hello," she said - "it\'s pages 1-5."')
29
+ * // → '"Hello," she said—"it's pages 1–5."'
30
+ * ```
31
+ */
32
+ export function transform(text, options = {}) {
33
+ text = hyphenReplace(text, options);
34
+ text = niceQuotes(text, options);
35
+ return text;
36
+ }
37
+ /**
38
+ * Default separator character for boundary marking.
39
+ * Uses Unicode Private Use Area character U+E000.
40
+ */
41
+ export const DEFAULT_SEPARATOR = "\uE000";
42
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAqB,MAAM,aAAa,CAAA;AAC3D,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,MAAM,GAEP,MAAM,aAAa,CAAA;AAcpB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,UAA4B,EAAE;IACpE,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACnC,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAChC,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAA"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Smart quote transformation
3
+ *
4
+ * Converts straight quotes to typographically correct curly quotes,
5
+ * handling contractions, possessives, and nested quotes.
6
+ */
7
+ export interface QuoteOptions {
8
+ /**
9
+ * A boundary marker character used when transforming text that spans
10
+ * multiple HTML elements. This character is treated as "transparent"
11
+ * in the regex patterns - it won't affect quote matching but allows
12
+ * the algorithm to work across element boundaries.
13
+ *
14
+ * Should be a character that doesn't appear in your text.
15
+ * Default: "\uE000" (Unicode Private Use Area)
16
+ */
17
+ separator?: string;
18
+ }
19
+ /**
20
+ * Converts standard quotes to typographic smart quotes.
21
+ *
22
+ * @param text - The text to transform
23
+ * @param options - Configuration options
24
+ * @returns The text with smart quotes
25
+ */
26
+ export declare function niceQuotes(text: string, options?: QuoteOptions): string;
27
+ //# sourceMappingURL=quotes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quotes.d.ts","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAYD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CA+D3E"}
package/dist/quotes.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Smart quote transformation
3
+ *
4
+ * Converts straight quotes to typographically correct curly quotes,
5
+ * handling contractions, possessives, and nested quotes.
6
+ */
7
+ const DEFAULT_SEPARATOR = "\uE000";
8
+ // Unicode typography characters
9
+ const EM_DASH = "\u2014"; // —
10
+ const LEFT_DOUBLE = "\u201C"; // "
11
+ const RIGHT_DOUBLE = "\u201D"; // "
12
+ const LEFT_SINGLE = "\u2018"; // '
13
+ const RIGHT_SINGLE = "\u2019"; // '
14
+ const ELLIPSIS = "\u2026"; // …
15
+ /**
16
+ * Converts standard quotes to typographic smart quotes.
17
+ *
18
+ * @param text - The text to transform
19
+ * @param options - Configuration options
20
+ * @returns The text with smart quotes
21
+ */
22
+ export function niceQuotes(text, options = {}) {
23
+ const chr = options.separator ?? DEFAULT_SEPARATOR;
24
+ // Single quotes //
25
+ // Ending comes first so as to not mess with the open quote
26
+ const afterEndingSinglePatterns = `\\s\\.!?;,\\)${EM_DASH}\\-\\]"`;
27
+ const afterEndingSingle = `(?=${chr}?(?:s${chr}?)?(?:[${afterEndingSinglePatterns}]|$))`;
28
+ const endingSingle = `(?<=[^\\s${LEFT_DOUBLE}'])[']${afterEndingSingle}`;
29
+ text = text.replace(new RegExp(endingSingle, "gm"), RIGHT_SINGLE);
30
+ // Contractions are sandwiched between two letters
31
+ const contraction = `(?<=[A-Za-z])['${RIGHT_SINGLE}](?=${chr}?[a-zA-Z])`;
32
+ text = text.replace(new RegExp(contraction, "gm"), RIGHT_SINGLE);
33
+ // Apostrophes always point down
34
+ // Whitelist for eg rock 'n' roll
35
+ const apostropheWhitelist = `(?=n${RIGHT_SINGLE} )`;
36
+ const endQuoteNotContraction = `(?!${contraction})${RIGHT_SINGLE}${afterEndingSingle}`;
37
+ // Convert to apostrophe if not followed by an end quote
38
+ // Note: The character class uses LEFT_SINGLE and ASCII straight quote (U+0027)
39
+ // NOT RIGHT_SINGLE - this is intentional for the algorithm to work correctly
40
+ const apostropheRegex = new RegExp(`(?<=^|[^\\w])'(${apostropheWhitelist}|(?![^${LEFT_SINGLE}'\\n]*${endQuoteNotContraction}))`, "gm");
41
+ text = text.replace(apostropheRegex, RIGHT_SINGLE);
42
+ // Beginning single quotes
43
+ const beginningSingle = `((?:^|[\\s${LEFT_DOUBLE}${RIGHT_DOUBLE}\\-\\(])${chr}?)['](?=${chr}?\\S)`;
44
+ text = text.replace(new RegExp(beginningSingle, "gm"), `$1${LEFT_SINGLE}`);
45
+ // Double quotes //
46
+ const beginningDouble = new RegExp(`(?<=^|[\\s\\(\\/\\[\\{\\-${EM_DASH}${chr}])(?<beforeChr>${chr}?)["](?<afterChr>(${chr}[ .,])|(?=${chr}?\\.{3}|${chr}?[^\\s\\)\\${EM_DASH},!?${chr};:.\\}]))`, "gm");
47
+ text = text.replace(beginningDouble, `$<beforeChr>${LEFT_DOUBLE}$<afterChr>`);
48
+ // Open quote after brace (generally in math mode)
49
+ text = text.replace(new RegExp(`(?<=\\{)(${chr}? )?["]`, "g"), `$1${LEFT_DOUBLE}`);
50
+ // note: Allowing 2 chrs in a row
51
+ const endingDouble = `([^\\s\\(])["](${chr}?)(?=${chr}|[\\s/\\).,;${EM_DASH}:\\-\\}!?s]|$)`;
52
+ text = text.replace(new RegExp(endingDouble, "g"), `$1${RIGHT_DOUBLE}$2`);
53
+ // If end of line, replace with right double quote
54
+ text = text.replace(new RegExp(`["](${chr}?)$`, "g"), `${RIGHT_DOUBLE}$1`);
55
+ // If single quote has a right double quote after it, replace with right single and then double
56
+ text = text.replace(new RegExp(`'(?=${RIGHT_DOUBLE})`, "gu"), RIGHT_SINGLE);
57
+ // Punctuation //
58
+ // Periods inside quotes
59
+ const periodRegex = new RegExp(`(?<![!?:\\.${ELLIPSIS}])(${chr}?)([${RIGHT_SINGLE}${RIGHT_DOUBLE}])(${chr}?)(?!\\.\\.\\.)\\.`, "g");
60
+ text = text.replace(periodRegex, "$1.$2$3");
61
+ // Commas outside of quotes
62
+ const commaRegex = new RegExp(`(?<![!?]),(${chr}?[${RIGHT_DOUBLE}${RIGHT_SINGLE}])`, "g");
63
+ text = text.replace(commaRegex, "$1,");
64
+ return text;
65
+ }
66
+ //# sourceMappingURL=quotes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quotes.js","sourceRoot":"","sources":["../src/quotes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,MAAM,iBAAiB,GAAG,QAAQ,CAAA;AAElC,gCAAgC;AAChC,MAAM,OAAO,GAAG,QAAQ,CAAA,CAAC,IAAI;AAC7B,MAAM,WAAW,GAAG,QAAQ,CAAA,CAAC,IAAI;AACjC,MAAM,YAAY,GAAG,QAAQ,CAAA,CAAC,IAAI;AAClC,MAAM,WAAW,GAAG,QAAQ,CAAA,CAAC,IAAI;AACjC,MAAM,YAAY,GAAG,QAAQ,CAAA,CAAC,IAAI;AAClC,MAAM,QAAQ,GAAG,QAAQ,CAAA,CAAC,IAAI;AAE9B;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,UAAwB,EAAE;IACjE,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,IAAI,iBAAiB,CAAA;IAElD,mBAAmB;IACnB,2DAA2D;IAC3D,MAAM,yBAAyB,GAAG,gBAAgB,OAAO,SAAS,CAAA;IAClE,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,yBAAyB,OAAO,CAAA;IACxF,MAAM,YAAY,GAAG,YAAY,WAAW,SAAS,iBAAiB,EAAE,CAAA;IACxE,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,YAAY,CAAC,CAAA;IAEjE,kDAAkD;IAClD,MAAM,WAAW,GAAG,kBAAkB,YAAY,OAAO,GAAG,YAAY,CAAA;IACxE,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,YAAY,CAAC,CAAA;IAEhE,gCAAgC;IAChC,kCAAkC;IAClC,MAAM,mBAAmB,GAAG,OAAO,YAAY,IAAI,CAAA;IACnD,MAAM,sBAAsB,GAAG,MAAM,WAAW,IAAI,YAAY,GAAG,iBAAiB,EAAE,CAAA;IACtF,yDAAyD;IACzD,+EAA+E;IAC/E,6EAA6E;IAC7E,MAAM,eAAe,GAAG,IAAI,MAAM,CAChC,kBAAkB,mBAAmB,SAAS,WAAW,SAAS,sBAAsB,IAAI,EAC5F,IAAI,CACL,CAAA;IACD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,YAAY,CAAC,CAAA;IAElD,0BAA0B;IAC1B,MAAM,eAAe,GAAG,aAAa,WAAW,GAAG,YAAY,WAAW,GAAG,WAAW,GAAG,OAAO,CAAA;IAClG,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,EAAE,KAAK,WAAW,EAAE,CAAC,CAAA;IAE1E,mBAAmB;IACnB,MAAM,eAAe,GAAG,IAAI,MAAM,CAChC,4BAA4B,OAAO,GAAG,GAAG,kBAAkB,GAAG,qBAAqB,GAAG,aAAa,GAAG,WAAW,GAAG,cAAc,OAAO,MAAM,GAAG,WAAW,EAC7J,IAAI,CACL,CAAA;IACD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,eAAe,WAAW,aAAa,CAAC,CAAA;IAE7E,kDAAkD;IAClD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,YAAY,GAAG,SAAS,EAAE,GAAG,CAAC,EAAE,KAAK,WAAW,EAAE,CAAC,CAAA;IAElF,iCAAiC;IACjC,MAAM,YAAY,GAAG,kBAAkB,GAAG,QAAQ,GAAG,eAAe,OAAO,gBAAgB,CAAA;IAC3F,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,YAAY,EAAE,GAAG,CAAC,EAAE,KAAK,YAAY,IAAI,CAAC,CAAA;IAEzE,kDAAkD;IAClD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,GAAG,KAAK,EAAE,GAAG,CAAC,EAAE,GAAG,YAAY,IAAI,CAAC,CAAA;IAC1E,+FAA+F;IAC/F,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,YAAY,GAAG,EAAE,IAAI,CAAC,EAAE,YAAY,CAAC,CAAA;IAE3E,iBAAiB;IACjB,wBAAwB;IACxB,MAAM,WAAW,GAAG,IAAI,MAAM,CAC5B,cAAc,QAAQ,MAAM,GAAG,OAAO,YAAY,GAAG,YAAY,MAAM,GAAG,oBAAoB,EAC9F,GAAG,CACJ,CAAA;IACD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IAE3C,2BAA2B;IAC3B,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,cAAc,GAAG,KAAK,YAAY,GAAG,YAAY,IAAI,EAAE,GAAG,CAAC,CAAA;IACzF,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;IAEtC,OAAO,IAAI,CAAA;AACb,CAAC"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "punctilio",
3
+ "version": "0.1.0",
4
+ "description": "Smart typography transformations: curly quotes, em-dashes, en-dashes, and more",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "keywords": [
18
+ "typography",
19
+ "smart-quotes",
20
+ "curly-quotes",
21
+ "em-dash",
22
+ "en-dash",
23
+ "typesetting",
24
+ "punctuation",
25
+ "text-formatting"
26
+ ],
27
+ "author": "Alexander Turner",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/alexander-turner/punctilio.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/alexander-turner/punctilio/issues"
35
+ },
36
+ "homepage": "https://github.com/alexander-turner/punctilio#readme",
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/jest": "^29.5.12",
42
+ "@types/node": "^20.11.0",
43
+ "jest": "^29.7.0",
44
+ "ts-jest": "^29.1.2",
45
+ "typescript": "^5.3.3"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc",
49
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
50
+ "postinstall": "bash scripts/install-hooks.sh || true",
51
+ "install-hooks": "bash scripts/install-hooks.sh"
52
+ }
53
+ }