punctilio 3.10.0 → 3.11.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/README.md +46 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +35 -13
- package/dist/cli.js.map +1 -1
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +4 -3
- package/dist/dashes.js.map +1 -1
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +2 -1
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -26
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +0 -4
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +2 -5
- package/dist/markdown.js.map +1 -1
- package/dist/prettier-plugin.d.ts.map +1 -1
- package/dist/prettier-plugin.js +13 -2
- package/dist/prettier-plugin.js.map +1 -1
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +3 -1
- package/dist/quotes.js.map +1 -1
- package/dist/rehype.d.ts +14 -36
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +14 -36
- package/dist/rehype.js.map +1 -1
- package/dist/remark.d.ts +0 -3
- package/dist/remark.d.ts.map +1 -1
- package/dist/remark.js +0 -7
- package/dist/remark.js.map +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +6 -5
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts +26 -9
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +40 -9
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +37 -13
- package/src/dashes.ts +23 -3
- package/src/html.ts +2 -3
- package/src/index.ts +20 -30
- package/src/markdown.ts +2 -7
- package/src/prettier-plugin.ts +14 -2
- package/src/quotes.ts +3 -1
- package/src/rehype.ts +14 -36
- package/src/remark.ts +0 -7
- package/src/symbols.ts +18 -5
- package/src/utils.ts +42 -9
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE9D,qEAAqE;AACrE,MAAM,sBAAsB,GAAG,IAAI,CAAA;AAEnC;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,KAAa;IAC9D,IAAI,OAAO,CAAC,MAAM,IAAI,sBAAsB,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IAChC,CAAC;IAED,yDAAyD;IACzD,IAAI,CAAC;QACH,IAAI,OAAO,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,KAAK,UAAU,EAAE,CAAC;YAC5D,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAC7B,gBAAgB,KAAK,kBAAkB,OAAO,CAAC,MAAM,cAAc,OAAO,MAAM,CACjF,CAAA;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,gDAAgD;IAClD,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAA;IAC1D,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,OAAO,CAAC,MAAM,gBAAgB,CAAA;AACtF,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { createHash } from "node:crypto"
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
11
11
|
import { readFile, writeFile } from "node:fs/promises"
|
|
12
|
-
import { dirname, extname, join, relative, resolve } from "node:path"
|
|
12
|
+
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path"
|
|
13
13
|
import { fileURLToPath } from "node:url"
|
|
14
14
|
|
|
15
15
|
import { Command, CommanderError, Option } from "commander"
|
|
@@ -21,6 +21,7 @@ import { transformMarkdown, type MarkdownOptions } from "./markdown.js"
|
|
|
21
21
|
import { transformHtml, type HtmlOptions } from "./html.js"
|
|
22
22
|
import { PUNCTUATION_STYLES, type PunctuationStyle } from "./quotes.js"
|
|
23
23
|
import { DASH_STYLES, type DashStyle } from "./dashes.js"
|
|
24
|
+
import { stableStringify } from "./utils.js"
|
|
24
25
|
|
|
25
26
|
type CliOptions = MarkdownOptions & HtmlOptions
|
|
26
27
|
type FileType = "md" | "html"
|
|
@@ -182,18 +183,27 @@ function hashString(s: string): string {
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
function hashOptions(opts: CliOptions): string {
|
|
185
|
-
|
|
186
|
-
.filter(([, v]) => v !== undefined)
|
|
187
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
188
|
-
return hashString(JSON.stringify(stable))
|
|
186
|
+
return hashString(stableStringify(opts))
|
|
189
187
|
}
|
|
190
188
|
|
|
191
|
-
function loadCache(location: string): Cache {
|
|
189
|
+
function loadCache(location: string, stderr: NodeJS.WritableStream): Cache {
|
|
192
190
|
if (!existsSync(location)) return { files: {} }
|
|
193
191
|
try {
|
|
194
192
|
const parsed = JSON.parse(readFileSync(location, "utf8")) as Cache
|
|
195
|
-
|
|
193
|
+
if (!parsed.files) {
|
|
194
|
+
stderr.write(`Warning: cache at ${location} is missing the "files" key; discarding.\n`)
|
|
195
|
+
return { files: {} }
|
|
196
|
+
}
|
|
197
|
+
// Drop absolute-path keys carried over from a pre-cwd-relative version
|
|
198
|
+
// of the cache. They never match modern lookups, so keeping them
|
|
199
|
+
// forever would just bloat the cache file on every save.
|
|
200
|
+
const files: Record<string, CacheEntry> = {}
|
|
201
|
+
for (const [k, v] of Object.entries(parsed.files)) {
|
|
202
|
+
if (!isAbsolute(k)) files[k] = v
|
|
203
|
+
}
|
|
204
|
+
return { files }
|
|
196
205
|
} catch {
|
|
206
|
+
stderr.write(`Warning: cache at ${location} could not be parsed; discarding.\n`)
|
|
197
207
|
return { files: {} }
|
|
198
208
|
}
|
|
199
209
|
}
|
|
@@ -203,6 +213,16 @@ function saveCache(location: string, cache: Cache): void {
|
|
|
203
213
|
writeFileSync(location, JSON.stringify(cache))
|
|
204
214
|
}
|
|
205
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Cache key for a file. Always cwd-relative so a project that moves
|
|
218
|
+
* directories (or syncs the cache file across machines via the same
|
|
219
|
+
* repo layout) keeps hitting the cache. Files outside cwd appear as
|
|
220
|
+
* `../foo.md` and still hash consistently.
|
|
221
|
+
*/
|
|
222
|
+
function cacheKey(filePath: string, cwd: string): string {
|
|
223
|
+
return relative(cwd, filePath)
|
|
224
|
+
}
|
|
225
|
+
|
|
206
226
|
function buildOptions(flags: ParsedFlags): CliOptions {
|
|
207
227
|
const opts: CliOptions = {}
|
|
208
228
|
if (flags.punctuationStyle) opts.punctuationStyle = flags.punctuationStyle
|
|
@@ -344,18 +364,22 @@ async function runValidated(
|
|
|
344
364
|
const cacheLocation = flags.cache === false
|
|
345
365
|
? undefined
|
|
346
366
|
: resolve(cwd, flags.cacheLocation ?? DEFAULT_CACHE_LOCATION)
|
|
347
|
-
const cache = cacheLocation ? loadCache(cacheLocation) : undefined
|
|
367
|
+
const cache = cacheLocation ? loadCache(cacheLocation, io.stderr) : undefined
|
|
348
368
|
const optionsHash = cache ? hashOptions(opts) : ""
|
|
349
369
|
|
|
350
370
|
let anyChanged = false
|
|
351
371
|
for (const path of files) {
|
|
352
372
|
const type = inferFileType(path, flags.type)
|
|
353
373
|
const original = await readFile(path, "utf8")
|
|
374
|
+
const key = cache ? cacheKey(path, cwd) : ""
|
|
354
375
|
if (cache) {
|
|
355
|
-
const entry = cache.files[
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
376
|
+
const entry = cache.files[key]
|
|
377
|
+
if (
|
|
378
|
+
entry !== undefined &&
|
|
379
|
+
entry.contentHash === hashString(original) &&
|
|
380
|
+
entry.optionsHash === optionsHash &&
|
|
381
|
+
entry.version === version
|
|
382
|
+
) continue
|
|
359
383
|
}
|
|
360
384
|
|
|
361
385
|
const formatted = matchTrailingNewline(original, await transformContent(original, type, opts))
|
|
@@ -370,7 +394,7 @@ async function runValidated(
|
|
|
370
394
|
}
|
|
371
395
|
}
|
|
372
396
|
if (cache && (unchanged || !check)) {
|
|
373
|
-
cache.files[
|
|
397
|
+
cache.files[key] = { contentHash: hashString(formatted), optionsHash, version }
|
|
374
398
|
}
|
|
375
399
|
}
|
|
376
400
|
|
package/src/dashes.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR, LATIN_LETTERS, wordBoundaryStart, wordBoundaryEnd, getEscapedSeparator, cachedRegExp } from "./constants.js"
|
|
6
|
+
import { namedGroups } from "./utils.js"
|
|
6
7
|
|
|
7
8
|
export const DASH_STYLES = ["american", "british", "none"] as const
|
|
8
9
|
export type DashStyle = (typeof DASH_STYLES)[number]
|
|
@@ -69,7 +70,13 @@ export function enDashNumberRange(text: string, options: DashOptions = {}): stri
|
|
|
69
70
|
text = text.replace(
|
|
70
71
|
cachedRegExp(positiveRangePattern, "g"),
|
|
71
72
|
(...args) => {
|
|
72
|
-
const groups =
|
|
73
|
+
const groups = namedGroups<{
|
|
74
|
+
precedingAreaCode?: string
|
|
75
|
+
start: string
|
|
76
|
+
end: string
|
|
77
|
+
following?: string
|
|
78
|
+
suffix?: string
|
|
79
|
+
}>(args)
|
|
73
80
|
if (groups.following) return args[0] as string
|
|
74
81
|
const startNum = groups.start.replace(cachedRegExp(chr, "g"), "")
|
|
75
82
|
const endNum = groups.end.replace(cachedRegExp(chr, "g"), "")
|
|
@@ -96,7 +103,13 @@ export function enDashNumberRange(text: string, options: DashOptions = {}): stri
|
|
|
96
103
|
"g"
|
|
97
104
|
),
|
|
98
105
|
(...args) => {
|
|
99
|
-
const groups =
|
|
106
|
+
const groups = namedGroups<{
|
|
107
|
+
start: string
|
|
108
|
+
neg?: string
|
|
109
|
+
end: string
|
|
110
|
+
following?: string
|
|
111
|
+
suffix?: string
|
|
112
|
+
}>(args)
|
|
100
113
|
if (groups.following) return args[0] as string
|
|
101
114
|
return `${groups.start}${EN_DASH}${groups.neg ? MINUS : ""}${groups.end}${groups.suffix ?? ""}`
|
|
102
115
|
}
|
|
@@ -121,7 +134,14 @@ export function enDashDateRange(text: string, options: DashOptions = {}): string
|
|
|
121
134
|
return text.replace(
|
|
122
135
|
cachedRegExp(`${wb}(?<startMonth>${monthPattern})${startYear}(?<preSep>${chr}?)(?<preSpace> ?)-(?<postSpace> ?)(?<postSep>${chr}?)(?<endMonth>${monthPattern})${endYear}${wbe}`, "g"),
|
|
123
136
|
(...args) => {
|
|
124
|
-
const groups =
|
|
137
|
+
const groups = namedGroups<{
|
|
138
|
+
startMonth: string
|
|
139
|
+
startYear?: string
|
|
140
|
+
preSep: string
|
|
141
|
+
endMonth: string
|
|
142
|
+
endYear?: string
|
|
143
|
+
postSep: string
|
|
144
|
+
}>(args)
|
|
125
145
|
const [pre, post] = dashStyle === "british" ? [" ", " "] : ["", ""]
|
|
126
146
|
return `${groups.startMonth}${groups.startYear || ""}${groups.preSep}${pre}${EN_DASH}${post}${groups.postSep}${groups.endMonth}${groups.endYear || ""}`
|
|
127
147
|
}
|
package/src/html.ts
CHANGED
|
@@ -15,6 +15,7 @@ import rehypeStringify from "rehype-stringify"
|
|
|
15
15
|
import QuickLRU from "quick-lru"
|
|
16
16
|
|
|
17
17
|
import { rehypePunctilio, type RehypePunctilioOptions } from "./rehype.js"
|
|
18
|
+
import { stableStringify } from "./utils.js"
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Options for the HTML transform pipeline.
|
|
@@ -77,9 +78,7 @@ export async function transformHtml(
|
|
|
77
78
|
return String(result)
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const optionsKey =
|
|
81
|
-
Object.entries(options).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b))
|
|
82
|
-
)
|
|
81
|
+
const optionsKey = stableStringify(options)
|
|
83
82
|
|
|
84
83
|
let processor = processorCache.get(optionsKey)
|
|
85
84
|
if (!processor) {
|
package/src/index.ts
CHANGED
|
@@ -173,7 +173,7 @@ import { niceQuotes } from "./quotes.js"
|
|
|
173
173
|
import { hyphenReplace } from "./dashes.js"
|
|
174
174
|
import { symbolTransform, fractions as fractionsTransform, degrees as degreesTransform, superscriptOrdinal as superscriptTransform, primeMarks, collapseSpaces as collapseSpacesTransform, punctuationLigatures as ligaturesTransform } from "./symbols.js"
|
|
175
175
|
import { nbspTransform as nbspTransformFn } from "./nbsp.js"
|
|
176
|
-
import { assertSeparatorCountPreserved, formatErrorString } from "./utils.js"
|
|
176
|
+
import { assertSeparatorCountPreserved, filterUndefined, formatErrorString } from "./utils.js"
|
|
177
177
|
import { DEFAULT_SEPARATOR, ISSUES_URL, UNICODE_SYMBOLS } from "./constants.js"
|
|
178
178
|
|
|
179
179
|
export { assertSeparatorAbsent, assertSeparatorCountPreserved, countSeparators, transformTextNodes } from "./utils.js"
|
|
@@ -210,10 +210,6 @@ const defaultOpts: Required<Omit<TransformOptions, "separator">> = {
|
|
|
210
210
|
* 9. collapseSpaces (collapses multiple spaces into one)
|
|
211
211
|
* 10. nbsp (non-breaking spaces, enabled by default)
|
|
212
212
|
*
|
|
213
|
-
* @param text - The text to transform
|
|
214
|
-
* @param options - Configuration options
|
|
215
|
-
* @returns The text with all typography improvements applied
|
|
216
|
-
*
|
|
217
213
|
* @example
|
|
218
214
|
* ```ts
|
|
219
215
|
* import { transform } from 'punctilio'
|
|
@@ -231,52 +227,46 @@ const defaultOpts: Required<Omit<TransformOptions, "separator">> = {
|
|
|
231
227
|
export function transform(text: string, options: TransformOptions = {}): string {
|
|
232
228
|
const separator = options.separator ?? DEFAULT_SEPARATOR
|
|
233
229
|
|
|
234
|
-
// Validate separator: must be non-empty and contain only BMP characters.
|
|
235
|
-
// for...of iterates by code point; surrogate pairs (non-BMP) yield a
|
|
236
|
-
// two-UTF16-unit string, so ch.length > 1 detects them.
|
|
237
230
|
if (separator.length === 0) {
|
|
238
231
|
throw new Error("Invalid separator: must not be empty.")
|
|
239
232
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
246
|
-
|
|
233
|
+
// Non-BMP characters are encoded in UTF-16 as surrogate pairs in the range
|
|
234
|
+
// U+D800–U+DFFF. A surrogate code unit (paired or lone) means the separator
|
|
235
|
+
// would survive some regexes as a single char and split others as two units.
|
|
236
|
+
if (/[\uD800-\uDFFF]/.test(separator)) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Invalid separator: must contain only BMP characters (no characters outside the Basic Multilingual Plane). ` +
|
|
239
|
+
`Received "${separator}" which contains a non-BMP character.`
|
|
240
|
+
)
|
|
247
241
|
}
|
|
248
242
|
|
|
249
243
|
const original = text
|
|
250
|
-
|
|
251
|
-
const definedOptions = Object.fromEntries(
|
|
252
|
-
Object.entries(options).filter(([, v]) => v !== undefined)
|
|
253
|
-
) as TransformOptions
|
|
254
|
-
const { symbols, fractions, degrees, superscript, ligatures, nbsp, collapseSpaces, checkIdempotency, ...separatorOpts } = { ...defaultOpts, ...definedOptions }
|
|
244
|
+
const { symbols, fractions, degrees, superscript, ligatures, nbsp, collapseSpaces, checkIdempotency, ...pipelineOpts } = { ...defaultOpts, ...filterUndefined(options) }
|
|
255
245
|
|
|
256
|
-
text = hyphenReplace(text,
|
|
257
|
-
if (
|
|
258
|
-
text = primeMarks(text,
|
|
246
|
+
text = hyphenReplace(text, pipelineOpts)
|
|
247
|
+
if (pipelineOpts.punctuationStyle !== "none") {
|
|
248
|
+
text = primeMarks(text, pipelineOpts)
|
|
259
249
|
}
|
|
260
|
-
text = niceQuotes(text,
|
|
250
|
+
text = niceQuotes(text, pipelineOpts)
|
|
261
251
|
|
|
262
252
|
if (symbols) {
|
|
263
|
-
text = symbolTransform(text,
|
|
253
|
+
text = symbolTransform(text, pipelineOpts)
|
|
264
254
|
}
|
|
265
255
|
|
|
266
256
|
if (fractions) {
|
|
267
|
-
text = fractionsTransform(text,
|
|
257
|
+
text = fractionsTransform(text, pipelineOpts)
|
|
268
258
|
}
|
|
269
259
|
|
|
270
260
|
if (degrees) {
|
|
271
|
-
text = degreesTransform(text,
|
|
261
|
+
text = degreesTransform(text, pipelineOpts)
|
|
272
262
|
}
|
|
273
263
|
|
|
274
264
|
if (superscript) {
|
|
275
|
-
text = superscriptTransform(text,
|
|
265
|
+
text = superscriptTransform(text, pipelineOpts)
|
|
276
266
|
}
|
|
277
267
|
|
|
278
268
|
if (ligatures) {
|
|
279
|
-
text = ligaturesTransform(text,
|
|
269
|
+
text = ligaturesTransform(text, pipelineOpts)
|
|
280
270
|
}
|
|
281
271
|
|
|
282
272
|
if (collapseSpaces) {
|
|
@@ -284,7 +274,7 @@ export function transform(text: string, options: TransformOptions = {}): string
|
|
|
284
274
|
}
|
|
285
275
|
|
|
286
276
|
if (nbsp) {
|
|
287
|
-
text = nbspTransformFn(text,
|
|
277
|
+
text = nbspTransformFn(text, pipelineOpts)
|
|
288
278
|
}
|
|
289
279
|
|
|
290
280
|
assertSeparatorCountPreserved(original, text, separator, "transform")
|
package/src/markdown.ts
CHANGED
|
@@ -16,6 +16,7 @@ import remarkStringify from "remark-stringify"
|
|
|
16
16
|
import QuickLRU from "quick-lru"
|
|
17
17
|
|
|
18
18
|
import { remarkPunctilio, type RemarkPunctilioOptions } from "./remark.js"
|
|
19
|
+
import { stableStringify } from "./utils.js"
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Options for the Markdown transform pipeline.
|
|
@@ -94,10 +95,6 @@ export function clearProcessorCache(): void {
|
|
|
94
95
|
* The unified processor pipeline is cached and reused across calls with
|
|
95
96
|
* identical options, avoiding redundant pipeline construction.
|
|
96
97
|
*
|
|
97
|
-
* @param input - The Markdown text to transform
|
|
98
|
-
* @param options - Configuration options for both the transform and serializer
|
|
99
|
-
* @returns The typographically improved Markdown text
|
|
100
|
-
*
|
|
101
98
|
* @example
|
|
102
99
|
* ```ts
|
|
103
100
|
* import { transformMarkdown } from 'punctilio/markdown'
|
|
@@ -110,9 +107,7 @@ export async function transformMarkdown(
|
|
|
110
107
|
input: string,
|
|
111
108
|
options: MarkdownOptions = {}
|
|
112
109
|
): Promise<string> {
|
|
113
|
-
const optionsKey =
|
|
114
|
-
Object.entries(options).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b))
|
|
115
|
-
)
|
|
110
|
+
const optionsKey = stableStringify(options)
|
|
116
111
|
|
|
117
112
|
let processor = processorCache.get(optionsKey)
|
|
118
113
|
if (!processor) {
|
package/src/prettier-plugin.ts
CHANGED
|
@@ -30,6 +30,19 @@ async function loadPunctilioConfig(searchFrom: string): Promise<RemarkPunctilioO
|
|
|
30
30
|
return !result || result.isEmpty ? {} : (result.config as RemarkPunctilioOptions)
|
|
31
31
|
}
|
|
32
32
|
|
|
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
|
+
async function runRemarkPunctilio(opts: RemarkPunctilioOptions, ast: Root): Promise<void> {
|
|
39
|
+
// The unified `Transformer` signature requires a VFile and a callback,
|
|
40
|
+
// but `remarkPunctilio` only ever reads the tree. Narrow to the actual
|
|
41
|
+
// shape here so the cast lives in one well-commented place.
|
|
42
|
+
const transformer = remarkPunctilio(opts) as (tree: Root) => void | Promise<void>
|
|
43
|
+
await transformer(ast)
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
const plugin: Plugin = {
|
|
34
47
|
parsers: {
|
|
35
48
|
markdown: {
|
|
@@ -38,8 +51,7 @@ const plugin: Plugin = {
|
|
|
38
51
|
const ast = await upstreamMarkdown.parse(text, options)
|
|
39
52
|
const searchFrom = typeof options.filepath === "string" ? dirname(options.filepath) : process.cwd()
|
|
40
53
|
const opts = await loadPunctilioConfig(searchFrom)
|
|
41
|
-
|
|
42
|
-
transform(ast)
|
|
54
|
+
await runRemarkPunctilio(opts, ast)
|
|
43
55
|
return ast
|
|
44
56
|
},
|
|
45
57
|
},
|
package/src/quotes.ts
CHANGED
|
@@ -215,7 +215,9 @@ function applyPunctuationStyle(text: string, sep: string, style: PunctuationStyl
|
|
|
215
215
|
text = moveOutsideToInside(text, escapedSep, ".", `(?!\\.\\.\\.)\\.`)
|
|
216
216
|
// Comma outside → inside: Hello", → Hello,"
|
|
217
217
|
text = moveOutsideToInside(text, escapedSep, ",", `,`)
|
|
218
|
-
} else
|
|
218
|
+
} else {
|
|
219
|
+
// Every non-"american" non-"none" style (british/german/french) places
|
|
220
|
+
// punctuation outside the quotes.
|
|
219
221
|
// Period inside → outside: "Hello." → "Hello".
|
|
220
222
|
// No terminal punctuation guard — "Stop!." inside is always wrong; move the period out.
|
|
221
223
|
// Match ALL consecutive closing quotes so nested quotes like .'" become '". in one pass.
|
package/src/rehype.ts
CHANGED
|
@@ -78,13 +78,9 @@ const DEFAULT_SKIP_TAGS = ["code", "pre", "script", "style", "kbd", "var", "samp
|
|
|
78
78
|
/**
|
|
79
79
|
* Flattens text nodes from an element tree into a single array.
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* exclude individual text nodes from the result. Applied after
|
|
85
|
-
* element-level `shouldSkip`, so the hook is never invoked for text
|
|
86
|
-
* inside an already-skipped element.
|
|
87
|
-
* @returns Array of Text nodes
|
|
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.
|
|
88
84
|
*
|
|
89
85
|
* @example
|
|
90
86
|
* ```ts
|
|
@@ -143,10 +139,6 @@ function flattenTextNodesImpl(
|
|
|
143
139
|
/**
|
|
144
140
|
* Extracts concatenated text content from an element.
|
|
145
141
|
*
|
|
146
|
-
* @param node - The element to extract text from
|
|
147
|
-
* @param shouldSkip - Optional function to determine which elements to skip
|
|
148
|
-
* @returns The combined text content
|
|
149
|
-
*
|
|
150
142
|
* @example
|
|
151
143
|
* ```ts
|
|
152
144
|
* const text = getTextContent(paragraphElement)
|
|
@@ -164,10 +156,7 @@ export function getTextContent(
|
|
|
164
156
|
|
|
165
157
|
/**
|
|
166
158
|
* Recursively finds the first text node in a tree of HTML elements.
|
|
167
|
-
*
|
|
168
|
-
* @param node - The root node to search from
|
|
169
|
-
* @param depth - Current recursion depth (internal use)
|
|
170
|
-
* @returns The first text node found, or null if no text nodes exist
|
|
159
|
+
* Returns null if no text nodes exist.
|
|
171
160
|
*
|
|
172
161
|
* @example
|
|
173
162
|
* ```ts
|
|
@@ -205,7 +194,6 @@ const CLOSER_TO_OPENER: Record<string, string> = { "\u201D": "\u201C" }
|
|
|
205
194
|
* excluded because \u2019 (right single quote) doubles as an apostrophe,
|
|
206
195
|
* making balance-checking unreliable.
|
|
207
196
|
*
|
|
208
|
-
* @param input - The text to validate
|
|
209
197
|
* @throws Error if double quotes are mismatched
|
|
210
198
|
*
|
|
211
199
|
* @example
|
|
@@ -245,20 +233,17 @@ export function assertSmartQuotesMatch(input: string): void {
|
|
|
245
233
|
* 2. Concatenates and transforms the whole paragraph
|
|
246
234
|
* 3. Splits the result back into the original text nodes
|
|
247
235
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* @param options - Optional transform options. Pass `shouldSkipText` here to
|
|
258
|
-
* opt individual text nodes out of transformation without skipping their
|
|
259
|
-
* enclosing element. Skipped text nodes keep their original `.value`.
|
|
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
|
+
*
|
|
260
245
|
* @throws Error if transformation alters the number of text nodes
|
|
261
|
-
* @throws Error if checkInvariance is true and the invariance check fails
|
|
246
|
+
* @throws Error if `checkInvariance` is true and the invariance check fails
|
|
262
247
|
*
|
|
263
248
|
* @example
|
|
264
249
|
* ```ts
|
|
@@ -430,11 +415,7 @@ function hasTextDescendant(
|
|
|
430
415
|
* When an element has block-level children, we recurse so each block
|
|
431
416
|
* is processed independently (e.g., `<div><p>A</p><p>B</p></div>`).
|
|
432
417
|
*
|
|
433
|
-
* @param node - The root element to search from
|
|
434
|
-
* @param shouldSkip - Function to determine which elements to skip
|
|
435
|
-
* @param depth - Current recursion depth (internal use)
|
|
436
418
|
* @param alreadyTransformed - Elements already processed; skipped during traversal to avoid redundant work
|
|
437
|
-
* @returns Array of elements that contain transformable text
|
|
438
419
|
*/
|
|
439
420
|
export function collectTransformableElements(
|
|
440
421
|
node: Element,
|
|
@@ -503,9 +484,6 @@ function markDescendants(node: Element, set: Set<Element>, depth: number = 0): v
|
|
|
503
484
|
/**
|
|
504
485
|
* Rehype plugin that applies punctilio typography transformations to HTML.
|
|
505
486
|
*
|
|
506
|
-
* @param options - Plugin configuration options
|
|
507
|
-
* @returns Unified transformer function
|
|
508
|
-
*
|
|
509
487
|
* @example
|
|
510
488
|
* ```ts
|
|
511
489
|
* import { unified } from 'unified'
|
package/src/remark.ts
CHANGED
|
@@ -32,10 +32,6 @@ const SKIP_TYPES = new Set(["inlineCode", "html"])
|
|
|
32
32
|
/**
|
|
33
33
|
* Recursively collects text nodes from a phrasing content tree,
|
|
34
34
|
* skipping code and HTML nodes.
|
|
35
|
-
*
|
|
36
|
-
* @param node - The phrasing content node to collect from
|
|
37
|
-
* @param depth - Current recursion depth (internal use)
|
|
38
|
-
* @returns Array of Text nodes
|
|
39
35
|
*/
|
|
40
36
|
function flattenTextNodes(
|
|
41
37
|
node: PhrasingContent,
|
|
@@ -66,9 +62,6 @@ function flattenTextNodes(
|
|
|
66
62
|
/**
|
|
67
63
|
* Remark plugin that applies punctilio typography transformations to Markdown.
|
|
68
64
|
*
|
|
69
|
-
* @param options - Plugin configuration options
|
|
70
|
-
* @returns Unified transformer function
|
|
71
|
-
*
|
|
72
65
|
* @example
|
|
73
66
|
* ```ts
|
|
74
67
|
* import { unified } from 'unified'
|
package/src/symbols.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { UNICODE_SYMBOLS, DEFAULT_SEPARATOR, LATIN_LETTERS, wordBoundaryEnd, SPACE_CHARS, cachedRegExp, getEscapedSeparator } from "./constants.js"
|
|
6
|
+
import { namedGroups } from "./utils.js"
|
|
6
7
|
|
|
7
8
|
export interface SymbolOptions {
|
|
8
9
|
/** Boundary marker for HTML element boundaries. Default: "\uE000\uE001" */
|
|
@@ -49,7 +50,7 @@ export function ellipsis(text: string, options: SymbolOptions = {}): string {
|
|
|
49
50
|
// into one ellipsis.
|
|
50
51
|
const pattern = cachedRegExp(`\\.(?:[${SPACE_CHARS}]|(?<sep1>${chr}))?\\.(?:[${SPACE_CHARS}]?|(?<sep2>${chr})?)\\.`, "g")
|
|
51
52
|
text = text.replace(pattern, (...args) => {
|
|
52
|
-
const { sep1, sep2 } =
|
|
53
|
+
const { sep1, sep2 } = namedGroups<{ sep1?: string; sep2?: string }>(args)
|
|
53
54
|
return ELLIPSIS + (sep1 ?? "") + (sep2 ?? "")
|
|
54
55
|
})
|
|
55
56
|
|
|
@@ -94,7 +95,7 @@ export function multiplication(text: string, options: SymbolOptions = {}): strin
|
|
|
94
95
|
)
|
|
95
96
|
|
|
96
97
|
text = text.replace(chainPattern, (...args) => {
|
|
97
|
-
const { firstNum, rest } =
|
|
98
|
+
const { firstNum, rest } = namedGroups<{ firstNum: string; rest: string }>(args)
|
|
98
99
|
// Skip hexadecimal: 0x... or 0X...
|
|
99
100
|
if (firstNum === "0" && /^x/i.test(rest)) return args[0] as string
|
|
100
101
|
|
|
@@ -111,7 +112,13 @@ export function multiplication(text: string, options: SymbolOptions = {}): strin
|
|
|
111
112
|
"gy"
|
|
112
113
|
),
|
|
113
114
|
(...innerArgs) => {
|
|
114
|
-
const groups =
|
|
115
|
+
const groups = namedGroups<{
|
|
116
|
+
pre: string
|
|
117
|
+
spaceBefore: string
|
|
118
|
+
spaceAfter: string
|
|
119
|
+
post: string
|
|
120
|
+
num: string
|
|
121
|
+
}>(innerArgs)
|
|
115
122
|
const space = groups.spaceBefore || groups.spaceAfter ? " " : ""
|
|
116
123
|
return `${groups.pre}${space}${MULTIPLICATION}${space}${groups.post}${groups.num}`
|
|
117
124
|
}
|
|
@@ -164,7 +171,7 @@ export function mathSymbols(text: string, options: SymbolOptions = {}): string {
|
|
|
164
171
|
// and preserves the text-node-count invariant in transformTextNodes.
|
|
165
172
|
const pattern = cachedRegExp(`${left}(?<sep>${chr})?${right}${lookahead(chr)}`, "g")
|
|
166
173
|
text = text.replace(pattern, (...args) => {
|
|
167
|
-
const { sep } =
|
|
174
|
+
const { sep } = namedGroups<{ sep?: string }>(args)
|
|
168
175
|
return `${replacement}${sep ?? ""}`
|
|
169
176
|
})
|
|
170
177
|
}
|
|
@@ -348,7 +355,13 @@ function balancedPrimeReplacer(primeChar: string, escapedSeparator: string) {
|
|
|
348
355
|
// Replace callback: (match, ...captures, offset, fullString, namedGroups)
|
|
349
356
|
return (...args: unknown[]): string => {
|
|
350
357
|
const fullMatch = args[0] as string
|
|
351
|
-
const groups =
|
|
358
|
+
const groups = namedGroups<{
|
|
359
|
+
digit?: string
|
|
360
|
+
sep?: string
|
|
361
|
+
afterSep?: string
|
|
362
|
+
contraction?: string
|
|
363
|
+
trailing?: string
|
|
364
|
+
}>(args)
|
|
352
365
|
|
|
353
366
|
// Contraction (letter + quote + letter): skip entirely
|
|
354
367
|
if (groups.contraction !== undefined) {
|
package/src/utils.ts
CHANGED
|
@@ -40,8 +40,6 @@ export function formatErrorString(content: string, label: string): string {
|
|
|
40
40
|
* Throws if any text node contains the separator character, which would
|
|
41
41
|
* corrupt the split/join mechanism used by the rehype and remark plugins.
|
|
42
42
|
*
|
|
43
|
-
* @param textValues - The raw text values to check
|
|
44
|
-
* @param separator - The separator character
|
|
45
43
|
* @throws Error if the separator is found in any text value
|
|
46
44
|
*/
|
|
47
45
|
export function assertSeparatorAbsent(textValues: string[], separator: string): void {
|
|
@@ -88,9 +86,8 @@ interface TextNode {
|
|
|
88
86
|
* 4. Splits the result back on the separator and writes each fragment
|
|
89
87
|
* back into the corresponding text node.
|
|
90
88
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @param separator - The marker string used to track node boundaries
|
|
89
|
+
* `textNodes` is mutated in place.
|
|
90
|
+
*
|
|
94
91
|
* @throws Error if the transformation alters the number of text nodes
|
|
95
92
|
*/
|
|
96
93
|
export function transformTextNodes(
|
|
@@ -122,10 +119,6 @@ export function transformTextNodes(
|
|
|
122
119
|
* Validates that a transformation preserved the separator count.
|
|
123
120
|
* Throws an error if separators were added or removed.
|
|
124
121
|
*
|
|
125
|
-
* @param original - The original text before transformation
|
|
126
|
-
* @param transformed - The text after transformation
|
|
127
|
-
* @param separator - The separator character to check (default: DEFAULT_SEPARATOR)
|
|
128
|
-
* @param transformName - Name of the transform for error messages (default: "transform")
|
|
129
122
|
* @throws Error if separator count changed
|
|
130
123
|
*/
|
|
131
124
|
export function assertSeparatorCountPreserved(
|
|
@@ -144,3 +137,43 @@ export function assertSeparatorCountPreserved(
|
|
|
144
137
|
)
|
|
145
138
|
}
|
|
146
139
|
}
|
|
140
|
+
|
|
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
|
+
*/
|
|
147
|
+
export function filterUndefined<T extends object>(obj: T): Partial<T> {
|
|
148
|
+
const result: Record<string, unknown> = {}
|
|
149
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
150
|
+
if (v !== undefined) result[k] = v
|
|
151
|
+
}
|
|
152
|
+
return result as Partial<T>
|
|
153
|
+
}
|
|
154
|
+
|
|
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
|
+
*/
|
|
162
|
+
export function stableStringify(obj: object): string {
|
|
163
|
+
const entries = Object.entries(obj)
|
|
164
|
+
.filter(([, v]) => v !== undefined)
|
|
165
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
166
|
+
return JSON.stringify(entries)
|
|
167
|
+
}
|
|
168
|
+
|
|
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
|
+
*/
|
|
177
|
+
export function namedGroups<G>(args: unknown[]): G {
|
|
178
|
+
return args[args.length - 1] as G
|
|
179
|
+
}
|