punctilio 3.4.1 → 3.5.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 +14 -0
- package/dist/rehype.d.ts +32 -4
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +37 -8
- package/dist/rehype.js.map +1 -1
- package/package.json +1 -1
- package/src/rehype.ts +76 -8
- package/src/tests/rehype.test.ts +113 -0
package/README.md
CHANGED
|
@@ -128,6 +128,20 @@ rehypePunctilio({
|
|
|
128
128
|
});
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
For finer-grained control, `shouldSkipText` opts specific text nodes out of transformation without skipping their enclosing element. The predicate receives the text node and its ancestor chain (root first, nearest last); returning `true` leaves the node’s value untouched. `shouldSkipText` runs after element-level skipping—it is never called for text inside an already-skipped element.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
rehypePunctilio({
|
|
135
|
+
// Skip anchor text that equals its href (URL-like link text).
|
|
136
|
+
shouldSkipText: (textNode, ancestors) => {
|
|
137
|
+
const parent = ancestors[ancestors.length - 1];
|
|
138
|
+
if (parent?.tagName !== "a") return false;
|
|
139
|
+
const href = parent.properties?.href;
|
|
140
|
+
return typeof href === "string" && href === textNode.value;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
131
145
|
## Options
|
|
132
146
|
|
|
133
147
|
`punctilio` doesn’t enable all transformations by default. Fractions and degrees tend to match too aggressively (perfectly applying the degree transformation requires semantic meaning). Superscript letters and punctuation ligatures have spotty font support. Furthermore, `ligatures = true` can change the meaning of text by collapsing question and exclamation marks.
|
package/dist/rehype.d.ts
CHANGED
|
@@ -13,10 +13,32 @@ import { type TransformOptions } from "./index.js";
|
|
|
13
13
|
type ElementPredicate = (node: Element) => boolean;
|
|
14
14
|
/** Function that transforms a plain-text string (e.g. smart quotes, dashes). */
|
|
15
15
|
type TextTransformer = (input: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Predicate that decides whether an individual text node should be excluded
|
|
18
|
+
* from transformation. Called after element-level `shouldSkip`, so it is
|
|
19
|
+
* never invoked for text inside elements that are already skipped.
|
|
20
|
+
*
|
|
21
|
+
* `ancestors` is the chain of Element parents for the text node, ordered
|
|
22
|
+
* root first, nearest last.
|
|
23
|
+
*/
|
|
24
|
+
export type TextNodeSkipPredicate = (textNode: Text, ancestors: readonly Element[]) => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Options shared by functions that flatten and transform text nodes inside
|
|
27
|
+
* an element tree.
|
|
28
|
+
*/
|
|
29
|
+
export interface ElementTransformOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Optional per-text-node skip predicate. When it returns `true` for a
|
|
32
|
+
* given text node, that node is excluded from the flattened output, is
|
|
33
|
+
* not passed to the transform function, and its `.value` is left
|
|
34
|
+
* untouched. Applied after element-level `shouldSkip`.
|
|
35
|
+
*/
|
|
36
|
+
shouldSkipText?: TextNodeSkipPredicate;
|
|
37
|
+
}
|
|
16
38
|
/**
|
|
17
39
|
* Options for the rehype-punctilio plugin.
|
|
18
40
|
*/
|
|
19
|
-
export interface RehypePunctilioOptions extends TransformOptions {
|
|
41
|
+
export interface RehypePunctilioOptions extends TransformOptions, ElementTransformOptions {
|
|
20
42
|
/**
|
|
21
43
|
* HTML tag names to skip when applying transformations.
|
|
22
44
|
* Content inside these elements won't have formatting improvements applied.
|
|
@@ -38,7 +60,10 @@ export interface RehypePunctilioOptions extends TransformOptions {
|
|
|
38
60
|
*
|
|
39
61
|
* @param node - The element or element content to process
|
|
40
62
|
* @param shouldSkip - Function to determine which elements to skip
|
|
41
|
-
* @param
|
|
63
|
+
* @param options - Optional flattening options. Pass `shouldSkipText` to
|
|
64
|
+
* exclude individual text nodes from the result. Applied after
|
|
65
|
+
* element-level `shouldSkip`, so the hook is never invoked for text
|
|
66
|
+
* inside an already-skipped element.
|
|
42
67
|
* @returns Array of Text nodes
|
|
43
68
|
*
|
|
44
69
|
* @example
|
|
@@ -46,7 +71,7 @@ export interface RehypePunctilioOptions extends TransformOptions {
|
|
|
46
71
|
* const textNodes = flattenTextNodes(paragraphElement, (el) => el.tagName === 'code')
|
|
47
72
|
* ```
|
|
48
73
|
*/
|
|
49
|
-
export declare function flattenTextNodes(node: Element | ElementContent, shouldSkip: ElementPredicate,
|
|
74
|
+
export declare function flattenTextNodes(node: Element | ElementContent, shouldSkip: ElementPredicate, options?: ElementTransformOptions): Text[];
|
|
50
75
|
/**
|
|
51
76
|
* Extracts concatenated text content from an element.
|
|
52
77
|
*
|
|
@@ -110,6 +135,9 @@ export declare function assertSmartQuotesMatch(input: string): void;
|
|
|
110
135
|
* `stripMarkers(transform(textWithMarkers)) === transform(stripMarkers(text))`.
|
|
111
136
|
* Useful for debugging transforms that accidentally interact with markers.
|
|
112
137
|
* Default: false
|
|
138
|
+
* @param options - Optional transform options. Pass `shouldSkipText` here to
|
|
139
|
+
* opt individual text nodes out of transformation without skipping their
|
|
140
|
+
* enclosing element. Skipped text nodes keep their original `.value`.
|
|
113
141
|
* @throws Error if transformation alters the number of text nodes
|
|
114
142
|
* @throws Error if checkInvariance is true and the invariance check fails
|
|
115
143
|
*
|
|
@@ -126,7 +154,7 @@ export declare function assertSmartQuotesMatch(input: string): void;
|
|
|
126
154
|
* )
|
|
127
155
|
* ```
|
|
128
156
|
*/
|
|
129
|
-
export declare function transformElement(node: Element, transformFn: TextTransformer, shouldSkip: ElementPredicate, separator: string, checkInvariance?: boolean): void;
|
|
157
|
+
export declare function transformElement(node: Element, transformFn: TextTransformer, shouldSkip: ElementPredicate, separator: string, checkInvariance?: boolean, options?: ElementTransformOptions): void;
|
|
130
158
|
/**
|
|
131
159
|
* Collects elements that should have text transformations applied.
|
|
132
160
|
*
|
package/dist/rehype.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rehype.d.ts","sourceRoot":"","sources":["../src/rehype.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,MAAM,CAAA;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAI1C,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAI7D,8FAA8F;AAC9F,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAA;AAElD,gFAAgF;AAChF,KAAK,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;AAEhD;;GAEG;AACH,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"rehype.d.ts","sourceRoot":"","sources":["../src/rehype.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,MAAM,CAAA;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAI1C,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAI7D,8FAA8F;AAC9F,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAA;AAElD,gFAAgF;AAChF,KAAK,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAClC,QAAQ,EAAE,IAAI,EACd,SAAS,EAAE,SAAS,OAAO,EAAE,KAC1B,OAAO,CAAA;AAEZ;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,qBAAqB,CAAA;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,sBACf,SAAQ,gBAAgB,EACtB,uBAAuB;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IAEnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAwCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,OAAO,GAAG,cAAc,EAC9B,UAAU,EAAE,gBAAgB,EAC5B,OAAO,CAAC,EAAE,uBAAuB,GAChC,IAAI,EAAE,CAMR;AAuCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,OAAO,EACb,UAAU,GAAE,gBAA8B,GACzC,MAAM,CAIR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,GAAG,WAAW,EAC1B,KAAK,GAAE,MAAU,GAChB,IAAI,GAAG,IAAI,CAeb;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAuB1D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,MAAM,EACjB,eAAe,GAAE,OAAe,EAChC,OAAO,CAAC,EAAE,uBAAuB,GAChC,IAAI,CAgCN;AAwGD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,gBAAgB,EAC5B,KAAK,GAAE,MAAU,GAChB,OAAO,EAAE,CAsCX;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,sBAA2B,GACnC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAuEzB;AAED,eAAe,eAAe,CAAA"}
|
package/dist/rehype.js
CHANGED
|
@@ -46,7 +46,10 @@ function hasAncestor(ancestors, predicate) {
|
|
|
46
46
|
*
|
|
47
47
|
* @param node - The element or element content to process
|
|
48
48
|
* @param shouldSkip - Function to determine which elements to skip
|
|
49
|
-
* @param
|
|
49
|
+
* @param options - Optional flattening options. Pass `shouldSkipText` to
|
|
50
|
+
* exclude individual text nodes from the result. Applied after
|
|
51
|
+
* element-level `shouldSkip`, so the hook is never invoked for text
|
|
52
|
+
* inside an already-skipped element.
|
|
50
53
|
* @returns Array of Text nodes
|
|
51
54
|
*
|
|
52
55
|
* @example
|
|
@@ -54,7 +57,14 @@ function hasAncestor(ancestors, predicate) {
|
|
|
54
57
|
* const textNodes = flattenTextNodes(paragraphElement, (el) => el.tagName === 'code')
|
|
55
58
|
* ```
|
|
56
59
|
*/
|
|
57
|
-
export function flattenTextNodes(node, shouldSkip,
|
|
60
|
+
export function flattenTextNodes(node, shouldSkip, options) {
|
|
61
|
+
const shouldSkipText = options?.shouldSkipText;
|
|
62
|
+
// Only track ancestors when shouldSkipText needs them, to avoid the
|
|
63
|
+
// O(n²) allocation cost of copying the ancestor chain at every level.
|
|
64
|
+
const ancestors = shouldSkipText ? [] : null;
|
|
65
|
+
return flattenTextNodesImpl(node, shouldSkip, shouldSkipText, ancestors, 0);
|
|
66
|
+
}
|
|
67
|
+
function flattenTextNodesImpl(node, shouldSkip, shouldSkipText, ancestors, depth) {
|
|
58
68
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
59
69
|
return [];
|
|
60
70
|
}
|
|
@@ -62,10 +72,21 @@ export function flattenTextNodes(node, shouldSkip, depth = 0) {
|
|
|
62
72
|
return [];
|
|
63
73
|
}
|
|
64
74
|
if (node.type === "text") {
|
|
75
|
+
// Snapshot ancestors at the callback boundary — the walker mutates the
|
|
76
|
+
// shared array as it recurses, so handing it out directly would let a
|
|
77
|
+
// caller observe a stale view if they captured the reference.
|
|
78
|
+
if (shouldSkipText && ancestors && shouldSkipText(node, ancestors.slice())) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
65
81
|
return [node];
|
|
66
82
|
}
|
|
67
83
|
if (node.type === "element") {
|
|
68
|
-
|
|
84
|
+
if (ancestors)
|
|
85
|
+
ancestors.push(node);
|
|
86
|
+
const result = node.children.flatMap((child) => flattenTextNodesImpl(child, shouldSkip, shouldSkipText, ancestors, depth + 1));
|
|
87
|
+
if (ancestors)
|
|
88
|
+
ancestors.pop();
|
|
89
|
+
return result;
|
|
69
90
|
}
|
|
70
91
|
return [];
|
|
71
92
|
}
|
|
@@ -173,6 +194,9 @@ export function assertSmartQuotesMatch(input) {
|
|
|
173
194
|
* `stripMarkers(transform(textWithMarkers)) === transform(stripMarkers(text))`.
|
|
174
195
|
* Useful for debugging transforms that accidentally interact with markers.
|
|
175
196
|
* Default: false
|
|
197
|
+
* @param options - Optional transform options. Pass `shouldSkipText` here to
|
|
198
|
+
* opt individual text nodes out of transformation without skipping their
|
|
199
|
+
* enclosing element. Skipped text nodes keep their original `.value`.
|
|
176
200
|
* @throws Error if transformation alters the number of text nodes
|
|
177
201
|
* @throws Error if checkInvariance is true and the invariance check fails
|
|
178
202
|
*
|
|
@@ -189,13 +213,12 @@ export function assertSmartQuotesMatch(input) {
|
|
|
189
213
|
* )
|
|
190
214
|
* ```
|
|
191
215
|
*/
|
|
192
|
-
export function transformElement(node, transformFn, shouldSkip, separator, checkInvariance = false) {
|
|
216
|
+
export function transformElement(node, transformFn, shouldSkip, separator, checkInvariance = false, options) {
|
|
193
217
|
/* istanbul ignore if -- defensive: elements should always have children array */
|
|
194
218
|
if (!node?.children) {
|
|
195
219
|
return;
|
|
196
220
|
}
|
|
197
|
-
const textNodes = flattenTextNodes(node, shouldSkip);
|
|
198
|
-
/* istanbul ignore if -- only hit when element has no text descendants */
|
|
221
|
+
const textNodes = flattenTextNodes(node, shouldSkip, options);
|
|
199
222
|
if (textNodes.length === 0) {
|
|
200
223
|
return;
|
|
201
224
|
}
|
|
@@ -401,7 +424,7 @@ function markDescendants(node, set, depth = 0) {
|
|
|
401
424
|
* ```
|
|
402
425
|
*/
|
|
403
426
|
export function rehypePunctilio(options = {}) {
|
|
404
|
-
const { skipTags = DEFAULT_SKIP_TAGS, skipClasses = [], separator = DEFAULT_SEPARATOR, ...transformOptions } = options;
|
|
427
|
+
const { skipTags = DEFAULT_SKIP_TAGS, skipClasses = [], separator = DEFAULT_SEPARATOR, shouldSkipText, ...transformOptions } = options;
|
|
405
428
|
const skipTagSet = new Set(skipTags);
|
|
406
429
|
const shouldSkip = (node) => {
|
|
407
430
|
if (skipTagSet.has(node.tagName)) {
|
|
@@ -423,6 +446,12 @@ export function rehypePunctilio(options = {}) {
|
|
|
423
446
|
const transformFn = (text) => {
|
|
424
447
|
return transform(text, pluginOptions);
|
|
425
448
|
};
|
|
449
|
+
// Allocate the per-element options object once; it's read-only inside
|
|
450
|
+
// transformElement, so sharing across calls is safe and avoids a fresh
|
|
451
|
+
// allocation per transformable element.
|
|
452
|
+
const elementOptions = shouldSkipText
|
|
453
|
+
? { shouldSkipText }
|
|
454
|
+
: undefined;
|
|
426
455
|
return (tree) => {
|
|
427
456
|
// Track transformed elements to avoid double-processing
|
|
428
457
|
const transformed = new Set();
|
|
@@ -442,7 +471,7 @@ export function rehypePunctilio(options = {}) {
|
|
|
442
471
|
const elementsToTransform = collectTransformableElements(node, shouldSkip);
|
|
443
472
|
for (const elt of elementsToTransform) {
|
|
444
473
|
if (!transformed.has(elt)) {
|
|
445
|
-
transformElement(elt, transformFn, shouldSkip, separator);
|
|
474
|
+
transformElement(elt, transformFn, shouldSkip, separator, false, elementOptions);
|
|
446
475
|
transformed.add(elt);
|
|
447
476
|
// Mark all descendants as processed since their text was included
|
|
448
477
|
markDescendants(elt, transformed);
|
package/dist/rehype.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rehype.js","sourceRoot":"","sources":["../src/rehype.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEvD,OAAO,EAAE,SAAS,EAAyB,MAAM,YAAY,CAAA;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"rehype.js","sourceRoot":"","sources":["../src/rehype.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/rehype.ts
CHANGED
|
@@ -22,10 +22,39 @@ type ElementPredicate = (node: Element) => boolean
|
|
|
22
22
|
/** Function that transforms a plain-text string (e.g. smart quotes, dashes). */
|
|
23
23
|
type TextTransformer = (input: string) => string
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Predicate that decides whether an individual text node should be excluded
|
|
27
|
+
* from transformation. Called after element-level `shouldSkip`, so it is
|
|
28
|
+
* never invoked for text inside elements that are already skipped.
|
|
29
|
+
*
|
|
30
|
+
* `ancestors` is the chain of Element parents for the text node, ordered
|
|
31
|
+
* root first, nearest last.
|
|
32
|
+
*/
|
|
33
|
+
export type TextNodeSkipPredicate = (
|
|
34
|
+
textNode: Text,
|
|
35
|
+
ancestors: readonly Element[],
|
|
36
|
+
) => boolean
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Options shared by functions that flatten and transform text nodes inside
|
|
40
|
+
* an element tree.
|
|
41
|
+
*/
|
|
42
|
+
export interface ElementTransformOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Optional per-text-node skip predicate. When it returns `true` for a
|
|
45
|
+
* given text node, that node is excluded from the flattened output, is
|
|
46
|
+
* not passed to the transform function, and its `.value` is left
|
|
47
|
+
* untouched. Applied after element-level `shouldSkip`.
|
|
48
|
+
*/
|
|
49
|
+
shouldSkipText?: TextNodeSkipPredicate
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
/**
|
|
26
53
|
* Options for the rehype-punctilio plugin.
|
|
27
54
|
*/
|
|
28
|
-
export interface RehypePunctilioOptions
|
|
55
|
+
export interface RehypePunctilioOptions
|
|
56
|
+
extends TransformOptions,
|
|
57
|
+
ElementTransformOptions {
|
|
29
58
|
/**
|
|
30
59
|
* HTML tag names to skip when applying transformations.
|
|
31
60
|
* Content inside these elements won't have formatting improvements applied.
|
|
@@ -87,7 +116,10 @@ function hasAncestor(
|
|
|
87
116
|
*
|
|
88
117
|
* @param node - The element or element content to process
|
|
89
118
|
* @param shouldSkip - Function to determine which elements to skip
|
|
90
|
-
* @param
|
|
119
|
+
* @param options - Optional flattening options. Pass `shouldSkipText` to
|
|
120
|
+
* exclude individual text nodes from the result. Applied after
|
|
121
|
+
* element-level `shouldSkip`, so the hook is never invoked for text
|
|
122
|
+
* inside an already-skipped element.
|
|
91
123
|
* @returns Array of Text nodes
|
|
92
124
|
*
|
|
93
125
|
* @example
|
|
@@ -98,7 +130,21 @@ function hasAncestor(
|
|
|
98
130
|
export function flattenTextNodes(
|
|
99
131
|
node: Element | ElementContent,
|
|
100
132
|
shouldSkip: ElementPredicate,
|
|
101
|
-
|
|
133
|
+
options?: ElementTransformOptions
|
|
134
|
+
): Text[] {
|
|
135
|
+
const shouldSkipText = options?.shouldSkipText
|
|
136
|
+
// Only track ancestors when shouldSkipText needs them, to avoid the
|
|
137
|
+
// O(n²) allocation cost of copying the ancestor chain at every level.
|
|
138
|
+
const ancestors: Element[] | null = shouldSkipText ? [] : null
|
|
139
|
+
return flattenTextNodesImpl(node, shouldSkip, shouldSkipText, ancestors, 0)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function flattenTextNodesImpl(
|
|
143
|
+
node: Element | ElementContent,
|
|
144
|
+
shouldSkip: ElementPredicate,
|
|
145
|
+
shouldSkipText: TextNodeSkipPredicate | undefined,
|
|
146
|
+
ancestors: Element[] | null,
|
|
147
|
+
depth: number
|
|
102
148
|
): Text[] {
|
|
103
149
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
104
150
|
return []
|
|
@@ -109,11 +155,22 @@ export function flattenTextNodes(
|
|
|
109
155
|
}
|
|
110
156
|
|
|
111
157
|
if (node.type === "text") {
|
|
158
|
+
// Snapshot ancestors at the callback boundary — the walker mutates the
|
|
159
|
+
// shared array as it recurses, so handing it out directly would let a
|
|
160
|
+
// caller observe a stale view if they captured the reference.
|
|
161
|
+
if (shouldSkipText && ancestors && shouldSkipText(node, ancestors.slice())) {
|
|
162
|
+
return []
|
|
163
|
+
}
|
|
112
164
|
return [node]
|
|
113
165
|
}
|
|
114
166
|
|
|
115
167
|
if (node.type === "element") {
|
|
116
|
-
|
|
168
|
+
if (ancestors) ancestors.push(node)
|
|
169
|
+
const result = node.children.flatMap((child) =>
|
|
170
|
+
flattenTextNodesImpl(child, shouldSkip, shouldSkipText, ancestors, depth + 1)
|
|
171
|
+
)
|
|
172
|
+
if (ancestors) ancestors.pop()
|
|
173
|
+
return result
|
|
117
174
|
}
|
|
118
175
|
|
|
119
176
|
return []
|
|
@@ -233,6 +290,9 @@ export function assertSmartQuotesMatch(input: string): void {
|
|
|
233
290
|
* `stripMarkers(transform(textWithMarkers)) === transform(stripMarkers(text))`.
|
|
234
291
|
* Useful for debugging transforms that accidentally interact with markers.
|
|
235
292
|
* Default: false
|
|
293
|
+
* @param options - Optional transform options. Pass `shouldSkipText` here to
|
|
294
|
+
* opt individual text nodes out of transformation without skipping their
|
|
295
|
+
* enclosing element. Skipped text nodes keep their original `.value`.
|
|
236
296
|
* @throws Error if transformation alters the number of text nodes
|
|
237
297
|
* @throws Error if checkInvariance is true and the invariance check fails
|
|
238
298
|
*
|
|
@@ -254,15 +314,15 @@ export function transformElement(
|
|
|
254
314
|
transformFn: TextTransformer,
|
|
255
315
|
shouldSkip: ElementPredicate,
|
|
256
316
|
separator: string,
|
|
257
|
-
checkInvariance: boolean = false
|
|
317
|
+
checkInvariance: boolean = false,
|
|
318
|
+
options?: ElementTransformOptions
|
|
258
319
|
): void {
|
|
259
320
|
/* istanbul ignore if -- defensive: elements should always have children array */
|
|
260
321
|
if (!node?.children) {
|
|
261
322
|
return
|
|
262
323
|
}
|
|
263
324
|
|
|
264
|
-
const textNodes = flattenTextNodes(node, shouldSkip)
|
|
265
|
-
/* istanbul ignore if -- only hit when element has no text descendants */
|
|
325
|
+
const textNodes = flattenTextNodes(node, shouldSkip, options)
|
|
266
326
|
if (textNodes.length === 0) {
|
|
267
327
|
return
|
|
268
328
|
}
|
|
@@ -506,6 +566,7 @@ export function rehypePunctilio(
|
|
|
506
566
|
skipTags = DEFAULT_SKIP_TAGS,
|
|
507
567
|
skipClasses = [],
|
|
508
568
|
separator = DEFAULT_SEPARATOR,
|
|
569
|
+
shouldSkipText,
|
|
509
570
|
...transformOptions
|
|
510
571
|
} = options
|
|
511
572
|
|
|
@@ -534,6 +595,13 @@ export function rehypePunctilio(
|
|
|
534
595
|
return transform(text, pluginOptions)
|
|
535
596
|
}
|
|
536
597
|
|
|
598
|
+
// Allocate the per-element options object once; it's read-only inside
|
|
599
|
+
// transformElement, so sharing across calls is safe and avoids a fresh
|
|
600
|
+
// allocation per transformable element.
|
|
601
|
+
const elementOptions: ElementTransformOptions | undefined = shouldSkipText
|
|
602
|
+
? { shouldSkipText }
|
|
603
|
+
: undefined
|
|
604
|
+
|
|
537
605
|
return (tree: Root) => {
|
|
538
606
|
// Track transformed elements to avoid double-processing
|
|
539
607
|
const transformed = new Set<Element>()
|
|
@@ -556,7 +624,7 @@ export function rehypePunctilio(
|
|
|
556
624
|
const elementsToTransform = collectTransformableElements(node, shouldSkip)
|
|
557
625
|
for (const elt of elementsToTransform) {
|
|
558
626
|
if (!transformed.has(elt)) {
|
|
559
|
-
transformElement(elt, transformFn, shouldSkip, separator)
|
|
627
|
+
transformElement(elt, transformFn, shouldSkip, separator, false, elementOptions)
|
|
560
628
|
transformed.add(elt)
|
|
561
629
|
// Mark all descendants as processed since their text was included
|
|
562
630
|
markDescendants(elt, transformed)
|
package/src/tests/rehype.test.ts
CHANGED
|
@@ -298,6 +298,119 @@ describe("rehypePunctilio", () => {
|
|
|
298
298
|
})
|
|
299
299
|
})
|
|
300
300
|
|
|
301
|
+
describe("shouldSkipText", () => {
|
|
302
|
+
const toUpper = (s: string) => s.toUpperCase()
|
|
303
|
+
|
|
304
|
+
it("is not called for text inside elements excluded by shouldSkip", () => {
|
|
305
|
+
const element = h("p", ["before ", h("code", "unchanged"), " after"]) as Element
|
|
306
|
+
const visitedValues: string[] = []
|
|
307
|
+
transformElement(element, toUpper, ignoreCode, DEFAULT_SEPARATOR, false, {
|
|
308
|
+
shouldSkipText: (textNode) => {
|
|
309
|
+
visitedValues.push(textNode.value)
|
|
310
|
+
return false
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
// Only "before " and " after" are visited — the code child short-circuits.
|
|
314
|
+
expect(visitedValues).toEqual(["before ", " after"])
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it("leaves a skipped text node's value untouched", () => {
|
|
318
|
+
const element = h("p", "hello") as Element
|
|
319
|
+
transformElement(
|
|
320
|
+
element, toUpper, ignoreNone, DEFAULT_SEPARATOR, false,
|
|
321
|
+
{ shouldSkipText: () => true },
|
|
322
|
+
)
|
|
323
|
+
expect((element.children[0] as Text).value).toBe("hello")
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it("passes ancestors root-first, nearest-last", () => {
|
|
327
|
+
const tree = h("div", [h("p", [h("em", "inner")])]) as Element
|
|
328
|
+
const captured: string[][] = []
|
|
329
|
+
flattenTextNodes(tree, ignoreNone, {
|
|
330
|
+
shouldSkipText: (_textNode, ancestors) => {
|
|
331
|
+
captured.push(ancestors.map((a) => a.tagName))
|
|
332
|
+
return false
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
expect(captured).toEqual([["div", "p", "em"]])
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it("hands out a snapshot of ancestors, not the live mutable array", () => {
|
|
339
|
+
// If the walker handed out the shared live array, the captured
|
|
340
|
+
// reference would be empty (or partially popped) by the time we
|
|
341
|
+
// inspect it after the walk completes.
|
|
342
|
+
const tree = h("div", [h("p", [h("em", "inner")])]) as Element
|
|
343
|
+
let captured: readonly Element[] | null = null
|
|
344
|
+
flattenTextNodes(tree, ignoreNone, {
|
|
345
|
+
shouldSkipText: (_textNode, ancestors) => {
|
|
346
|
+
captured = ancestors
|
|
347
|
+
return false
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
expect(captured).not.toBeNull()
|
|
351
|
+
expect(captured!.map((a) => a.tagName)).toEqual(["div", "p", "em"])
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it("gives each text node its own ancestor chain across the same walk", () => {
|
|
355
|
+
// shallow text under <div>, deep text under <div><p><em>; the chains
|
|
356
|
+
// must differ. A bug that forgets to pop or shares the live array
|
|
357
|
+
// would conflate them.
|
|
358
|
+
const tree = h("div", [
|
|
359
|
+
"shallow",
|
|
360
|
+
h("p", [h("em", "deep")]),
|
|
361
|
+
]) as Element
|
|
362
|
+
const captured: Record<string, string[]> = {}
|
|
363
|
+
flattenTextNodes(tree, ignoreNone, {
|
|
364
|
+
shouldSkipText: (textNode, ancestors) => {
|
|
365
|
+
captured[textNode.value] = ancestors.map((a) => a.tagName)
|
|
366
|
+
return false
|
|
367
|
+
},
|
|
368
|
+
})
|
|
369
|
+
expect(captured).toEqual({
|
|
370
|
+
shallow: ["div"],
|
|
371
|
+
deep: ["div", "p", "em"],
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it("transforms accepted text while leaving rejected text literal", () => {
|
|
376
|
+
const element = h("p", ["hello ", h("em", "world")]) as Element
|
|
377
|
+
transformElement(
|
|
378
|
+
element, toUpper, ignoreNone, DEFAULT_SEPARATOR, false,
|
|
379
|
+
{ shouldSkipText: (textNode) => textNode.value === "hello " },
|
|
380
|
+
)
|
|
381
|
+
expect((element.children[0] as Text).value).toBe("hello ")
|
|
382
|
+
expect(((element.children[1] as Element).children[0] as Text).value).toBe("WORLD")
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it("invariance check passes when some text nodes are skipped", () => {
|
|
386
|
+
const element = h("p", ["hello ", h("em", "world")]) as Element
|
|
387
|
+
expect(() =>
|
|
388
|
+
transformElement(
|
|
389
|
+
element, toUpper, ignoreNone, DEFAULT_SEPARATOR, true,
|
|
390
|
+
{ shouldSkipText: (textNode) => textNode.value === "hello " },
|
|
391
|
+
)
|
|
392
|
+
).not.toThrow()
|
|
393
|
+
expect((element.children[0] as Text).value).toBe("hello ")
|
|
394
|
+
expect(((element.children[1] as Element).children[0] as Text).value).toBe("WORLD")
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it("plugin option skips text matching predicate end-to-end", async () => {
|
|
398
|
+
const html = '<p>Visit <a href="https://example.com/x">https://example.com/x</a> now.</p>'
|
|
399
|
+
const result = await processHtml(html, {
|
|
400
|
+
nbsp: false,
|
|
401
|
+
shouldSkipText: (textNode, ancestors) => {
|
|
402
|
+
const parent = ancestors[ancestors.length - 1]
|
|
403
|
+
if (parent?.tagName !== "a") return false
|
|
404
|
+
const href = parent.properties?.href
|
|
405
|
+
return typeof href === "string" && href === textNode.value
|
|
406
|
+
},
|
|
407
|
+
})
|
|
408
|
+
// The anchor text is preserved literally (slashes unchanged); surrounding
|
|
409
|
+
// text is transformed (nothing to change here, but no crash).
|
|
410
|
+
expect(result).toBe('<p>Visit <a href="https://example.com/x">https://example.com/x</a> now.</p>')
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
|
|
301
414
|
describe("getTextContent", () => {
|
|
302
415
|
it.each([
|
|
303
416
|
["simple text", fixtures.simple, ignoreNone, "Hello, world!"],
|