punctilio 5.2.0 → 5.2.2
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 +6 -6
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/rehype.d.ts +31 -9
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +132 -30
- package/dist/rehype.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/rehype.ts +413 -205
package/src/rehype.ts
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import type {
|
|
2
|
+
Element,
|
|
3
|
+
ElementContent,
|
|
4
|
+
Parent,
|
|
5
|
+
Root,
|
|
6
|
+
RootContent,
|
|
7
|
+
Text,
|
|
8
|
+
} from "hast";
|
|
9
|
+
import type { Transformer } from "unified";
|
|
10
|
+
|
|
11
|
+
import { SKIP, visitParents } from "unist-util-visit-parents";
|
|
12
|
+
|
|
13
|
+
import { transformView } from "./index.js";
|
|
14
|
+
import {
|
|
15
|
+
TRANSFORM_OPTION_KEYS,
|
|
16
|
+
type TransformOptions,
|
|
17
|
+
} from "./transform-options.js";
|
|
18
|
+
import { MAX_RECURSION_DEPTH } from "./constants.js";
|
|
19
|
+
import { assertKnownOptionKeys, formatErrorString } from "./utils.js";
|
|
20
|
+
import {
|
|
21
|
+
buildProseView,
|
|
22
|
+
type ProsePass,
|
|
23
|
+
type ProseView,
|
|
24
|
+
splitAtIndices,
|
|
25
|
+
} from "./prose-view.js";
|
|
26
|
+
|
|
27
|
+
export type ElementPredicate = (node: Element) => boolean;
|
|
13
28
|
|
|
14
29
|
/** Per-text-node skip predicate, called after element-level `shouldSkip`. */
|
|
15
30
|
export type TextNodeSkipPredicate = (
|
|
16
31
|
textNode: Text,
|
|
17
32
|
ancestors: readonly Element[],
|
|
18
|
-
) => boolean
|
|
33
|
+
) => boolean;
|
|
19
34
|
|
|
20
35
|
export interface ElementTransformOptions {
|
|
21
36
|
/**
|
|
@@ -24,19 +39,18 @@ export interface ElementTransformOptions {
|
|
|
24
39
|
* not passed to the transform function, and its `.value` is left
|
|
25
40
|
* untouched. Applied after element-level `shouldSkip`.
|
|
26
41
|
*/
|
|
27
|
-
shouldSkipText?: TextNodeSkipPredicate
|
|
42
|
+
shouldSkipText?: TextNodeSkipPredicate;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
export interface RehypePunctilioOptions
|
|
31
|
-
extends TransformOptions,
|
|
32
|
-
ElementTransformOptions {
|
|
46
|
+
extends TransformOptions, ElementTransformOptions {
|
|
33
47
|
/**
|
|
34
48
|
* HTML tag names to skip when applying transformations.
|
|
35
49
|
* Content inside these elements won't have formatting improvements applied.
|
|
36
50
|
*
|
|
37
51
|
* Default: ["code", "pre", "script", "style", "kbd", "var", "samp", "template", "math", "svg"]
|
|
38
52
|
*/
|
|
39
|
-
skipTags?: string[]
|
|
53
|
+
skipTags?: string[];
|
|
40
54
|
|
|
41
55
|
/**
|
|
42
56
|
* CSS class names that indicate content should skip formatting.
|
|
@@ -45,7 +59,7 @@ export interface RehypePunctilioOptions
|
|
|
45
59
|
*
|
|
46
60
|
* Default: []
|
|
47
61
|
*/
|
|
48
|
-
skipClasses?: string[]
|
|
62
|
+
skipClasses?: string[];
|
|
49
63
|
|
|
50
64
|
/**
|
|
51
65
|
* Invert the element model: transform text inside every element except the
|
|
@@ -56,95 +70,136 @@ export interface RehypePunctilioOptions
|
|
|
56
70
|
*
|
|
57
71
|
* Default: false
|
|
58
72
|
*/
|
|
59
|
-
transformAllElements?: boolean
|
|
73
|
+
transformAllElements?: boolean;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
|
-
const DEFAULT_SKIP_TAGS = [
|
|
76
|
+
const DEFAULT_SKIP_TAGS = [
|
|
77
|
+
"code",
|
|
78
|
+
"pre",
|
|
79
|
+
"script",
|
|
80
|
+
"style",
|
|
81
|
+
"kbd",
|
|
82
|
+
"var",
|
|
83
|
+
"samp",
|
|
84
|
+
"template",
|
|
85
|
+
"math",
|
|
86
|
+
"svg",
|
|
87
|
+
];
|
|
63
88
|
|
|
64
89
|
// Form elements whose text content is a literal control value, not prose.
|
|
65
90
|
// Hard-skipped (whole subtree) under `transformAllElements`, on top of the
|
|
66
91
|
// user skip-list, so their values are protected even when nested inside a
|
|
67
92
|
// transformable element.
|
|
68
|
-
const FORM_VALUE_TAGS = ["textarea", "input"]
|
|
93
|
+
const FORM_VALUE_TAGS = ["textarea", "input"];
|
|
69
94
|
|
|
70
95
|
// `<select>` is not prose itself, but its `<option>` children are (and are
|
|
71
96
|
// transformed under the default allowlist). Under `transformAllElements` we
|
|
72
97
|
// exclude `select` as a transform *unit* while still recursing into — and
|
|
73
98
|
// transforming — its options, so the inverted mode never transforms less than
|
|
74
99
|
// the allowlist.
|
|
75
|
-
const NON_PROSE_CONTAINER_TAGS = new Set(["select"])
|
|
100
|
+
const NON_PROSE_CONTAINER_TAGS = new Set(["select"]);
|
|
76
101
|
|
|
77
102
|
/** Option keys handled by `rehypePunctilio` itself rather than `transform()`. */
|
|
78
|
-
export const REHYPE_ONLY_OPTION_KEYS: readonly string[] = [
|
|
103
|
+
export const REHYPE_ONLY_OPTION_KEYS: readonly string[] = [
|
|
104
|
+
"skipTags",
|
|
105
|
+
"skipClasses",
|
|
106
|
+
"shouldSkipText",
|
|
107
|
+
"transformAllElements",
|
|
108
|
+
];
|
|
79
109
|
|
|
80
110
|
/** Runtime list of valid `rehypePunctilio` option keys. */
|
|
81
|
-
export const REHYPE_OPTION_KEYS: readonly string[] = [
|
|
111
|
+
export const REHYPE_OPTION_KEYS: readonly string[] = [
|
|
112
|
+
...TRANSFORM_OPTION_KEYS,
|
|
113
|
+
...REHYPE_ONLY_OPTION_KEYS,
|
|
114
|
+
];
|
|
82
115
|
|
|
83
116
|
// Void/replaced inline elements that render atomic content (or a hard break)
|
|
84
117
|
// and carry no transformable text. When one sits between two text nodes it is a
|
|
85
118
|
// real visual separator, so the flattener records an opaque gap there.
|
|
86
119
|
const OPAQUE_VOID_TAGS = new Set([
|
|
87
|
-
"img",
|
|
88
|
-
"
|
|
89
|
-
|
|
120
|
+
"img",
|
|
121
|
+
"br",
|
|
122
|
+
"wbr",
|
|
123
|
+
"input",
|
|
124
|
+
"hr",
|
|
125
|
+
"embed",
|
|
126
|
+
"area",
|
|
127
|
+
"source",
|
|
128
|
+
"track",
|
|
129
|
+
"col",
|
|
130
|
+
"param",
|
|
131
|
+
"keygen",
|
|
132
|
+
]);
|
|
90
133
|
|
|
91
134
|
/** Flattened text nodes plus the opaque gaps that fell between adjacent ones. */
|
|
92
135
|
export interface FlattenedProse {
|
|
93
|
-
nodes: Text[]
|
|
136
|
+
nodes: Text[];
|
|
94
137
|
/** Indices into `nodes` (1..n-1) with removed opaque content immediately before them. */
|
|
95
|
-
opaqueBefore: Set<number
|
|
138
|
+
opaqueBefore: Set<number>;
|
|
96
139
|
}
|
|
97
140
|
|
|
98
141
|
interface ProseCollector {
|
|
99
|
-
nodes: Text[]
|
|
100
|
-
opaqueBefore: Set<number
|
|
101
|
-
shouldSkip: ElementPredicate
|
|
102
|
-
shouldSkipText: TextNodeSkipPredicate | undefined
|
|
103
|
-
ancestors: Element[] | null
|
|
142
|
+
nodes: Text[];
|
|
143
|
+
opaqueBefore: Set<number>;
|
|
144
|
+
shouldSkip: ElementPredicate;
|
|
145
|
+
shouldSkipText: TextNodeSkipPredicate | undefined;
|
|
146
|
+
ancestors: Element[] | null;
|
|
104
147
|
// An opaque element was dropped since the last emitted text node; the next
|
|
105
148
|
// emitted node records an opaque gap before it.
|
|
106
|
-
pendingOpaque: boolean
|
|
149
|
+
pendingOpaque: boolean;
|
|
107
150
|
}
|
|
108
151
|
|
|
109
|
-
function collectProse(
|
|
152
|
+
function collectProse(
|
|
153
|
+
node: Element | ElementContent,
|
|
154
|
+
depth: number,
|
|
155
|
+
c: ProseCollector,
|
|
156
|
+
): void {
|
|
110
157
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
111
|
-
return
|
|
158
|
+
return;
|
|
112
159
|
}
|
|
113
160
|
|
|
114
161
|
if (node.type === "element") {
|
|
115
162
|
if (c.shouldSkip(node)) {
|
|
116
163
|
// A skipped element with content is a visual separator; an empty one
|
|
117
164
|
// renders nothing and leaves its neighbors genuinely adjacent.
|
|
118
|
-
if (node.children.length > 0) c.pendingOpaque = true
|
|
119
|
-
return
|
|
165
|
+
if (node.children.length > 0) c.pendingOpaque = true;
|
|
166
|
+
return;
|
|
120
167
|
}
|
|
121
168
|
if (OPAQUE_VOID_TAGS.has(node.tagName)) {
|
|
122
|
-
c.pendingOpaque = true
|
|
123
|
-
return
|
|
169
|
+
c.pendingOpaque = true;
|
|
170
|
+
return;
|
|
124
171
|
}
|
|
125
|
-
if (c.ancestors) c.ancestors.push(node)
|
|
126
|
-
for (const child of node.children) collectProse(child, depth + 1, c)
|
|
127
|
-
if (c.ancestors) c.ancestors.pop()
|
|
128
|
-
return
|
|
172
|
+
if (c.ancestors) c.ancestors.push(node);
|
|
173
|
+
for (const child of node.children) collectProse(child, depth + 1, c);
|
|
174
|
+
if (c.ancestors) c.ancestors.pop();
|
|
175
|
+
return;
|
|
129
176
|
}
|
|
130
177
|
|
|
131
178
|
if (node.type === "text") {
|
|
132
179
|
// Snapshot ancestors at the callback boundary — the walker mutates the
|
|
133
180
|
// shared array as it recurses, so handing it out directly would let a
|
|
134
181
|
// caller observe a stale view if they captured the reference.
|
|
135
|
-
if (
|
|
182
|
+
if (
|
|
183
|
+
c.shouldSkipText &&
|
|
184
|
+
c.ancestors &&
|
|
185
|
+
c.shouldSkipText(node, c.ancestors.slice())
|
|
186
|
+
) {
|
|
136
187
|
// Preserved-but-untransformed text is a separator between its neighbors.
|
|
137
|
-
c.pendingOpaque = true
|
|
138
|
-
return
|
|
188
|
+
c.pendingOpaque = true;
|
|
189
|
+
return;
|
|
139
190
|
}
|
|
140
|
-
if (c.pendingOpaque && c.nodes.length > 0)
|
|
141
|
-
|
|
142
|
-
c.
|
|
191
|
+
if (c.pendingOpaque && c.nodes.length > 0)
|
|
192
|
+
c.opaqueBefore.add(c.nodes.length);
|
|
193
|
+
c.pendingOpaque = false;
|
|
194
|
+
c.nodes.push(node);
|
|
143
195
|
}
|
|
144
196
|
}
|
|
145
197
|
|
|
146
|
-
function newCollector(
|
|
147
|
-
|
|
198
|
+
function newCollector(
|
|
199
|
+
shouldSkip: ElementPredicate,
|
|
200
|
+
options?: ElementTransformOptions,
|
|
201
|
+
): ProseCollector {
|
|
202
|
+
const shouldSkipText = options?.shouldSkipText;
|
|
148
203
|
return {
|
|
149
204
|
nodes: [],
|
|
150
205
|
opaqueBefore: new Set(),
|
|
@@ -154,87 +209,94 @@ function newCollector(shouldSkip: ElementPredicate, options?: ElementTransformOp
|
|
|
154
209
|
// O(n²) allocation cost of copying the ancestor chain at every level.
|
|
155
210
|
ancestors: shouldSkipText ? [] : null,
|
|
156
211
|
pendingOpaque: false,
|
|
157
|
-
}
|
|
212
|
+
};
|
|
158
213
|
}
|
|
159
214
|
|
|
160
215
|
/** Flattens an element's transformable text nodes, tracking opaque gaps. */
|
|
161
216
|
export function flattenProse(
|
|
162
217
|
node: Element | ElementContent,
|
|
163
218
|
shouldSkip: ElementPredicate,
|
|
164
|
-
options?: ElementTransformOptions
|
|
219
|
+
options?: ElementTransformOptions,
|
|
165
220
|
): FlattenedProse {
|
|
166
|
-
const c = newCollector(shouldSkip, options)
|
|
167
|
-
collectProse(node, 0, c)
|
|
168
|
-
return { nodes: c.nodes, opaqueBefore: c.opaqueBefore }
|
|
221
|
+
const c = newCollector(shouldSkip, options);
|
|
222
|
+
collectProse(node, 0, c);
|
|
223
|
+
return { nodes: c.nodes, opaqueBefore: c.opaqueBefore };
|
|
169
224
|
}
|
|
170
225
|
|
|
171
226
|
export function flattenTextNodes(
|
|
172
227
|
node: Element | ElementContent,
|
|
173
228
|
shouldSkip: ElementPredicate,
|
|
174
|
-
options?: ElementTransformOptions
|
|
229
|
+
options?: ElementTransformOptions,
|
|
175
230
|
): Text[] {
|
|
176
|
-
return flattenProse(node, shouldSkip, options).nodes
|
|
231
|
+
return flattenProse(node, shouldSkip, options).nodes;
|
|
177
232
|
}
|
|
178
233
|
|
|
179
234
|
export function getTextContent(
|
|
180
235
|
node: Element,
|
|
181
|
-
shouldSkip: ElementPredicate = () => false
|
|
236
|
+
shouldSkip: ElementPredicate = () => false,
|
|
182
237
|
): string {
|
|
183
238
|
return flattenTextNodes(node, shouldSkip)
|
|
184
239
|
.map((n) => n.value)
|
|
185
|
-
.join("")
|
|
240
|
+
.join("");
|
|
186
241
|
}
|
|
187
242
|
|
|
188
243
|
export function getFirstTextNode(
|
|
189
244
|
node: Parent | RootContent,
|
|
190
|
-
depth: number = 0
|
|
245
|
+
depth: number = 0,
|
|
191
246
|
): Text | null {
|
|
192
|
-
if (!node || depth > MAX_RECURSION_DEPTH) return null
|
|
247
|
+
if (!node || depth > MAX_RECURSION_DEPTH) return null;
|
|
193
248
|
|
|
194
249
|
if (node.type === "text") {
|
|
195
|
-
return node as Text
|
|
250
|
+
return node as Text;
|
|
196
251
|
}
|
|
197
252
|
|
|
198
253
|
if ("children" in node && node.children.length > 0) {
|
|
199
254
|
for (const child of node.children) {
|
|
200
|
-
const textNode = getFirstTextNode(child as Parent, depth + 1)
|
|
201
|
-
if (textNode) return textNode
|
|
255
|
+
const textNode = getFirstTextNode(child as Parent, depth + 1);
|
|
256
|
+
if (textNode) return textNode;
|
|
202
257
|
}
|
|
203
258
|
}
|
|
204
259
|
|
|
205
|
-
return null
|
|
260
|
+
return null;
|
|
206
261
|
}
|
|
207
262
|
|
|
208
|
-
const QUOTE_OPENERS = new Set(["\u201C"])
|
|
209
|
-
const CLOSER_TO_OPENER: Record<string, string> = { "\u201D": "\u201C" }
|
|
263
|
+
const QUOTE_OPENERS = new Set(["\u201C"]);
|
|
264
|
+
const CLOSER_TO_OPENER: Record<string, string> = { "\u201D": "\u201C" };
|
|
210
265
|
|
|
211
266
|
// Only checks double quotes \u2014 single quotes are excluded because
|
|
212
267
|
// U+2019 doubles as an apostrophe, making balance-checking unreliable.
|
|
213
268
|
export function assertSmartQuotesMatch(input: string): void {
|
|
214
|
-
if (!input) return
|
|
269
|
+
if (!input) return;
|
|
215
270
|
|
|
216
|
-
const stack: string[] = []
|
|
271
|
+
const stack: string[] = [];
|
|
217
272
|
|
|
218
273
|
for (const char of input) {
|
|
219
274
|
if (QUOTE_OPENERS.has(char)) {
|
|
220
|
-
stack.push(char)
|
|
275
|
+
stack.push(char);
|
|
221
276
|
} else if (char in CLOSER_TO_OPENER) {
|
|
222
|
-
if (
|
|
223
|
-
stack.
|
|
277
|
+
if (
|
|
278
|
+
stack.length > 0 &&
|
|
279
|
+
stack[stack.length - 1] === CLOSER_TO_OPENER[char]
|
|
280
|
+
) {
|
|
281
|
+
stack.pop();
|
|
224
282
|
} else {
|
|
225
|
-
throw new Error(
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Mismatched quotes in ${formatErrorString(input, "input")}`,
|
|
285
|
+
);
|
|
226
286
|
}
|
|
227
287
|
}
|
|
228
288
|
}
|
|
229
289
|
|
|
230
290
|
if (stack.length > 0) {
|
|
231
|
-
throw new Error(
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Mismatched quotes in ${formatErrorString(input, "input")}`,
|
|
293
|
+
);
|
|
232
294
|
}
|
|
233
295
|
}
|
|
234
296
|
|
|
235
297
|
export interface ProseViewOfOptions extends ElementTransformOptions {
|
|
236
298
|
/** Element-level skip predicate; skipped subtrees contribute no text nodes. */
|
|
237
|
-
shouldSkip?: ElementPredicate
|
|
299
|
+
shouldSkip?: ElementPredicate;
|
|
238
300
|
}
|
|
239
301
|
|
|
240
302
|
/**
|
|
@@ -244,19 +306,22 @@ export interface ProseViewOfOptions extends ElementTransformOptions {
|
|
|
244
306
|
* view spans opaque gaps; the transform pipeline uses {@link proseViewsOf},
|
|
245
307
|
* which splits on them so no pass rewrites across removed atomic content.
|
|
246
308
|
*/
|
|
247
|
-
export function proseViewOf(
|
|
309
|
+
export function proseViewOf(
|
|
310
|
+
element: Element,
|
|
311
|
+
options: ProseViewOfOptions = {},
|
|
312
|
+
): ProseView | null {
|
|
248
313
|
/* istanbul ignore if -- defensive: elements should always have children array */
|
|
249
314
|
if (!element?.children) {
|
|
250
|
-
return null
|
|
315
|
+
return null;
|
|
251
316
|
}
|
|
252
317
|
|
|
253
|
-
const shouldSkip = options.shouldSkip ?? (() => false)
|
|
254
|
-
const textNodes = flattenTextNodes(element, shouldSkip, options)
|
|
318
|
+
const shouldSkip = options.shouldSkip ?? (() => false);
|
|
319
|
+
const textNodes = flattenTextNodes(element, shouldSkip, options);
|
|
255
320
|
if (textNodes.length === 0) {
|
|
256
|
-
return null
|
|
321
|
+
return null;
|
|
257
322
|
}
|
|
258
323
|
|
|
259
|
-
return buildProseView(textNodes)
|
|
324
|
+
return buildProseView(textNodes);
|
|
260
325
|
}
|
|
261
326
|
|
|
262
327
|
/**
|
|
@@ -265,14 +330,19 @@ export function proseViewOf(element: Element, options: ProseViewOfOptions = {}):
|
|
|
265
330
|
* inline content) keeps their surrounding text in separate views, so a pass can
|
|
266
331
|
* never treat text as adjacent across content that visually separates it.
|
|
267
332
|
*/
|
|
268
|
-
export function proseViewsOf(
|
|
333
|
+
export function proseViewsOf(
|
|
334
|
+
element: Element,
|
|
335
|
+
options: ProseViewOfOptions = {},
|
|
336
|
+
): ProseView[] {
|
|
269
337
|
/* istanbul ignore if -- defensive: elements should always have children array */
|
|
270
338
|
if (!element?.children) {
|
|
271
|
-
return []
|
|
339
|
+
return [];
|
|
272
340
|
}
|
|
273
|
-
const shouldSkip = options.shouldSkip ?? (() => false)
|
|
274
|
-
const { nodes, opaqueBefore } = flattenProse(element, shouldSkip, options)
|
|
275
|
-
return splitAtIndices(nodes, opaqueBefore).map((group) =>
|
|
341
|
+
const shouldSkip = options.shouldSkip ?? (() => false);
|
|
342
|
+
const { nodes, opaqueBefore } = flattenProse(element, shouldSkip, options);
|
|
343
|
+
return splitAtIndices(nodes, opaqueBefore).map((group) =>
|
|
344
|
+
buildProseView(group),
|
|
345
|
+
);
|
|
276
346
|
}
|
|
277
347
|
|
|
278
348
|
/**
|
|
@@ -282,16 +352,20 @@ export function proseViewsOf(element: Element, options: ProseViewOfOptions = {})
|
|
|
282
352
|
*/
|
|
283
353
|
export type PassEntry =
|
|
284
354
|
| ProsePass
|
|
285
|
-
| {
|
|
355
|
+
| {
|
|
356
|
+
pass: ProsePass;
|
|
357
|
+
shouldSkip?: ElementPredicate;
|
|
358
|
+
shouldSkipText?: TextNodeSkipPredicate;
|
|
359
|
+
};
|
|
286
360
|
|
|
287
361
|
interface ResolvedPassEntry {
|
|
288
|
-
pass: ProsePass
|
|
289
|
-
shouldSkip?: ElementPredicate
|
|
290
|
-
shouldSkipText?: TextNodeSkipPredicate
|
|
362
|
+
pass: ProsePass;
|
|
363
|
+
shouldSkip?: ElementPredicate;
|
|
364
|
+
shouldSkipText?: TextNodeSkipPredicate;
|
|
291
365
|
}
|
|
292
366
|
|
|
293
367
|
function resolvePassEntry(entry: PassEntry): ResolvedPassEntry {
|
|
294
|
-
return typeof entry === "function" ? { pass: entry } : entry
|
|
368
|
+
return typeof entry === "function" ? { pass: entry } : entry;
|
|
295
369
|
}
|
|
296
370
|
|
|
297
371
|
/** OR-combines an optional entry predicate onto an optional base predicate. */
|
|
@@ -299,9 +373,9 @@ function mergePredicates<Args extends unknown[]>(
|
|
|
299
373
|
base: ((...args: Args) => boolean) | undefined,
|
|
300
374
|
extra: ((...args: Args) => boolean) | undefined,
|
|
301
375
|
): ((...args: Args) => boolean) | undefined {
|
|
302
|
-
if (!base) return extra
|
|
303
|
-
if (!extra) return base
|
|
304
|
-
return (...args) => base(...args) || extra(...args)
|
|
376
|
+
if (!base) return extra;
|
|
377
|
+
if (!extra) return base;
|
|
378
|
+
return (...args) => base(...args) || extra(...args);
|
|
305
379
|
}
|
|
306
380
|
|
|
307
381
|
/**
|
|
@@ -315,55 +389,100 @@ function mergePredicates<Args extends unknown[]>(
|
|
|
315
389
|
* fresh view (built after the previous pass committed) over the text nodes
|
|
316
390
|
* that survive both the base `options` predicates and its own.
|
|
317
391
|
*
|
|
318
|
-
* The view is a single
|
|
319
|
-
* boundary recorded at every opaque gap (removed
|
|
320
|
-
* Caller passes stay boundary-aware via
|
|
321
|
-
* decides for itself whether to act across a
|
|
322
|
-
* skipped `<code>` runs, gluing a dash after a
|
|
323
|
-
* being blocked wholesale. Passes that must not
|
|
324
|
-
* boundary marks; use {@link proseViewsOf} directly
|
|
392
|
+
* The view is a single spanning view (over the whole element, or a run's loose
|
|
393
|
+
* inline children), with a boundary recorded at every opaque gap (removed
|
|
394
|
+
* skipped element, image, …). Caller passes stay boundary-aware via
|
|
395
|
+
* `view.hasBoundary`, so each pass decides for itself whether to act across a
|
|
396
|
+
* gap — spacing a slash between two skipped `<code>` runs, gluing a dash after a
|
|
397
|
+
* skipped element — rather than being blocked wholesale. Passes that must not
|
|
398
|
+
* cross a gap consult the same boundary marks; use {@link proseViewsOf} directly
|
|
399
|
+
* for hard per-segment views.
|
|
400
|
+
*
|
|
401
|
+
* Accepts a bare element (formatted whole) or a {@link ProseUnit} from
|
|
402
|
+
* {@link collectProseUnits}; a "run" unit formats a container's loose inline
|
|
403
|
+
* text without pulling in its block children.
|
|
325
404
|
*/
|
|
326
405
|
export function applyPasses(
|
|
327
|
-
|
|
406
|
+
target: Element | ProseUnit,
|
|
328
407
|
passes: readonly PassEntry[],
|
|
329
408
|
options: ProseViewOfOptions = {},
|
|
330
409
|
): void {
|
|
331
|
-
|
|
332
|
-
|
|
410
|
+
const makeView = (viewOptions: ProseViewOfOptions): ProseView | null => {
|
|
411
|
+
if ("kind" in target) {
|
|
412
|
+
return target.kind === "element"
|
|
413
|
+
? proseViewOf(target.element, viewOptions)
|
|
414
|
+
: runViewOf(target.container, target.children, viewOptions);
|
|
415
|
+
}
|
|
416
|
+
return proseViewOf(target, viewOptions);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
let currentView: ProseView | null = null;
|
|
420
|
+
let currentPredicates: Pick<
|
|
421
|
+
ResolvedPassEntry,
|
|
422
|
+
"shouldSkip" | "shouldSkipText"
|
|
423
|
+
> | null = null;
|
|
333
424
|
|
|
334
425
|
for (const entry of passes) {
|
|
335
|
-
const { pass, shouldSkip, shouldSkipText } = resolvePassEntry(entry)
|
|
426
|
+
const { pass, shouldSkip, shouldSkipText } = resolvePassEntry(entry);
|
|
336
427
|
|
|
337
428
|
const samePredicates =
|
|
338
429
|
currentPredicates !== null &&
|
|
339
430
|
currentPredicates.shouldSkip === shouldSkip &&
|
|
340
|
-
currentPredicates.shouldSkipText === shouldSkipText
|
|
431
|
+
currentPredicates.shouldSkipText === shouldSkipText;
|
|
341
432
|
if (!samePredicates) {
|
|
342
|
-
currentView =
|
|
433
|
+
currentView = makeView({
|
|
343
434
|
shouldSkip: mergePredicates(options.shouldSkip, shouldSkip),
|
|
344
435
|
shouldSkipText: mergePredicates(options.shouldSkipText, shouldSkipText),
|
|
345
|
-
})
|
|
346
|
-
currentPredicates = { shouldSkip, shouldSkipText }
|
|
436
|
+
});
|
|
437
|
+
currentPredicates = { shouldSkip, shouldSkipText };
|
|
347
438
|
}
|
|
348
439
|
|
|
349
440
|
// No transformable text under this entry's predicates; a later entry may
|
|
350
441
|
// still see text (its skip set differs), so keep going.
|
|
351
442
|
if (currentView === null) {
|
|
352
|
-
continue
|
|
443
|
+
continue;
|
|
353
444
|
}
|
|
354
|
-
pass(currentView)
|
|
445
|
+
pass(currentView);
|
|
355
446
|
}
|
|
356
447
|
}
|
|
357
448
|
|
|
358
449
|
// Block children cause per-block processing to avoid merging text across
|
|
359
450
|
// semantically independent blocks (e.g., separate <p>s inside a <div>).
|
|
360
451
|
const BLOCK_ELEMENTS = new Set([
|
|
361
|
-
"address",
|
|
362
|
-
"
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
|
|
452
|
+
"address",
|
|
453
|
+
"article",
|
|
454
|
+
"aside",
|
|
455
|
+
"blockquote",
|
|
456
|
+
"details",
|
|
457
|
+
"dialog",
|
|
458
|
+
"dd",
|
|
459
|
+
"div",
|
|
460
|
+
"dl",
|
|
461
|
+
"dt",
|
|
462
|
+
"fieldset",
|
|
463
|
+
"figcaption",
|
|
464
|
+
"figure",
|
|
465
|
+
"footer",
|
|
466
|
+
"form",
|
|
467
|
+
"h1",
|
|
468
|
+
"h2",
|
|
469
|
+
"h3",
|
|
470
|
+
"h4",
|
|
471
|
+
"h5",
|
|
472
|
+
"h6",
|
|
473
|
+
"header",
|
|
474
|
+
"hgroup",
|
|
475
|
+
"hr",
|
|
476
|
+
"li",
|
|
477
|
+
"main",
|
|
478
|
+
"nav",
|
|
479
|
+
"ol",
|
|
480
|
+
"p",
|
|
481
|
+
"pre",
|
|
482
|
+
"section",
|
|
483
|
+
"table",
|
|
484
|
+
"ul",
|
|
485
|
+
]);
|
|
367
486
|
|
|
368
487
|
const TRANSFORMABLE_ELEMENTS = new Set([
|
|
369
488
|
"p",
|
|
@@ -424,36 +543,38 @@ const TRANSFORMABLE_ELEMENTS = new Set([
|
|
|
424
543
|
"button",
|
|
425
544
|
"option",
|
|
426
545
|
"output",
|
|
427
|
-
])
|
|
546
|
+
]);
|
|
428
547
|
|
|
429
548
|
// A tag name containing "-" is a custom element per the HTML custom-element
|
|
430
549
|
// naming rule; assume those hold transformable prose like built-in elements.
|
|
431
550
|
function isTransformableElement(tagName: string): boolean {
|
|
432
|
-
return TRANSFORMABLE_ELEMENTS.has(tagName) || tagName.includes("-")
|
|
551
|
+
return TRANSFORMABLE_ELEMENTS.has(tagName) || tagName.includes("-");
|
|
433
552
|
}
|
|
434
553
|
|
|
435
554
|
function hasTextDescendant(
|
|
436
555
|
node: Element | ElementContent,
|
|
437
556
|
shouldSkip: ElementPredicate,
|
|
438
|
-
depth: number = 0
|
|
557
|
+
depth: number = 0,
|
|
439
558
|
): boolean {
|
|
440
559
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
441
|
-
return false
|
|
560
|
+
return false;
|
|
442
561
|
}
|
|
443
562
|
|
|
444
563
|
if (node.type === "element" && shouldSkip(node)) {
|
|
445
|
-
return false
|
|
564
|
+
return false;
|
|
446
565
|
}
|
|
447
566
|
|
|
448
567
|
if (node.type === "text") {
|
|
449
|
-
return true
|
|
568
|
+
return true;
|
|
450
569
|
}
|
|
451
570
|
|
|
452
571
|
if (node.type === "element") {
|
|
453
|
-
return node.children.some((child) =>
|
|
572
|
+
return node.children.some((child) =>
|
|
573
|
+
hasTextDescendant(child, shouldSkip, depth + 1),
|
|
574
|
+
);
|
|
454
575
|
}
|
|
455
576
|
|
|
456
|
-
return false
|
|
577
|
+
return false;
|
|
457
578
|
}
|
|
458
579
|
|
|
459
580
|
export interface CollectProseBlocksOptions {
|
|
@@ -461,77 +582,117 @@ export interface CollectProseBlocksOptions {
|
|
|
461
582
|
* HTML tag names to skip. Default: the plugin's default skip list
|
|
462
583
|
* ("code", "pre", "script", ...).
|
|
463
584
|
*/
|
|
464
|
-
skipTags?: string[]
|
|
585
|
+
skipTags?: string[];
|
|
465
586
|
/** CSS class names whose elements (and their subtrees) are skipped. Default: [] */
|
|
466
|
-
skipClasses?: string[]
|
|
587
|
+
skipClasses?: string[];
|
|
467
588
|
/** Additional element-level skip predicate, OR-ed with the tag/class skips. */
|
|
468
|
-
shouldSkip?: ElementPredicate
|
|
589
|
+
shouldSkip?: ElementPredicate;
|
|
469
590
|
/** Invert the element model as in {@link RehypePunctilioOptions.transformAllElements}. Default: false */
|
|
470
|
-
transformAllElements?: boolean
|
|
591
|
+
transformAllElements?: boolean;
|
|
471
592
|
}
|
|
472
593
|
|
|
473
594
|
/** Skip predicate and transformable-tag test resolved from the options. */
|
|
474
595
|
interface ResolvedCollectOptions {
|
|
475
|
-
shouldSkip: ElementPredicate
|
|
476
|
-
isTransformable: (tagName: string) => boolean
|
|
596
|
+
shouldSkip: ElementPredicate;
|
|
597
|
+
isTransformable: (tagName: string) => boolean;
|
|
477
598
|
}
|
|
478
599
|
|
|
479
|
-
function resolveCollectOptions(
|
|
480
|
-
|
|
600
|
+
function resolveCollectOptions(
|
|
601
|
+
options: CollectProseBlocksOptions,
|
|
602
|
+
): ResolvedCollectOptions {
|
|
603
|
+
const {
|
|
604
|
+
skipTags = DEFAULT_SKIP_TAGS,
|
|
605
|
+
skipClasses = [],
|
|
606
|
+
shouldSkip,
|
|
607
|
+
transformAllElements = false,
|
|
608
|
+
} = options;
|
|
481
609
|
|
|
482
610
|
// In inverted mode, form-value elements join the skip-list so their literal
|
|
483
611
|
// values are neither transformed nor flattened into a transformable ancestor.
|
|
484
|
-
const skipTagSet = new Set(
|
|
485
|
-
|
|
612
|
+
const skipTagSet = new Set(
|
|
613
|
+
transformAllElements ? [...skipTags, ...FORM_VALUE_TAGS] : skipTags,
|
|
614
|
+
);
|
|
615
|
+
const skipClassSet = new Set(skipClasses);
|
|
486
616
|
|
|
487
617
|
const hasSkipClass = (node: Element): boolean => {
|
|
488
|
-
if (skipClassSet.size === 0) return false
|
|
489
|
-
const classNames = node.properties?.className
|
|
618
|
+
if (skipClassSet.size === 0) return false;
|
|
619
|
+
const classNames = node.properties?.className;
|
|
490
620
|
const classes = Array.isArray(classNames)
|
|
491
621
|
? classNames.filter((c): c is string => typeof c === "string")
|
|
492
|
-
: typeof classNames === "string"
|
|
493
|
-
|
|
494
|
-
|
|
622
|
+
: typeof classNames === "string"
|
|
623
|
+
? classNames.split(/\s+/)
|
|
624
|
+
: [];
|
|
625
|
+
return classes.some((cls) => skipClassSet.has(cls));
|
|
626
|
+
};
|
|
495
627
|
|
|
496
628
|
return {
|
|
497
629
|
shouldSkip: (node) =>
|
|
498
|
-
skipTagSet.has(node.tagName) ||
|
|
630
|
+
skipTagSet.has(node.tagName) ||
|
|
631
|
+
hasSkipClass(node) ||
|
|
632
|
+
(shouldSkip?.(node) ?? false),
|
|
499
633
|
// Inverted mode transforms every non-skipped element except non-prose
|
|
500
634
|
// containers; the default keeps the `TRANSFORMABLE_ELEMENTS` allowlist
|
|
501
635
|
// (plus custom elements).
|
|
502
636
|
isTransformable: transformAllElements
|
|
503
637
|
? (tagName) => !NON_PROSE_CONTAINER_TAGS.has(tagName)
|
|
504
638
|
: isTransformableElement,
|
|
505
|
-
}
|
|
639
|
+
};
|
|
506
640
|
}
|
|
507
641
|
|
|
508
642
|
// A prose unit is either a whole transformable element (an inline-only leaf) or
|
|
509
643
|
// a "run" of consecutive inline siblings that sit alongside block-level
|
|
510
644
|
// children. Runs let a container's loose inline text transform independently of
|
|
511
645
|
// its block children instead of merging across the block boundary.
|
|
512
|
-
type ProseUnit =
|
|
646
|
+
export type ProseUnit =
|
|
513
647
|
| { kind: "element"; element: Element }
|
|
514
|
-
| { kind: "run"; container: Element; children: ElementContent[] }
|
|
648
|
+
| { kind: "run"; container: Element; children: ElementContent[] };
|
|
515
649
|
|
|
516
650
|
/**
|
|
517
651
|
* Collects the leaf elements under `root` (inclusive) whose text should be
|
|
518
652
|
* transformed as one prose block: transformable elements with direct text or
|
|
519
653
|
* inline-only text descendants. Elements with block-level children recurse so
|
|
520
654
|
* each block transforms independently. Loose inline text mixed among block
|
|
521
|
-
* children is
|
|
655
|
+
* children is NOT represented here — use {@link collectProseUnits} to reach it.
|
|
656
|
+
*/
|
|
657
|
+
export function collectProseBlocks(
|
|
658
|
+
root: Element,
|
|
659
|
+
options: CollectProseBlocksOptions = {},
|
|
660
|
+
): Element[] {
|
|
661
|
+
const { shouldSkip, isTransformable } = resolveCollectOptions(options);
|
|
662
|
+
return collectProseUnitsImpl(
|
|
663
|
+
root,
|
|
664
|
+
shouldSkip,
|
|
665
|
+
0,
|
|
666
|
+
undefined,
|
|
667
|
+
isTransformable,
|
|
668
|
+
).flatMap((unit) => (unit.kind === "element" ? [unit.element] : []));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Like {@link collectProseBlocks}, but also returns "run" units — maximal groups
|
|
673
|
+
* of loose inline siblings sitting beside block children (e.g. the bare text in
|
|
674
|
+
* `<blockquote><p>…</p>loose text</blockquote>`). A consumer that walks elements
|
|
675
|
+
* cannot otherwise reach that text (it belongs to no element of its own); pass
|
|
676
|
+
* each unit to {@link applyPasses}, which formats a run without merging its
|
|
677
|
+
* container's block children.
|
|
522
678
|
*/
|
|
523
|
-
export function
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
679
|
+
export function collectProseUnits(
|
|
680
|
+
root: Element,
|
|
681
|
+
options: CollectProseBlocksOptions = {},
|
|
682
|
+
): ProseUnit[] {
|
|
683
|
+
const { shouldSkip, isTransformable } = resolveCollectOptions(options);
|
|
684
|
+
return collectProseUnitsImpl(root, shouldSkip, 0, undefined, isTransformable);
|
|
527
685
|
}
|
|
528
686
|
|
|
529
|
-
function runHasProse(
|
|
687
|
+
function runHasProse(
|
|
688
|
+
children: readonly ElementContent[],
|
|
689
|
+
shouldSkip: ElementPredicate,
|
|
690
|
+
): boolean {
|
|
530
691
|
return children.some(
|
|
531
692
|
(child) =>
|
|
532
693
|
(child.type === "text" && child.value.trim() !== "") ||
|
|
533
|
-
(child.type === "element" && hasTextDescendant(child, shouldSkip))
|
|
534
|
-
)
|
|
694
|
+
(child.type === "element" && hasTextDescendant(child, shouldSkip)),
|
|
695
|
+
);
|
|
535
696
|
}
|
|
536
697
|
|
|
537
698
|
function collectProseUnitsImpl(
|
|
@@ -539,15 +700,15 @@ function collectProseUnitsImpl(
|
|
|
539
700
|
shouldSkip: ElementPredicate,
|
|
540
701
|
depth: number,
|
|
541
702
|
alreadyTransformed: ReadonlySet<Element> | undefined,
|
|
542
|
-
isTransformable: (tagName: string) => boolean
|
|
703
|
+
isTransformable: (tagName: string) => boolean,
|
|
543
704
|
): ProseUnit[] {
|
|
544
705
|
/* istanbul ignore if -- defensive: prevents stack overflow from malicious HTML */
|
|
545
706
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
546
|
-
return []
|
|
707
|
+
return [];
|
|
547
708
|
}
|
|
548
709
|
|
|
549
710
|
if (shouldSkip(node) || alreadyTransformed?.has(node)) {
|
|
550
|
-
return []
|
|
711
|
+
return [];
|
|
551
712
|
}
|
|
552
713
|
|
|
553
714
|
// Whitespace-only text nodes between block siblings (the newlines a parser
|
|
@@ -556,11 +717,11 @@ function collectProseUnitsImpl(
|
|
|
556
717
|
// and merge its blocks into one transform unit, pairing quotes across the
|
|
557
718
|
// paragraph boundary. Real (non-whitespace) direct text still marks a leaf.
|
|
558
719
|
const hasDirectText = node.children.some(
|
|
559
|
-
(child) => child.type === "text" && child.value.trim() !== ""
|
|
560
|
-
)
|
|
720
|
+
(child) => child.type === "text" && child.value.trim() !== "",
|
|
721
|
+
);
|
|
561
722
|
const hasBlockChildren = node.children.some(
|
|
562
|
-
(child) => child.type === "element" && BLOCK_ELEMENTS.has(child.tagName)
|
|
563
|
-
)
|
|
723
|
+
(child) => child.type === "element" && BLOCK_ELEMENTS.has(child.tagName),
|
|
724
|
+
);
|
|
564
725
|
|
|
565
726
|
// Inline-only transformable node: a single leaf unit. `hasTextDescendant` is
|
|
566
727
|
// only walked when there's no direct text (short-circuit avoids the traversal).
|
|
@@ -569,41 +730,54 @@ function collectProseUnitsImpl(
|
|
|
569
730
|
!hasBlockChildren &&
|
|
570
731
|
(hasDirectText || hasTextDescendant(node, shouldSkip))
|
|
571
732
|
) {
|
|
572
|
-
return [{ kind: "element", element: node }]
|
|
733
|
+
return [{ kind: "element", element: node }];
|
|
573
734
|
}
|
|
574
735
|
|
|
575
|
-
const units: ProseUnit[] = []
|
|
736
|
+
const units: ProseUnit[] = [];
|
|
576
737
|
if (isTransformable(node.tagName) && hasBlockChildren) {
|
|
577
738
|
// Mixed content: split each maximal run of inline siblings from the block
|
|
578
739
|
// children so loose text transforms on its own, then recurse per block.
|
|
579
|
-
let run: ElementContent[] = []
|
|
740
|
+
let run: ElementContent[] = [];
|
|
580
741
|
const flushRun = () => {
|
|
581
|
-
if (runHasProse(run, shouldSkip))
|
|
582
|
-
|
|
583
|
-
|
|
742
|
+
if (runHasProse(run, shouldSkip))
|
|
743
|
+
units.push({ kind: "run", container: node, children: run });
|
|
744
|
+
run = [];
|
|
745
|
+
};
|
|
584
746
|
for (const child of node.children) {
|
|
585
747
|
if (child.type === "element" && BLOCK_ELEMENTS.has(child.tagName)) {
|
|
586
|
-
flushRun()
|
|
587
|
-
for (const u of collectProseUnitsImpl(
|
|
588
|
-
|
|
748
|
+
flushRun();
|
|
749
|
+
for (const u of collectProseUnitsImpl(
|
|
750
|
+
child,
|
|
751
|
+
shouldSkip,
|
|
752
|
+
depth + 1,
|
|
753
|
+
alreadyTransformed,
|
|
754
|
+
isTransformable,
|
|
755
|
+
)) {
|
|
756
|
+
units.push(u);
|
|
589
757
|
}
|
|
590
758
|
} else {
|
|
591
|
-
run.push(child)
|
|
759
|
+
run.push(child);
|
|
592
760
|
}
|
|
593
761
|
}
|
|
594
|
-
flushRun()
|
|
762
|
+
flushRun();
|
|
595
763
|
} else {
|
|
596
764
|
// Non-transformable, or transformable but textless: recurse into elements.
|
|
597
765
|
for (const child of node.children) {
|
|
598
766
|
if (child.type === "element") {
|
|
599
|
-
for (const u of collectProseUnitsImpl(
|
|
600
|
-
|
|
767
|
+
for (const u of collectProseUnitsImpl(
|
|
768
|
+
child,
|
|
769
|
+
shouldSkip,
|
|
770
|
+
depth + 1,
|
|
771
|
+
alreadyTransformed,
|
|
772
|
+
isTransformable,
|
|
773
|
+
)) {
|
|
774
|
+
units.push(u);
|
|
601
775
|
}
|
|
602
776
|
}
|
|
603
777
|
}
|
|
604
778
|
}
|
|
605
779
|
|
|
606
|
-
return units
|
|
780
|
+
return units;
|
|
607
781
|
}
|
|
608
782
|
|
|
609
783
|
// Flatten a run's inline siblings to their text nodes (with opaque gaps),
|
|
@@ -613,32 +787,51 @@ function flattenRunProse(
|
|
|
613
787
|
container: Element,
|
|
614
788
|
children: readonly ElementContent[],
|
|
615
789
|
shouldSkip: ElementPredicate,
|
|
616
|
-
options: ProseViewOfOptions
|
|
790
|
+
options: ProseViewOfOptions,
|
|
617
791
|
): FlattenedProse {
|
|
618
|
-
const c = newCollector(shouldSkip, options)
|
|
619
|
-
if (c.ancestors) c.ancestors.push(container)
|
|
620
|
-
for (const child of children) collectProse(child, 1, c)
|
|
621
|
-
return { nodes: c.nodes, opaqueBefore: c.opaqueBefore }
|
|
792
|
+
const c = newCollector(shouldSkip, options);
|
|
793
|
+
if (c.ancestors) c.ancestors.push(container);
|
|
794
|
+
for (const child of children) collectProse(child, 1, c);
|
|
795
|
+
return { nodes: c.nodes, opaqueBefore: c.opaqueBefore };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* A single spanning ProseView over a run's loose inline children, with a
|
|
800
|
+
* boundary at every opaque gap — the run analogue of {@link proseViewOf}. Null
|
|
801
|
+
* when the run holds no transformable text.
|
|
802
|
+
*/
|
|
803
|
+
function runViewOf(
|
|
804
|
+
container: Element,
|
|
805
|
+
children: readonly ElementContent[],
|
|
806
|
+
options: ProseViewOfOptions,
|
|
807
|
+
): ProseView | null {
|
|
808
|
+
const shouldSkip = options.shouldSkip ?? (() => false);
|
|
809
|
+
const { nodes } = flattenRunProse(container, children, shouldSkip, options);
|
|
810
|
+
return nodes.length === 0 ? null : buildProseView(nodes);
|
|
622
811
|
}
|
|
623
812
|
|
|
624
|
-
function markDescendants(
|
|
813
|
+
function markDescendants(
|
|
814
|
+
node: Element,
|
|
815
|
+
set: Set<Element>,
|
|
816
|
+
depth: number = 0,
|
|
817
|
+
): void {
|
|
625
818
|
/* istanbul ignore if -- defensive: prevents stack overflow from malicious HTML */
|
|
626
819
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
627
|
-
return
|
|
820
|
+
return;
|
|
628
821
|
}
|
|
629
822
|
|
|
630
823
|
for (const child of node.children) {
|
|
631
824
|
if (child.type === "element") {
|
|
632
|
-
set.add(child)
|
|
633
|
-
markDescendants(child, set, depth + 1)
|
|
825
|
+
set.add(child);
|
|
826
|
+
markDescendants(child, set, depth + 1);
|
|
634
827
|
}
|
|
635
828
|
}
|
|
636
829
|
}
|
|
637
830
|
|
|
638
831
|
export function rehypePunctilio(
|
|
639
|
-
options: RehypePunctilioOptions = {}
|
|
832
|
+
options: RehypePunctilioOptions = {},
|
|
640
833
|
): Transformer<Root, Root> {
|
|
641
|
-
assertKnownOptionKeys(options, REHYPE_OPTION_KEYS, "rehypePunctilio")
|
|
834
|
+
assertKnownOptionKeys(options, REHYPE_OPTION_KEYS, "rehypePunctilio");
|
|
642
835
|
|
|
643
836
|
const {
|
|
644
837
|
skipTags,
|
|
@@ -646,61 +839,76 @@ export function rehypePunctilio(
|
|
|
646
839
|
shouldSkipText,
|
|
647
840
|
transformAllElements,
|
|
648
841
|
...transformOptions
|
|
649
|
-
} = options
|
|
842
|
+
} = options;
|
|
650
843
|
|
|
651
|
-
const { shouldSkip, isTransformable } = resolveCollectOptions({
|
|
844
|
+
const { shouldSkip, isTransformable } = resolveCollectOptions({
|
|
845
|
+
skipTags,
|
|
846
|
+
skipClasses,
|
|
847
|
+
transformAllElements,
|
|
848
|
+
});
|
|
652
849
|
|
|
653
850
|
// Allocate the per-element options object once; it's read-only inside
|
|
654
851
|
// proseViewOf, so sharing across calls is safe and avoids a fresh
|
|
655
852
|
// allocation per transformable element.
|
|
656
853
|
const viewOptions: ProseViewOfOptions = shouldSkipText
|
|
657
854
|
? { shouldSkip, shouldSkipText }
|
|
658
|
-
: { shouldSkip }
|
|
855
|
+
: { shouldSkip };
|
|
659
856
|
|
|
660
857
|
return (tree: Root) => {
|
|
661
858
|
// Track transformed elements to avoid double-processing
|
|
662
|
-
const transformed = new Set<Element>()
|
|
859
|
+
const transformed = new Set<Element>();
|
|
663
860
|
|
|
664
861
|
visitParents(tree, "element", (node) => {
|
|
665
862
|
// Skip subtree if already transformed
|
|
666
863
|
if (transformed.has(node)) {
|
|
667
|
-
return SKIP
|
|
864
|
+
return SKIP;
|
|
668
865
|
}
|
|
669
866
|
|
|
670
867
|
if (shouldSkip(node)) {
|
|
671
|
-
return SKIP
|
|
868
|
+
return SKIP;
|
|
672
869
|
}
|
|
673
870
|
|
|
674
871
|
// Collect and transform every prose unit in this subtree.
|
|
675
|
-
const units = collectProseUnitsImpl(
|
|
872
|
+
const units = collectProseUnitsImpl(
|
|
873
|
+
node,
|
|
874
|
+
shouldSkip,
|
|
875
|
+
0,
|
|
876
|
+
transformed,
|
|
877
|
+
isTransformable,
|
|
878
|
+
);
|
|
676
879
|
for (const unit of units) {
|
|
677
880
|
if (unit.kind === "element") {
|
|
678
881
|
if (!transformed.has(unit.element)) {
|
|
679
882
|
for (const view of proseViewsOf(unit.element, viewOptions)) {
|
|
680
|
-
transformView(view, transformOptions)
|
|
883
|
+
transformView(view, transformOptions);
|
|
681
884
|
}
|
|
682
|
-
transformed.add(unit.element)
|
|
885
|
+
transformed.add(unit.element);
|
|
683
886
|
// Mark all descendants as processed since their text was included
|
|
684
|
-
markDescendants(unit.element, transformed)
|
|
887
|
+
markDescendants(unit.element, transformed);
|
|
685
888
|
}
|
|
686
889
|
} else {
|
|
687
|
-
const { nodes, opaqueBefore } = flattenRunProse(
|
|
890
|
+
const { nodes, opaqueBefore } = flattenRunProse(
|
|
891
|
+
unit.container,
|
|
892
|
+
unit.children,
|
|
893
|
+
shouldSkip,
|
|
894
|
+
viewOptions,
|
|
895
|
+
);
|
|
688
896
|
for (const group of splitAtIndices(nodes, opaqueBefore)) {
|
|
689
|
-
transformView(buildProseView(group), transformOptions)
|
|
897
|
+
transformView(buildProseView(group), transformOptions);
|
|
690
898
|
}
|
|
691
899
|
// Mark the container so a later visit doesn't re-collect its runs,
|
|
692
900
|
// and mark the run's inline-element members' subtrees.
|
|
693
|
-
transformed.add(unit.container)
|
|
901
|
+
transformed.add(unit.container);
|
|
694
902
|
for (const child of unit.children) {
|
|
695
903
|
if (child.type === "element") {
|
|
696
|
-
transformed.add(child)
|
|
697
|
-
markDescendants(child, transformed)
|
|
904
|
+
transformed.add(child);
|
|
905
|
+
markDescendants(child, transformed);
|
|
698
906
|
}
|
|
699
907
|
}
|
|
700
908
|
}
|
|
701
909
|
}
|
|
702
|
-
})
|
|
703
|
-
}
|
|
910
|
+
});
|
|
911
|
+
};
|
|
704
912
|
}
|
|
705
913
|
|
|
706
|
-
export default rehypePunctilio
|
|
914
|
+
export default rehypePunctilio;
|