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.
- package/dist/cli.d.ts +1 -12
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -26
- package/dist/cli.js.map +1 -1
- package/dist/constants.d.ts +13 -61
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +12 -61
- package/dist/constants.js.map +1 -1
- package/dist/dashes.d.ts +0 -9
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +5 -27
- package/dist/dashes.js.map +1 -1
- package/dist/html.d.ts +1 -32
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +1 -29
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +5 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -45
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +1 -34
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +1 -34
- package/dist/markdown.js.map +1 -1
- package/dist/nbsp.d.ts +1 -68
- package/dist/nbsp.d.ts.map +1 -1
- package/dist/nbsp.js +7 -87
- package/dist/nbsp.js.map +1 -1
- package/dist/prettier-plugin.d.ts +0 -14
- package/dist/prettier-plugin.d.ts.map +1 -1
- package/dist/prettier-plugin.js +0 -19
- package/dist/prettier-plugin.js.map +1 -1
- package/dist/quotes.d.ts +0 -3
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +11 -58
- package/dist/quotes.js.map +1 -1
- package/dist/rehype.d.ts +5 -142
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +9 -146
- package/dist/rehype.js.map +1 -1
- package/dist/remark.d.ts +0 -30
- package/dist/remark.d.ts.map +1 -1
- package/dist/remark.js +0 -41
- package/dist/remark.js.map +1 -1
- package/dist/symbols.d.ts +1 -7
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +10 -54
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts +10 -58
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +10 -58
- package/dist/utils.js.map +1 -1
- package/package.json +6 -5
- package/src/cli.ts +7 -33
- package/src/constants.ts +13 -62
- package/src/dashes.ts +5 -28
- package/src/html.ts +1 -33
- package/src/index.ts +30 -70
- package/src/markdown.ts +1 -38
- package/src/nbsp.ts +7 -88
- package/src/prettier-plugin.ts +0 -20
- package/src/quotes.ts +11 -59
- package/src/rehype.ts +11 -165
- package/src/remark.ts +1 -43
- package/src/symbols.ts +10 -67
- 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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
45
|
-
|
|
19
|
+
nbspAfterShortWords,
|
|
20
|
+
nbspBeforeLastWord,
|
|
46
21
|
nbspBetweenInitials,
|
|
22
|
+
nbspBetweenNumberAndUnit,
|
|
23
|
+
type NbspOptions,
|
|
47
24
|
nbspTransform,
|
|
48
|
-
UNITS,
|
|
49
|
-
HONORIFICS,
|
|
50
25
|
REFERENCE_ABBREVIATIONS,
|
|
51
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
package/src/prettier-plugin.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
170
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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"
|