punctilio 4.3.1 → 5.0.1
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 +37 -23
- package/dist/constants.d.ts +0 -27
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -40
- package/dist/constants.js.map +1 -1
- package/dist/dashes.d.ts +11 -7
- package/dist/dashes.d.ts.map +1 -1
- package/dist/dashes.js +423 -284
- package/dist/dashes.js.map +1 -1
- package/dist/index.d.ts +14 -118
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -116
- package/dist/index.js.map +1 -1
- package/dist/markdown.js +1 -1
- package/dist/markdown.js.map +1 -1
- package/dist/nbsp.d.ts +19 -13
- package/dist/nbsp.d.ts.map +1 -1
- package/dist/nbsp.js +214 -69
- package/dist/nbsp.js.map +1 -1
- package/dist/prose-view.d.ts +48 -7
- package/dist/prose-view.d.ts.map +1 -1
- package/dist/prose-view.js +163 -27
- package/dist/prose-view.js.map +1 -1
- package/dist/quote-classifier.d.ts +5 -4
- package/dist/quote-classifier.d.ts.map +1 -1
- package/dist/quote-classifier.js +601 -88
- package/dist/quote-classifier.js.map +1 -1
- package/dist/quotes.d.ts +12 -4
- package/dist/quotes.d.ts.map +1 -1
- package/dist/quotes.js +24 -27
- package/dist/quotes.js.map +1 -1
- package/dist/rehype.d.ts +53 -8
- package/dist/rehype.d.ts.map +1 -1
- package/dist/rehype.js +105 -62
- package/dist/rehype.js.map +1 -1
- package/dist/remark.d.ts +1 -1
- package/dist/remark.d.ts.map +1 -1
- package/dist/remark.js +6 -9
- package/dist/remark.js.map +1 -1
- package/dist/symbols.d.ts +25 -14
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +812 -220
- package/dist/symbols.js.map +1 -1
- package/dist/transform-options.d.ts +97 -0
- package/dist/transform-options.d.ts.map +1 -0
- package/dist/transform-options.js +34 -0
- package/dist/transform-options.js.map +1 -0
- package/dist/utils.d.ts +0 -16
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +0 -56
- package/dist/utils.js.map +1 -1
- package/package.json +8 -3
- package/src/constants.ts +0 -46
- package/src/dashes.ts +450 -269
- package/src/index.ts +70 -291
- package/src/markdown.ts +1 -1
- package/src/nbsp.ts +255 -78
- package/src/prose-view.ts +234 -39
- package/src/quote-classifier.ts +618 -88
- package/src/quotes.ts +36 -19
- package/src/rehype.ts +169 -88
- package/src/remark.ts +6 -11
- package/src/symbols.ts +814 -251
- package/src/transform-options.ts +143 -0
- package/src/utils.ts +0 -82
package/src/quotes.ts
CHANGED
|
@@ -1,31 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { classifyAndRenderQuotes, type PunctuationStyle } from "./quote-classifier.js"
|
|
1
|
+
import { cachedRegExp, UNICODE_SYMBOLS } from "./constants.js"
|
|
2
|
+
import { classifyAndRenderQuotes, convertPrimeMarks, type PunctuationStyle } from "./quote-classifier.js"
|
|
3
|
+
import { overInput, type ProseView, replaceAllInView } from "./prose-view.js"
|
|
3
4
|
|
|
4
5
|
const { RIGHT_SINGLE_QUOTE, MODIFIER_LETTER_APOSTROPHE } = UNICODE_SYMBOLS
|
|
5
6
|
|
|
6
7
|
export { PUNCTUATION_STYLES, type PunctuationStyle, type QuoteRole } from "./quote-classifier.js"
|
|
7
8
|
|
|
8
9
|
export interface QuoteOptions {
|
|
9
|
-
/** Boundary marker for HTML element boundaries. Default: "\uE000\uE001" */
|
|
10
|
-
separator?: string
|
|
11
10
|
/** "american" (inside), "british" (outside), "none". Default: "american" */
|
|
12
11
|
punctuationStyle?: PunctuationStyle
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Convert prime-mark candidates (5'10" → 5′10″) before quote
|
|
14
|
+
* classification. `false` leaves them to the quote rules. Applies to
|
|
15
|
+
* `niceQuotes` only; `classifyApostrophes` never converts primes.
|
|
16
|
+
* Default: true
|
|
17
|
+
*/
|
|
18
|
+
primes?: boolean
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
/** Convert straight quotes to smart quotes. */
|
|
23
|
-
export function niceQuotes(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return
|
|
28
|
-
|
|
22
|
+
export function niceQuotes(input: string, options?: QuoteOptions): string
|
|
23
|
+
export function niceQuotes(input: ProseView, options?: QuoteOptions): void
|
|
24
|
+
export function niceQuotes(input: string | ProseView, options: QuoteOptions = {}): string | void {
|
|
25
|
+
const punctuationStyle = options.punctuationStyle ?? "american"
|
|
26
|
+
return overInput(input, (view) => {
|
|
27
|
+
if (punctuationStyle !== "none") {
|
|
28
|
+
if (options.primes !== false) {
|
|
29
|
+
convertPrimeMarks(view)
|
|
30
|
+
}
|
|
31
|
+
// Apostrophes render as U+2019, the same glyph as closing single quotes,
|
|
32
|
+
// per the Unicode Standard.
|
|
33
|
+
classifyAndRenderQuotes(view, punctuationStyle, RIGHT_SINGLE_QUOTE)
|
|
34
|
+
}
|
|
35
|
+
// Any U+02BC already in the input is normalized to U+2019 as well
|
|
36
|
+
// (including under punctuationStyle "none").
|
|
37
|
+
replaceAllInView(view, cachedRegExp(MODIFIER_LETTER_APOSTROPHE, "g"), () => RIGHT_SINGLE_QUOTE)
|
|
38
|
+
})
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
/**
|
|
@@ -38,6 +48,13 @@ export function niceQuotes(text: string, options: QuoteOptions = {}): string {
|
|
|
38
48
|
* For display output, use {@link niceQuotes} or `transform()` instead,
|
|
39
49
|
* which use U+2019 for both per the Unicode Standard.
|
|
40
50
|
*/
|
|
41
|
-
export function classifyApostrophes(
|
|
42
|
-
|
|
51
|
+
export function classifyApostrophes(input: string, options?: QuoteOptions): string
|
|
52
|
+
export function classifyApostrophes(input: ProseView, options?: QuoteOptions): void
|
|
53
|
+
export function classifyApostrophes(input: string | ProseView, options: QuoteOptions = {}): string | void {
|
|
54
|
+
const punctuationStyle = options.punctuationStyle ?? "american"
|
|
55
|
+
return overInput(input, (view) => {
|
|
56
|
+
if (punctuationStyle !== "none") {
|
|
57
|
+
classifyAndRenderQuotes(view, punctuationStyle, MODIFIER_LETTER_APOSTROPHE)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
43
60
|
}
|
package/src/rehype.ts
CHANGED
|
@@ -3,14 +3,13 @@ import type { Transformer } from "unified"
|
|
|
3
3
|
|
|
4
4
|
import { SKIP, visitParents } from "unist-util-visit-parents"
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { transformView } from "./index.js"
|
|
7
|
+
import { TRANSFORM_OPTION_KEYS, type TransformOptions } from "./transform-options.js"
|
|
8
|
+
import { MAX_RECURSION_DEPTH } from "./constants.js"
|
|
8
9
|
import { assertKnownOptionKeys, formatErrorString } from "./utils.js"
|
|
9
|
-
import { buildProseView,
|
|
10
|
+
import { buildProseView, type ProsePass, type ProseView } from "./prose-view.js"
|
|
10
11
|
|
|
11
|
-
type ElementPredicate = (node: Element) => boolean
|
|
12
|
-
|
|
13
|
-
type TextTransformer = (input: string) => string
|
|
12
|
+
export type ElementPredicate = (node: Element) => boolean
|
|
14
13
|
|
|
15
14
|
/** Per-text-node skip predicate, called after element-level `shouldSkip`. */
|
|
16
15
|
export type TextNodeSkipPredicate = (
|
|
@@ -186,50 +185,101 @@ export function assertSmartQuotesMatch(input: string): void {
|
|
|
186
185
|
}
|
|
187
186
|
}
|
|
188
187
|
|
|
188
|
+
export interface ProseViewOfOptions extends ElementTransformOptions {
|
|
189
|
+
/** Element-level skip predicate; skipped subtrees contribute no text nodes. */
|
|
190
|
+
shouldSkip?: ElementPredicate
|
|
191
|
+
}
|
|
192
|
+
|
|
189
193
|
/**
|
|
190
|
-
*
|
|
191
|
-
* `
|
|
192
|
-
*
|
|
194
|
+
* Builds a ProseView over the element's transformable text nodes, honoring
|
|
195
|
+
* the element-level `shouldSkip` and per-text-node `shouldSkipText`
|
|
196
|
+
* predicates. Returns null when the element holds no transformable text.
|
|
193
197
|
*/
|
|
194
|
-
export function
|
|
195
|
-
node: Element,
|
|
196
|
-
transformFn: TextTransformer,
|
|
197
|
-
shouldSkip: ElementPredicate,
|
|
198
|
-
separator: string,
|
|
199
|
-
checkInvariance: boolean = false,
|
|
200
|
-
options?: ElementTransformOptions
|
|
201
|
-
): void {
|
|
198
|
+
export function proseViewOf(element: Element, options: ProseViewOfOptions = {}): ProseView | null {
|
|
202
199
|
/* istanbul ignore if -- defensive: elements should always have children array */
|
|
203
|
-
if (!
|
|
204
|
-
return
|
|
200
|
+
if (!element?.children) {
|
|
201
|
+
return null
|
|
205
202
|
}
|
|
206
203
|
|
|
207
|
-
const
|
|
204
|
+
const shouldSkip = options.shouldSkip ?? (() => false)
|
|
205
|
+
const textNodes = flattenTextNodes(element, shouldSkip, options)
|
|
208
206
|
if (textNodes.length === 0) {
|
|
209
|
-
return
|
|
207
|
+
return null
|
|
210
208
|
}
|
|
211
209
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
210
|
+
return buildProseView(textNodes)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* One entry in an `applyPasses` sequence: a bare pass, or a pass with its own
|
|
215
|
+
* skip predicates layered on top of the base options (the per-transform
|
|
216
|
+
* skip-set case, e.g. a fractions pass that additionally skips `<a>`).
|
|
217
|
+
*/
|
|
218
|
+
export type PassEntry =
|
|
219
|
+
| ProsePass
|
|
220
|
+
| { pass: ProsePass; shouldSkip?: ElementPredicate; shouldSkipText?: TextNodeSkipPredicate }
|
|
221
|
+
|
|
222
|
+
interface ResolvedPassEntry {
|
|
223
|
+
pass: ProsePass
|
|
224
|
+
shouldSkip?: ElementPredicate
|
|
225
|
+
shouldSkipText?: TextNodeSkipPredicate
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resolvePassEntry(entry: PassEntry): ResolvedPassEntry {
|
|
229
|
+
return typeof entry === "function" ? { pass: entry } : entry
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** OR-combines an optional entry predicate onto an optional base predicate. */
|
|
233
|
+
function mergePredicates<Args extends unknown[]>(
|
|
234
|
+
base: ((...args: Args) => boolean) | undefined,
|
|
235
|
+
extra: ((...args: Args) => boolean) | undefined,
|
|
236
|
+
): ((...args: Args) => boolean) | undefined {
|
|
237
|
+
if (!base) return extra
|
|
238
|
+
if (!extra) return base
|
|
239
|
+
return (...args) => base(...args) || extra(...args)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Runs `passes` in order over `element`'s transformable text, owning the
|
|
244
|
+
* ProseView lifecycle so callers never touch a view directly. Each pass
|
|
245
|
+
* commits its edits before the next runs, so passes see each other's
|
|
246
|
+
* committed output — the same sequencing `transform()`'s pipeline uses.
|
|
247
|
+
*
|
|
248
|
+
* Entries whose predicates match the previous entry's share one view;
|
|
249
|
+
* an entry with different `shouldSkip`/`shouldSkipText` predicates gets a
|
|
250
|
+
* fresh view (built after the previous pass committed) over the text nodes
|
|
251
|
+
* that survive both the base `options` predicates and its own.
|
|
252
|
+
*/
|
|
253
|
+
export function applyPasses(
|
|
254
|
+
element: Element,
|
|
255
|
+
passes: readonly PassEntry[],
|
|
256
|
+
options: ProseViewOfOptions = {},
|
|
257
|
+
): void {
|
|
258
|
+
let currentView: ProseView | null = null
|
|
259
|
+
let currentPredicates: Pick<ResolvedPassEntry, "shouldSkip" | "shouldSkipText"> | null = null
|
|
260
|
+
|
|
261
|
+
for (const entry of passes) {
|
|
262
|
+
const { pass, shouldSkip, shouldSkipText } = resolvePassEntry(entry)
|
|
263
|
+
|
|
264
|
+
const samePredicates =
|
|
265
|
+
currentPredicates !== null &&
|
|
266
|
+
currentPredicates.shouldSkip === shouldSkip &&
|
|
267
|
+
currentPredicates.shouldSkipText === shouldSkipText
|
|
268
|
+
if (!samePredicates) {
|
|
269
|
+
currentView = proseViewOf(element, {
|
|
270
|
+
shouldSkip: mergePredicates(options.shouldSkip, shouldSkip),
|
|
271
|
+
shouldSkipText: mergePredicates(options.shouldSkipText, shouldSkipText),
|
|
272
|
+
})
|
|
273
|
+
currentPredicates = { shouldSkip, shouldSkipText }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// No transformable text under this entry's predicates; later entries may
|
|
277
|
+
// still see text (their skip sets differ), so keep going.
|
|
278
|
+
if (currentView === null) {
|
|
279
|
+
continue
|
|
232
280
|
}
|
|
281
|
+
|
|
282
|
+
pass(currentView)
|
|
233
283
|
}
|
|
234
284
|
}
|
|
235
285
|
|
|
@@ -334,12 +384,72 @@ function hasTextDescendant(
|
|
|
334
384
|
return false
|
|
335
385
|
}
|
|
336
386
|
|
|
337
|
-
export
|
|
387
|
+
export interface CollectProseBlocksOptions {
|
|
388
|
+
/**
|
|
389
|
+
* HTML tag names to skip. Default: the plugin's default skip list
|
|
390
|
+
* ("code", "pre", "script", ...).
|
|
391
|
+
*/
|
|
392
|
+
skipTags?: string[]
|
|
393
|
+
/** CSS class names whose elements (and their subtrees) are skipped. Default: [] */
|
|
394
|
+
skipClasses?: string[]
|
|
395
|
+
/** Additional element-level skip predicate, OR-ed with the tag/class skips. */
|
|
396
|
+
shouldSkip?: ElementPredicate
|
|
397
|
+
/** Invert the element model as in {@link RehypePunctilioOptions.transformAllElements}. Default: false */
|
|
398
|
+
transformAllElements?: boolean
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Skip predicate and transformable-tag test resolved from the options. */
|
|
402
|
+
interface ResolvedCollectOptions {
|
|
403
|
+
shouldSkip: ElementPredicate
|
|
404
|
+
isTransformable: (tagName: string) => boolean
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function resolveCollectOptions(options: CollectProseBlocksOptions): ResolvedCollectOptions {
|
|
408
|
+
const { skipTags = DEFAULT_SKIP_TAGS, skipClasses = [], shouldSkip, transformAllElements = false } = options
|
|
409
|
+
|
|
410
|
+
// In inverted mode, form-value elements join the skip-list so their literal
|
|
411
|
+
// values are neither transformed nor flattened into a transformable ancestor.
|
|
412
|
+
const skipTagSet = new Set(transformAllElements ? [...skipTags, ...FORM_VALUE_TAGS] : skipTags)
|
|
413
|
+
const skipClassSet = new Set(skipClasses)
|
|
414
|
+
|
|
415
|
+
const hasSkipClass = (node: Element): boolean => {
|
|
416
|
+
if (skipClassSet.size === 0) return false
|
|
417
|
+
const classNames = node.properties?.className
|
|
418
|
+
const classes = Array.isArray(classNames)
|
|
419
|
+
? classNames.filter((c): c is string => typeof c === "string")
|
|
420
|
+
: typeof classNames === "string" ? classNames.split(/\s+/) : []
|
|
421
|
+
return classes.some((cls) => skipClassSet.has(cls))
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
shouldSkip: (node) =>
|
|
426
|
+
skipTagSet.has(node.tagName) || hasSkipClass(node) || (shouldSkip?.(node) ?? false),
|
|
427
|
+
// Inverted mode transforms every non-skipped element except non-prose
|
|
428
|
+
// containers; the default keeps the `TRANSFORMABLE_ELEMENTS` allowlist
|
|
429
|
+
// (plus custom elements).
|
|
430
|
+
isTransformable: transformAllElements
|
|
431
|
+
? (tagName) => !NON_PROSE_CONTAINER_TAGS.has(tagName)
|
|
432
|
+
: isTransformableElement,
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Collects the elements under `root` (inclusive) whose text should be
|
|
438
|
+
* transformed as one prose block: transformable elements with direct text or
|
|
439
|
+
* inline-only text descendants. Elements with block-level children recurse so
|
|
440
|
+
* each block transforms independently.
|
|
441
|
+
*/
|
|
442
|
+
export function collectProseBlocks(root: Element, options: CollectProseBlocksOptions = {}): Element[] {
|
|
443
|
+
const { shouldSkip, isTransformable } = resolveCollectOptions(options)
|
|
444
|
+
return collectProseBlocksImpl(root, shouldSkip, 0, undefined, isTransformable)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function collectProseBlocksImpl(
|
|
338
448
|
node: Element,
|
|
339
449
|
shouldSkip: ElementPredicate,
|
|
340
|
-
depth: number
|
|
341
|
-
alreadyTransformed
|
|
342
|
-
isTransformable: (tagName: string) => boolean
|
|
450
|
+
depth: number,
|
|
451
|
+
alreadyTransformed: ReadonlySet<Element> | undefined,
|
|
452
|
+
isTransformable: (tagName: string) => boolean
|
|
343
453
|
): Element[] {
|
|
344
454
|
/* istanbul ignore if -- defensive: prevents stack overflow from malicious HTML */
|
|
345
455
|
if (depth > MAX_RECURSION_DEPTH) {
|
|
@@ -371,7 +481,7 @@ export function collectTransformableElements(
|
|
|
371
481
|
// children that should be independent, or has no text to transform.
|
|
372
482
|
for (const child of node.children) {
|
|
373
483
|
if (child.type === "element") {
|
|
374
|
-
const childResults =
|
|
484
|
+
const childResults = collectProseBlocksImpl(child, shouldSkip, depth + 1, alreadyTransformed, isTransformable)
|
|
375
485
|
for (const r of childResults) results.push(r)
|
|
376
486
|
}
|
|
377
487
|
}
|
|
@@ -400,53 +510,21 @@ export function rehypePunctilio(
|
|
|
400
510
|
assertKnownOptionKeys(options, REHYPE_OPTION_KEYS, "rehypePunctilio")
|
|
401
511
|
|
|
402
512
|
const {
|
|
403
|
-
skipTags
|
|
404
|
-
skipClasses
|
|
405
|
-
separator = DEFAULT_SEPARATOR,
|
|
513
|
+
skipTags,
|
|
514
|
+
skipClasses,
|
|
406
515
|
shouldSkipText,
|
|
407
|
-
transformAllElements
|
|
516
|
+
transformAllElements,
|
|
408
517
|
...transformOptions
|
|
409
518
|
} = options
|
|
410
519
|
|
|
411
|
-
|
|
412
|
-
// values are neither transformed nor flattened into a transformable ancestor.
|
|
413
|
-
const skipTagSet = new Set(transformAllElements ? [...skipTags, ...FORM_VALUE_TAGS] : skipTags)
|
|
414
|
-
const skipClassSet = new Set(skipClasses)
|
|
415
|
-
|
|
416
|
-
// Inverted mode transforms every non-skipped element except non-prose
|
|
417
|
-
// containers; the default keeps the `TRANSFORMABLE_ELEMENTS` allowlist (plus
|
|
418
|
-
// custom elements).
|
|
419
|
-
const isTransformable = transformAllElements
|
|
420
|
-
? (tagName: string) => !NON_PROSE_CONTAINER_TAGS.has(tagName)
|
|
421
|
-
: isTransformableElement
|
|
422
|
-
|
|
423
|
-
const hasSkipClass = (node: Element): boolean => {
|
|
424
|
-
if (skipClassSet.size === 0) return false
|
|
425
|
-
const classNames = node.properties?.className
|
|
426
|
-
const classes = Array.isArray(classNames)
|
|
427
|
-
? classNames.filter((c): c is string => typeof c === "string")
|
|
428
|
-
: typeof classNames === "string" ? classNames.split(/\s+/) : []
|
|
429
|
-
return classes.some((cls) => skipClassSet.has(cls))
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const shouldSkip = (node: Element): boolean =>
|
|
433
|
-
skipTagSet.has(node.tagName) || hasSkipClass(node)
|
|
434
|
-
|
|
435
|
-
const pluginOptions = {
|
|
436
|
-
...transformOptions,
|
|
437
|
-
separator,
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const transformFn = (text: string): string => {
|
|
441
|
-
return transform(text, pluginOptions)
|
|
442
|
-
}
|
|
520
|
+
const { shouldSkip, isTransformable } = resolveCollectOptions({ skipTags, skipClasses, transformAllElements })
|
|
443
521
|
|
|
444
522
|
// Allocate the per-element options object once; it's read-only inside
|
|
445
|
-
//
|
|
523
|
+
// proseViewOf, so sharing across calls is safe and avoids a fresh
|
|
446
524
|
// allocation per transformable element.
|
|
447
|
-
const
|
|
448
|
-
? { shouldSkipText }
|
|
449
|
-
:
|
|
525
|
+
const viewOptions: ProseViewOfOptions = shouldSkipText
|
|
526
|
+
? { shouldSkip, shouldSkipText }
|
|
527
|
+
: { shouldSkip }
|
|
450
528
|
|
|
451
529
|
return (tree: Root) => {
|
|
452
530
|
// Track transformed elements to avoid double-processing
|
|
@@ -463,10 +541,13 @@ export function rehypePunctilio(
|
|
|
463
541
|
}
|
|
464
542
|
|
|
465
543
|
// Collect and transform elements with text content
|
|
466
|
-
const elementsToTransform =
|
|
544
|
+
const elementsToTransform = collectProseBlocksImpl(node, shouldSkip, 0, transformed, isTransformable)
|
|
467
545
|
for (const elt of elementsToTransform) {
|
|
468
546
|
if (!transformed.has(elt)) {
|
|
469
|
-
|
|
547
|
+
const view = proseViewOf(elt, viewOptions)
|
|
548
|
+
if (view !== null) {
|
|
549
|
+
transformView(view, transformOptions)
|
|
550
|
+
}
|
|
470
551
|
transformed.add(elt)
|
|
471
552
|
// Mark all descendants as processed since their text was included
|
|
472
553
|
markDescendants(elt, transformed)
|
package/src/remark.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type { Transformer } from "unified"
|
|
|
3
3
|
|
|
4
4
|
import { visitParents } from "unist-util-visit-parents"
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { transformView } from "./index.js"
|
|
7
|
+
import { TRANSFORM_OPTION_KEYS, type TransformOptions } from "./transform-options.js"
|
|
8
|
+
import { MAX_RECURSION_DEPTH } from "./constants.js"
|
|
9
|
+
import { buildProseView } from "./prose-view.js"
|
|
10
|
+
import { assertKnownOptionKeys } from "./utils.js"
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Same options as `transform()`, except `nbsp` defaults to `false`:
|
|
@@ -49,19 +51,12 @@ export function remarkPunctilio(
|
|
|
49
51
|
): Transformer<Root, Root> {
|
|
50
52
|
assertKnownOptionKeys(options, TRANSFORM_OPTION_KEYS, "remarkPunctilio")
|
|
51
53
|
|
|
52
|
-
const separator = options.separator ?? DEFAULT_SEPARATOR
|
|
53
|
-
|
|
54
54
|
// Markdown is a source format, so default `nbsp` to false (`?? false`
|
|
55
55
|
// rather than spread defaults so an explicit `nbsp: undefined` also gets
|
|
56
56
|
// the Markdown default instead of falling through to transform's).
|
|
57
57
|
const pluginOptions = {
|
|
58
58
|
...options,
|
|
59
59
|
nbsp: options.nbsp ?? false,
|
|
60
|
-
separator,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const transformFn = (text: string): string => {
|
|
64
|
-
return transform(text, pluginOptions)
|
|
65
60
|
}
|
|
66
61
|
|
|
67
62
|
return (tree: Root) => {
|
|
@@ -88,7 +83,7 @@ export function remarkPunctilio(
|
|
|
88
83
|
return
|
|
89
84
|
}
|
|
90
85
|
|
|
91
|
-
|
|
86
|
+
transformView(buildProseView(textNodes), pluginOptions)
|
|
92
87
|
})
|
|
93
88
|
}
|
|
94
89
|
}
|