punctilio 5.0.2 → 5.0.3
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 +1 -1
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +26 -0
- package/dist/constants.js.map +1 -1
- package/dist/dashes.d.ts +3 -6
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +54 -60
- package/dist/dashes.js.map +1 -1
- package/dist/nbsp.d.ts +8 -16
- package/dist/nbsp.d.ts.map +1 -1
- package/dist/nbsp.js +17 -31
- package/dist/nbsp.js.map +1 -1
- package/dist/prose-view.d.ts +13 -0
- package/dist/prose-view.d.ts.map +1 -1
- package/dist/prose-view.js +20 -0
- package/dist/prose-view.js.map +1 -1
- package/dist/quote-classifier.d.ts.map +1 -1
- package/dist/quote-classifier.js +23 -54
- package/dist/quote-classifier.js.map +1 -1
- package/dist/symbols.d.ts +11 -22
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +28 -43
- package/dist/symbols.js.map +1 -1
- package/dist/utils.d.ts +0 -11
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +0 -18
- package/dist/utils.js.map +1 -1
- package/package.json +3 -2
- package/src/constants.ts +32 -0
- package/src/dashes.ts +33 -71
- package/src/nbsp.ts +14 -48
- package/src/prose-view.ts +24 -0
- package/src/quote-classifier.ts +23 -60
- package/src/symbols.ts +18 -67
- package/src/utils.ts +0 -19
package/dist/utils.js
CHANGED
|
@@ -59,22 +59,4 @@ export function stableStringify(obj) {
|
|
|
59
59
|
export function namedGroups(args) {
|
|
60
60
|
return args[args.length - 1];
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Extracts the match offset and input string from a `.replace()` callback's
|
|
64
|
-
* arguments, independent of capture-group count. The trailing arguments are
|
|
65
|
-
* `..., offset, input[, namedGroups]`: the input is the last string (the
|
|
66
|
-
* named-groups object, when present, follows it) and the offset is the
|
|
67
|
-
* number immediately before the input. @internal
|
|
68
|
-
*/
|
|
69
|
-
export function replaceCallbackContext(args) {
|
|
70
|
-
let inputIndex = args.length - 1;
|
|
71
|
-
while (inputIndex >= 0 && typeof args[inputIndex] !== "string") {
|
|
72
|
-
inputIndex--;
|
|
73
|
-
}
|
|
74
|
-
const offset = args[inputIndex - 1];
|
|
75
|
-
if (inputIndex < 1 || typeof offset !== "number") {
|
|
76
|
-
throw new Error("Could not locate offset and input in replace-callback arguments.");
|
|
77
|
-
}
|
|
78
|
-
return { offset, input: args[inputIndex] };
|
|
79
|
-
}
|
|
80
62
|
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "punctilio",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.3",
|
|
4
4
|
"description": "Smart typography transformations: curly quotes, em-dashes, en-dashes, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -164,6 +164,7 @@
|
|
|
164
164
|
"lint": "eslint src scripts",
|
|
165
165
|
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
|
166
166
|
"benchmark": "pnpm run build && node benchmark.mjs",
|
|
167
|
-
"mutation": "STRYKER_RUN=1 stryker run"
|
|
167
|
+
"mutation": "STRYKER_RUN=1 stryker run",
|
|
168
|
+
"mutation:changed": "node scripts/mutate-changed.mjs"
|
|
168
169
|
}
|
|
169
170
|
}
|
package/src/constants.ts
CHANGED
|
@@ -111,6 +111,38 @@ export const SPACE_CHARS = ` \t${UNICODE_SYMBOLS.NBSP}${UNICODE_SYMBOLS.NNBSP}`
|
|
|
111
111
|
/** Non-breaking space chars only (NBSP, NNBSP) for regex `[...]`. */
|
|
112
112
|
export const NBSP_CHARS = `${UNICODE_SYMBOLS.NBSP}${UNICODE_SYMBOLS.NNBSP}`
|
|
113
113
|
|
|
114
|
+
/** Latin letters (incl. European accents) as a compiled regex; see {@link LATIN_LETTERS}. */
|
|
115
|
+
export const LATIN_LETTER_RE = new RegExp(`[${LATIN_LETTERS}]`, "u")
|
|
116
|
+
|
|
117
|
+
/** Single `\w` word-character matcher shared across passes. */
|
|
118
|
+
export const WORD_RE = /\w/
|
|
119
|
+
|
|
120
|
+
/** Matches a single {@link SPACE_CHARS} character (space, tab, NBSP, NNBSP). */
|
|
121
|
+
export const SPACE_CHAR_RE = new RegExp(`[${SPACE_CHARS}]`)
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Glyphs the symbol passes fold word characters into (multiplication `x` →
|
|
125
|
+
* `×`, ordinal suffixes → superscripts). Rules that run before those passes
|
|
126
|
+
* must treat a folded glyph like the word character it folds from, or a gate
|
|
127
|
+
* decided on `6x3` flips once a re-run sees `6×3`.
|
|
128
|
+
*/
|
|
129
|
+
export const FOLDED_WORD_CHARS = new Set<string>([
|
|
130
|
+
UNICODE_SYMBOLS.MULTIPLICATION,
|
|
131
|
+
...UNICODE_SYMBOLS.SUPERSCRIPT_ST, ...UNICODE_SYMBOLS.SUPERSCRIPT_ND,
|
|
132
|
+
...UNICODE_SYMBOLS.SUPERSCRIPT_RD, ...UNICODE_SYMBOLS.SUPERSCRIPT_TH,
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
/** `\w` extended with {@link FOLDED_WORD_CHARS} for fold-stable `\b` checks. */
|
|
136
|
+
export function isWordLike(ch: string | undefined): boolean {
|
|
137
|
+
return ch !== undefined && (WORD_RE.test(ch) || FOLDED_WORD_CHARS.has(ch))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Maximum stacked node boundaries tolerated within a single match before a
|
|
142
|
+
* pass treats the span as crossing a real gap rather than an editing seam.
|
|
143
|
+
*/
|
|
144
|
+
export const MAX_BOUNDARY_SEPARATORS = 3
|
|
145
|
+
|
|
114
146
|
/** Max AST recursion depth; guards against stack overflow from deep nesting. */
|
|
115
147
|
export const MAX_RECURSION_DEPTH = 1000
|
|
116
148
|
|
package/src/dashes.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { cachedRegExp, LATIN_LETTERS, NBSP_CHARS,
|
|
2
|
-
import { boundaryCountAt, overInput, type ProseView, replaceAllInView, type ReplaceAllOptions } from "./prose-view.js"
|
|
1
|
+
import { cachedRegExp, FOLDED_WORD_CHARS, isWordLike, LATIN_LETTER_RE, LATIN_LETTERS, MAX_BOUNDARY_SEPARATORS, NBSP_CHARS, SPACE_CHAR_RE, UNICODE_SYMBOLS } from "./constants.js"
|
|
2
|
+
import { boundaryCountAt, exceedsSingleBoundary, makeProsePass, overInput, type ProseView, replaceAllInView, type ReplaceAllOptions } from "./prose-view.js"
|
|
3
3
|
import { namedGroups } from "./utils.js"
|
|
4
4
|
|
|
5
5
|
export const DASH_STYLES = ["american", "british", "none"] as const
|
|
@@ -13,7 +13,6 @@ export interface DashOptions {
|
|
|
13
13
|
const {
|
|
14
14
|
EN_DASH, EM_DASH, MINUS, MULTIPLICATION, PLUS_MINUS,
|
|
15
15
|
LEFT_DOUBLE_QUOTE, RIGHT_DOUBLE_QUOTE, LEFT_SINGLE_QUOTE, RIGHT_SINGLE_QUOTE,
|
|
16
|
-
SUPERSCRIPT_ST, SUPERSCRIPT_ND, SUPERSCRIPT_RD, SUPERSCRIPT_TH,
|
|
17
16
|
} = UNICODE_SYMBOLS
|
|
18
17
|
|
|
19
18
|
// Prevents false-positive ranges in model names like "Llama-2-7B".
|
|
@@ -34,26 +33,6 @@ const months: readonly string[] = [
|
|
|
34
33
|
|
|
35
34
|
const monthPattern = months.join("|")
|
|
36
35
|
|
|
37
|
-
const WORD_CHAR_RE = /\w/
|
|
38
|
-
const LATIN_LETTER_RE = new RegExp(`[${LATIN_LETTERS}]`)
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Glyphs the symbol passes fold word characters into (multiplication `x` →
|
|
42
|
-
* `×`, ordinal suffixes → superscripts). The dash rules run before those
|
|
43
|
-
* passes, so their word-character tests must treat a folded glyph like the
|
|
44
|
-
* word character it folds from — otherwise a range blocked at `6x3` converts
|
|
45
|
-
* once a re-run sees `6×3`.
|
|
46
|
-
*/
|
|
47
|
-
const FOLDED_WORD_CHARS = new Set<string>([
|
|
48
|
-
MULTIPLICATION,
|
|
49
|
-
...SUPERSCRIPT_ST, ...SUPERSCRIPT_ND, ...SUPERSCRIPT_RD, ...SUPERSCRIPT_TH,
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
/** `\w` extended with {@link FOLDED_WORD_CHARS} for fold-stable `\b` checks. */
|
|
53
|
-
function isWordChar(ch: string | undefined): boolean {
|
|
54
|
-
return ch !== undefined && (WORD_CHAR_RE.test(ch) || FOLDED_WORD_CHARS.has(ch))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
36
|
/**
|
|
58
37
|
* Glyphs the math-symbol pass folds two-character operators into (`=~` → `≈`,
|
|
59
38
|
* `!=` → `≠`, ...). The fold tolerates one node boundary at its junction, so
|
|
@@ -101,7 +80,7 @@ function pass(
|
|
|
101
80
|
//
|
|
102
81
|
// The boundary-tolerance positions throughout this module reproduce the
|
|
103
82
|
// element-boundary semantics of the pre-v5 sentinel-marked pipeline; they are
|
|
104
|
-
// pinned by the
|
|
83
|
+
// pinned by the HTML regression corpus and the migration's differential fuzz. Over a
|
|
105
84
|
// clean ProseView, `replaceAllInView` skips any match with an interior boundary
|
|
106
85
|
// unless `allowBoundaries` opts in. Each pass below builds an `allowBoundaries`
|
|
107
86
|
// callback that admits a match only when every interior boundary sits at a
|
|
@@ -142,9 +121,6 @@ function interiorBoundariesAllowed(
|
|
|
142
121
|
return true
|
|
143
122
|
}
|
|
144
123
|
|
|
145
|
-
/** Separators tolerated by the word-boundary helpers (mirrors constants.ts). */
|
|
146
|
-
const MAX_BOUNDARY_SEPARATORS = 3
|
|
147
|
-
|
|
148
124
|
/**
|
|
149
125
|
* Reproduces `wordBoundaryStart` = `(?<!\w${sep}{0,3})\b` at the marked position
|
|
150
126
|
* `matchStart` (the start char is a word char — a digit). The `\b` requires a
|
|
@@ -155,7 +131,7 @@ const MAX_BOUNDARY_SEPARATORS = 3
|
|
|
155
131
|
function wordBoundaryStartOk(view: ProseView, matchStart: number): boolean {
|
|
156
132
|
if (matchStart === 0) return true
|
|
157
133
|
const nb = boundaryCountAt(view, matchStart)
|
|
158
|
-
const prevWord =
|
|
134
|
+
const prevWord = isWordLike(view.text[matchStart - 1])
|
|
159
135
|
if (nb === 0) return !prevWord // `\b` needs a non-word char immediately left
|
|
160
136
|
// A separator sits left (non-word) so `\b` holds; the lookbehind blocks only
|
|
161
137
|
// when a word char is within ≤MAX boundaries.
|
|
@@ -172,7 +148,7 @@ function wordBoundaryEndOk(view: ProseView, pos: number): boolean {
|
|
|
172
148
|
const text = view.text
|
|
173
149
|
// `wordBoundaryEndOk` is only ever called at the end of a digit, letter, or
|
|
174
150
|
// suffix run, so `pos` is always ≥ 1 and `text[pos - 1]` is a real char.
|
|
175
|
-
const leftWord =
|
|
151
|
+
const leftWord = isWordLike(text[pos - 1])
|
|
176
152
|
// A multiplication sign one space to the right blocks like the word char
|
|
177
153
|
// it folds from: the multiplication pass renders `5X 8` as `5 × 8`, and a
|
|
178
154
|
// range blocked at `3-5X` must stay blocked at `3-5 ×`.
|
|
@@ -180,7 +156,7 @@ function wordBoundaryEndOk(view: ProseView, pos: number): boolean {
|
|
|
180
156
|
return false
|
|
181
157
|
}
|
|
182
158
|
const nb = boundaryCountAt(view, pos)
|
|
183
|
-
const nextCleanWord =
|
|
159
|
+
const nextCleanWord = isWordLike(text[pos])
|
|
184
160
|
// The marked char right after pos is a separator when nb > 0 (non-word).
|
|
185
161
|
const rightWord = nb > 0 ? false : nextCleanWord
|
|
186
162
|
if (leftWord === rightWord) return false // no `\b`
|
|
@@ -195,14 +171,10 @@ function wordBoundaryEndOk(view: ProseView, pos: number): boolean {
|
|
|
195
171
|
const CURRENCY_RE = /[$€£¥₹]/
|
|
196
172
|
|
|
197
173
|
/** Convert number ranges to en-dash (e.g., "1-5" → "1–5"). */
|
|
198
|
-
export
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
convertPositiveRanges(view)
|
|
203
|
-
convertNegativeRanges(view)
|
|
204
|
-
})
|
|
205
|
-
}
|
|
174
|
+
export const enDashNumberRange = makeProsePass((view) => {
|
|
175
|
+
convertPositiveRanges(view)
|
|
176
|
+
convertNegativeRanges(view)
|
|
177
|
+
})
|
|
206
178
|
|
|
207
179
|
/**
|
|
208
180
|
* The positive-range shape is `phoneAreaCode? \b (?:p\.?|[currency])?\d[\d.,]*
|
|
@@ -276,13 +248,13 @@ function matchRangeBody(view: ProseView, startStart: number): number | null {
|
|
|
276
248
|
if (start === null) return null
|
|
277
249
|
const startSpanEnd = startStart + start.length
|
|
278
250
|
// One slot between start and the dash tolerates a single boundary there.
|
|
279
|
-
if (
|
|
251
|
+
if (exceedsSingleBoundary(view, startSpanEnd)) return null
|
|
280
252
|
// Dash run of 1..3 hyphens that never spans an interior boundary (a boundary
|
|
281
253
|
// between two hyphens truncates the run).
|
|
282
254
|
if (text[startSpanEnd] !== "-") return null
|
|
283
255
|
let dashEnd = startSpanEnd + 1
|
|
284
256
|
while (text[dashEnd] === "-" && dashEnd - startSpanEnd < 3 && !view.hasBoundary(dashEnd)) dashEnd++
|
|
285
|
-
if (
|
|
257
|
+
if (exceedsSingleBoundary(view, dashEnd)) return null
|
|
286
258
|
|
|
287
259
|
// `readStartRun` already stops at the first interior boundary, so the start
|
|
288
260
|
// span is boundary-free and its clean digits are the whole run.
|
|
@@ -431,7 +403,7 @@ function matchNegativeRangeAt(view: ProseView, i: number): number | null {
|
|
|
431
403
|
let startEnd = i + 1
|
|
432
404
|
while (/[\d.,]/.test(text[startEnd] ?? "") && !view.hasBoundary(startEnd)) startEnd++
|
|
433
405
|
// One slot between start and the dash tolerates a single boundary there.
|
|
434
|
-
if (
|
|
406
|
+
if (exceedsSingleBoundary(view, startEnd)) return null
|
|
435
407
|
if (text[startEnd] !== "-") return null
|
|
436
408
|
|
|
437
409
|
// `-{1,3}?` then optional `(?<neg>-)`: a 1..3 hyphen run with no interior
|
|
@@ -442,7 +414,7 @@ function matchNegativeRangeAt(view: ProseView, i: number): number | null {
|
|
|
442
414
|
const hasNeg = dashRunLen >= 2
|
|
443
415
|
const negEnd = dashRunEnd
|
|
444
416
|
// The `end` head slot tolerates one boundary.
|
|
445
|
-
if (
|
|
417
|
+
if (exceedsSingleBoundary(view, negEnd)) return null
|
|
446
418
|
|
|
447
419
|
const end = readNegEndRun(view, negEnd)
|
|
448
420
|
if (end === null) return null
|
|
@@ -487,11 +459,11 @@ function followingCandidates(view: ProseView, endSpanEnd: number, allowMinus: bo
|
|
|
487
459
|
const candidates = [endSpanEnd]
|
|
488
460
|
let pos = endSpanEnd
|
|
489
461
|
for (;;) {
|
|
490
|
-
if (
|
|
462
|
+
if (exceedsSingleBoundary(view, pos)) break
|
|
491
463
|
const dashCh = view.text[pos]
|
|
492
464
|
if (dashCh !== "-" && !(allowMinus && dashCh === MINUS)) break
|
|
493
465
|
const digitStart = pos + 1
|
|
494
|
-
if (
|
|
466
|
+
if (exceedsSingleBoundary(view, digitStart)) break
|
|
495
467
|
if (!/\d/.test(view.text[digitStart] ?? "")) break
|
|
496
468
|
let end = digitStart + 1
|
|
497
469
|
while (/\d/.test(view.text[end] ?? "") && !view.hasBoundary(end)) end++
|
|
@@ -521,7 +493,7 @@ function suffixWbeEnd(view: ProseView, pos: number, allowAmPm: boolean): number
|
|
|
521
493
|
// An optional suffix: one tolerated boundary then am/pm or one of
|
|
522
494
|
// {@link RANGE_SUFFIX_RE}'s letters, followed by wbe. The suffix letters sit
|
|
523
495
|
// at the clean `pos`.
|
|
524
|
-
if (
|
|
496
|
+
if (exceedsSingleBoundary(view, pos)) return null
|
|
525
497
|
const text = view.text
|
|
526
498
|
// `[AaPp][Mm]` is contiguous — a boundary between the two letters breaks the
|
|
527
499
|
// suffix.
|
|
@@ -536,8 +508,6 @@ function suffixWbeEnd(view: ProseView, pos: number, allowAmPm: boolean): number
|
|
|
536
508
|
|
|
537
509
|
const NOT_AFTER_DASH_RE = new RegExp(`[${DISALLOWED_PREFIX_CLASS_FRAGMENT}${LATIN_LETTERS}${MULTIPLICATION}${PLUS_MINUS}.+]`)
|
|
538
510
|
|
|
539
|
-
const SPACE_CHAR_RE = new RegExp(`[${SPACE_CHARS}]`)
|
|
540
|
-
|
|
541
511
|
/**
|
|
542
512
|
* Offset of the dot reachable backwards across one inter-dot gap ending at the
|
|
543
513
|
* dot at `dotIndex` (the mirror of the ellipsis pass's forward gap rule: the
|
|
@@ -706,15 +676,11 @@ function enDashDateRangeOverView(view: ProseView, dashStyle: DashStyle): void {
|
|
|
706
676
|
// ---------------------------------------------------------------------------
|
|
707
677
|
|
|
708
678
|
/** Convert hyphens to minus signs in numeric contexts (e.g., "-5" → "−5"). */
|
|
709
|
-
export
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
minusSpacedSubtraction(view)
|
|
715
|
-
minusDirectNegative(view)
|
|
716
|
-
})
|
|
717
|
-
}
|
|
679
|
+
export const minusReplace = makeProsePass((view) => {
|
|
680
|
+
minusSubtractionOfNegative(view)
|
|
681
|
+
minusSpacedSubtraction(view)
|
|
682
|
+
minusDirectNegative(view)
|
|
683
|
+
})
|
|
718
684
|
|
|
719
685
|
/** Pattern 1a/1b: spaced math subtraction after a digit (`5 - 3`, `5 - -3`). */
|
|
720
686
|
function minusSubtractionOfNegative(view: ProseView): void {
|
|
@@ -764,13 +730,13 @@ const NUM_BODY_RE = /^\d*\.?\d+/
|
|
|
764
730
|
function minusSubtractionPrefixOk(match: RegExpExecArray, view: ProseView, prefixLen: number): boolean {
|
|
765
731
|
// The slot in the lookbehind (`(?<=\d)`) sits at the match start between the
|
|
766
732
|
// leading digit and the space; it tolerates at most one boundary.
|
|
767
|
-
if (
|
|
733
|
+
if (exceedsSingleBoundary(view, match.index)) return false
|
|
768
734
|
const numStart = match.index + prefixLen
|
|
769
735
|
for (const b of view.boundaries) {
|
|
770
736
|
if (b > match.index && b < numStart) return false
|
|
771
737
|
}
|
|
772
738
|
// The slot before `num` tolerates at most one boundary at the num head.
|
|
773
|
-
if (
|
|
739
|
+
if (exceedsSingleBoundary(view, numStart)) return false
|
|
774
740
|
// `num` stops at the first interior boundary; the clean run up to it must
|
|
775
741
|
// still satisfy `\d*\.?\d+` (the mandatory trailing `\d+`).
|
|
776
742
|
const numEnd = match.index + match[0].length
|
|
@@ -1047,8 +1013,8 @@ function spacedArrowAhead(view: ProseView, runEnd: number): boolean {
|
|
|
1047
1013
|
// No hyphens: both slots fall at this offset (≤2 boundaries total).
|
|
1048
1014
|
if (boundaryCountAt(view, runEnd) > 2) return false
|
|
1049
1015
|
} else {
|
|
1050
|
-
if (
|
|
1051
|
-
if (
|
|
1016
|
+
if (exceedsSingleBoundary(view, runEnd)) return false // first slot
|
|
1017
|
+
if (exceedsSingleBoundary(view, h)) return false // second slot
|
|
1052
1018
|
}
|
|
1053
1019
|
// `≥` guards like the `>=` it folds from: the math pass folds the `>` this
|
|
1054
1020
|
// guard keys on, and a dash blocked before `–>=` must stay blocked at `–≥`.
|
|
@@ -1185,8 +1151,8 @@ function convertUnspacedEmDashes(view: ProseView, rendered: string): void {
|
|
|
1185
1151
|
(match, v) => {
|
|
1186
1152
|
// One boundary is tolerated on each side of the em-dash; two or more
|
|
1187
1153
|
// exceed the slot and block the match.
|
|
1188
|
-
if (
|
|
1189
|
-
if (
|
|
1154
|
+
if (exceedsSingleBoundary(v, match.index)) return null
|
|
1155
|
+
if (exceedsSingleBoundary(v, match.index + match[0].length)) return null
|
|
1190
1156
|
v.replace(match.index, match.index + match[0].length, rendered)
|
|
1191
1157
|
return null
|
|
1192
1158
|
},
|
|
@@ -1250,7 +1216,7 @@ function preserveLineLeadingEmDashSpace(view: ProseView): void {
|
|
|
1250
1216
|
(match, v) => {
|
|
1251
1217
|
// One boundary is tolerated between the line start and the em-dash; two or
|
|
1252
1218
|
// more exceed that slot and block the match.
|
|
1253
|
-
if (
|
|
1219
|
+
if (exceedsSingleBoundary(v, match.index)) return null
|
|
1254
1220
|
const groups = namedGroups<{ after: string }>(matchArgs(match))
|
|
1255
1221
|
return `${EM_DASH} ${groups.after}`
|
|
1256
1222
|
},
|
|
@@ -1292,14 +1258,10 @@ const { WORD_JOINER } = UNICODE_SYMBOLS
|
|
|
1292
1258
|
* `nbspTransform` (U+00A0 breaks `Fig. 1` searches). Apply only in rendered
|
|
1293
1259
|
* HTML; do not write into Markdown source.
|
|
1294
1260
|
*/
|
|
1295
|
-
export
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
const re = cachedRegExp(`(?<=[^\\s${WORD_JOINER}])[${EM_DASH}${EN_DASH}]`, "gu")
|
|
1300
|
-
replaceAllInView(view, re, (match) => `${WORD_JOINER}${match[0]}`)
|
|
1301
|
-
})
|
|
1302
|
-
}
|
|
1261
|
+
export const dashWordJoiner = makeProsePass((view) => {
|
|
1262
|
+
const re = cachedRegExp(`(?<=[^\\s${WORD_JOINER}])[${EM_DASH}${EN_DASH}]`, "gu")
|
|
1263
|
+
replaceAllInView(view, re, (match) => `${WORD_JOINER}${match[0]}`)
|
|
1264
|
+
})
|
|
1303
1265
|
|
|
1304
1266
|
/** Reconstructs `.replace()`-style callback args from a RegExpExecArray. */
|
|
1305
1267
|
function matchArgs(match: RegExpExecArray): unknown[] {
|
package/src/nbsp.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { cachedRegExp, LATIN_LETTERS, NBSP_CHARS, SPACE_CHARS, UNICODE_SYMBOLS } from "./constants.js"
|
|
2
|
-
import { boundaryCountAt, overInput, type ProseView, replaceAllInView } from "./prose-view.js"
|
|
1
|
+
import { cachedRegExp, LATIN_LETTER_RE, LATIN_LETTERS, NBSP_CHARS, SPACE_CHARS, UNICODE_SYMBOLS } from "./constants.js"
|
|
2
|
+
import { boundaryCountAt, exceedsSingleBoundary, makeProsePass, overInput, type ProseView, replaceAllInView } from "./prose-view.js"
|
|
3
3
|
|
|
4
4
|
const {
|
|
5
5
|
NBSP,
|
|
@@ -95,8 +95,8 @@ const COPYRIGHT_SYMBOLS = `[${COPYRIGHT}${REGISTERED}${TRADEMARK}]`
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
// The boundary-tolerance positions in this module reproduce the element-boundary
|
|
98
|
-
// semantics of the pre-v5 sentinel-marked pipeline; they are pinned by the
|
|
99
|
-
// corpus and the migration's differential fuzz.
|
|
98
|
+
// semantics of the pre-v5 sentinel-marked pipeline; they are pinned by the HTML
|
|
99
|
+
// regression corpus and the migration's differential fuzz.
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* A tolerated slot admits exactly ONE node boundary at that position; two
|
|
@@ -110,18 +110,14 @@ function boundariesOnlyAtSlots(view: ProseView, start: number, end: number, slot
|
|
|
110
110
|
if (!slots.includes(boundary)) return false
|
|
111
111
|
}
|
|
112
112
|
for (const slot of slots) {
|
|
113
|
-
if (
|
|
113
|
+
if (exceedsSingleBoundary(view, slot)) return false
|
|
114
114
|
}
|
|
115
115
|
return true
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Skips when preceded by NBSP or back-to-back with the previous match,
|
|
119
119
|
// preventing 3+ words from binding into a single line-break atom.
|
|
120
|
-
export
|
|
121
|
-
export function nbspAfterShortWords(input: ProseView): void
|
|
122
|
-
export function nbspAfterShortWords(input: string | ProseView): string | void {
|
|
123
|
-
return overInput(input, nbspAfterShortWordsOverView)
|
|
124
|
-
}
|
|
120
|
+
export const nbspAfterShortWords = makeProsePass(nbspAfterShortWordsOverView)
|
|
125
121
|
|
|
126
122
|
function nbspAfterShortWordsOverView(view: ProseView): void {
|
|
127
123
|
const pattern = cachedRegExp(
|
|
@@ -157,11 +153,7 @@ function nbspAfterShortWordsOverView(view: ProseView): void {
|
|
|
157
153
|
view.commit()
|
|
158
154
|
}
|
|
159
155
|
|
|
160
|
-
export
|
|
161
|
-
export function nbspBetweenNumberAndUnit(input: ProseView): void
|
|
162
|
-
export function nbspBetweenNumberAndUnit(input: string | ProseView): string | void {
|
|
163
|
-
return overInput(input, nbspBetweenNumberAndUnitOverView)
|
|
164
|
-
}
|
|
156
|
+
export const nbspBetweenNumberAndUnit = makeProsePass(nbspBetweenNumberAndUnitOverView)
|
|
165
157
|
|
|
166
158
|
function nbspBetweenNumberAndUnitOverView(view: ProseView): void {
|
|
167
159
|
const pattern = cachedRegExp(
|
|
@@ -198,8 +190,6 @@ const MAX_LAST_WORD_LENGTH = 10
|
|
|
198
190
|
// Bounded so the lookbehind stays ReDoS-safe.
|
|
199
191
|
const MAX_MIDDLE_WORD_LENGTH = 15
|
|
200
192
|
|
|
201
|
-
const LATIN_LETTER_RE = cachedRegExp(`[${LATIN_LETTERS}]`, "u")
|
|
202
|
-
|
|
203
193
|
/**
|
|
204
194
|
* The cascade lookbehind `(?<![NBSP][LATIN]{1,15})` over clean text: blocks
|
|
205
195
|
* when an NBSP/NNBSP is followed by 1..15 Latin letters ending at the space. A
|
|
@@ -219,11 +209,7 @@ function cascadeBlocksLastWord(view: ProseView, spaceOffset: number): boolean {
|
|
|
219
209
|
}
|
|
220
210
|
|
|
221
211
|
// Skips when the second-to-last word is already glued backwards via NBSP.
|
|
222
|
-
export
|
|
223
|
-
export function nbspBeforeLastWord(input: ProseView): void
|
|
224
|
-
export function nbspBeforeLastWord(input: string | ProseView): string | void {
|
|
225
|
-
return overInput(input, nbspBeforeLastWordOverView)
|
|
226
|
-
}
|
|
212
|
+
export const nbspBeforeLastWord = makeProsePass(nbspBeforeLastWordOverView)
|
|
227
213
|
|
|
228
214
|
function nbspBeforeLastWordOverView(view: ProseView): void {
|
|
229
215
|
// The `(?<=\w|boundary)` lookbehind and the `[NBSP][LATIN]{1,15}` cascade
|
|
@@ -248,7 +234,7 @@ function nbspBeforeLastWordOverView(view: ProseView): void {
|
|
|
248
234
|
// `\n\n`/end-of-text; two adjacent boundaries block the match. The
|
|
249
235
|
// end-of-text slot sits at the match end (never an interior boundary),
|
|
250
236
|
// so the cap is checked here rather than only in allowBoundaries.
|
|
251
|
-
if (
|
|
237
|
+
if (exceedsSingleBoundary(v, end - match.groups!.ending.length)) return null
|
|
252
238
|
// Edit only the leading space so the lastWord/ending (and any boundary
|
|
253
239
|
// before \n\n) keep their positions.
|
|
254
240
|
v.replace(start, start + 1, NBSP)
|
|
@@ -332,35 +318,15 @@ function nbspBetweenInitialsOverView(view: ProseView): void {
|
|
|
332
318
|
nbspBeforeContext(view, `${UNICODE_UPPERCASE}\\.`, UNICODE_UPPERCASE, "gu", "initial")
|
|
333
319
|
}
|
|
334
320
|
|
|
335
|
-
export
|
|
336
|
-
export function nbspAfterReferenceAbbreviations(input: ProseView): void
|
|
337
|
-
export function nbspAfterReferenceAbbreviations(input: string | ProseView): string | void {
|
|
338
|
-
return overInput(input, nbspAfterReferenceAbbreviationsOverView)
|
|
339
|
-
}
|
|
321
|
+
export const nbspAfterReferenceAbbreviations = makeProsePass(nbspAfterReferenceAbbreviationsOverView)
|
|
340
322
|
|
|
341
|
-
export
|
|
342
|
-
export function nbspAfterSectionSymbols(input: ProseView): void
|
|
343
|
-
export function nbspAfterSectionSymbols(input: string | ProseView): string | void {
|
|
344
|
-
return overInput(input, nbspAfterSectionSymbolsOverView)
|
|
345
|
-
}
|
|
323
|
+
export const nbspAfterSectionSymbols = makeProsePass(nbspAfterSectionSymbolsOverView)
|
|
346
324
|
|
|
347
|
-
export
|
|
348
|
-
export function nbspAfterHonorifics(input: ProseView): void
|
|
349
|
-
export function nbspAfterHonorifics(input: string | ProseView): string | void {
|
|
350
|
-
return overInput(input, nbspAfterHonorificsOverView)
|
|
351
|
-
}
|
|
325
|
+
export const nbspAfterHonorifics = makeProsePass(nbspAfterHonorificsOverView)
|
|
352
326
|
|
|
353
|
-
export
|
|
354
|
-
export function nbspAfterCopyrightSymbols(input: ProseView): void
|
|
355
|
-
export function nbspAfterCopyrightSymbols(input: string | ProseView): string | void {
|
|
356
|
-
return overInput(input, nbspAfterCopyrightSymbolsOverView)
|
|
357
|
-
}
|
|
327
|
+
export const nbspAfterCopyrightSymbols = makeProsePass(nbspAfterCopyrightSymbolsOverView)
|
|
358
328
|
|
|
359
|
-
export
|
|
360
|
-
export function nbspBetweenInitials(input: ProseView): void
|
|
361
|
-
export function nbspBetweenInitials(input: string | ProseView): string | void {
|
|
362
|
-
return overInput(input, nbspBetweenInitialsOverView)
|
|
363
|
-
}
|
|
329
|
+
export const nbspBetweenInitials = makeProsePass(nbspBetweenInitialsOverView)
|
|
364
330
|
|
|
365
331
|
// Specific patterns first so they claim matches before generic nbspAfterShortWords.
|
|
366
332
|
const NBSP_TRANSFORMS: ((view: ProseView) => void)[] = [
|
package/src/prose-view.ts
CHANGED
|
@@ -244,6 +244,15 @@ export function boundaryCountAt(view: ProseView, offset: number): number {
|
|
|
244
244
|
return count
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* True when more than one node boundary is stacked at `offset` — i.e. the
|
|
249
|
+
* position exceeds the single-boundary tolerance the passes allow in an
|
|
250
|
+
* editing slot.
|
|
251
|
+
*/
|
|
252
|
+
export function exceedsSingleBoundary(view: ProseView, offset: number): boolean {
|
|
253
|
+
return boundaryCountAt(view, offset) > 1
|
|
254
|
+
}
|
|
255
|
+
|
|
247
256
|
export function replaceAllInView(
|
|
248
257
|
view: ProseView,
|
|
249
258
|
regex: RegExp,
|
|
@@ -518,3 +527,18 @@ export function overInput(input: string | ProseView, run: (view: ProseView) => v
|
|
|
518
527
|
run(input)
|
|
519
528
|
input.commit()
|
|
520
529
|
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Wraps a view runner as a dual-input {@link ProsePass}: the returned function
|
|
533
|
+
* transforms a string and returns it, or edits a ProseView in place. Collapses
|
|
534
|
+
* the string/ProseView overload triple that every zero-option public pass would
|
|
535
|
+
* otherwise repeat.
|
|
536
|
+
*/
|
|
537
|
+
export function makeProsePass(run: (view: ProseView) => void): ProsePass {
|
|
538
|
+
function pass(input: string): string
|
|
539
|
+
function pass(input: ProseView): void
|
|
540
|
+
function pass(input: string | ProseView): string | void {
|
|
541
|
+
return overInput(input, run)
|
|
542
|
+
}
|
|
543
|
+
return pass
|
|
544
|
+
}
|