@readme/markdown 14.10.0 → 14.10.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/dist/lib/render-diff/differ.d.ts +49 -0
- package/dist/lib/render-diff/index.d.ts +2 -0
- package/dist/lib/render-diff/types.d.ts +119 -0
- package/dist/lib/render-fixture/index.d.ts +19 -0
- package/dist/lib/render-fixture/loadFixture.d.ts +21 -0
- package/dist/lib/render-fixture/renderFixture.d.ts +17 -0
- package/dist/main.js +119 -7
- package/dist/main.node.js +119 -7
- package/dist/main.node.js.map +1 -1
- package/dist/render-diff.node.js +3568 -0
- package/dist/render-diff.node.js.map +1 -0
- package/dist/render-fixture.css +6 -0
- package/dist/render-fixture.css.map +1 -0
- package/dist/render-fixture.node.js +126418 -0
- package/dist/render-fixture.node.js.map +1 -0
- package/package.json +30 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural HTML diff for MDX-vs-MDXish render comparison.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm overview:
|
|
5
|
+
* parseDocument(html, { xmlMode: true }) → walk → canonical {tag, attrs, children} tree
|
|
6
|
+
* apply canonicalization pipeline (sort/normalize/collapse)
|
|
7
|
+
* compute bottom-up content hash
|
|
8
|
+
* fast-path match if hashes equal
|
|
9
|
+
* otherwise, recursive walk emitting Change records
|
|
10
|
+
*
|
|
11
|
+
* Tree-walk targets the htmlparser2 / domhandler node shape. The canonicalization
|
|
12
|
+
* rules and severity model are transplanted verbatim from the reference differ.ts
|
|
13
|
+
* (PR #18479 in the readme repo). The tree-walk is re-targeted to htmlparser2/domhandler.
|
|
14
|
+
*
|
|
15
|
+
* The lint disables below are intentional and scoped to this file:
|
|
16
|
+
* - `no-restricted-syntax` + `no-continue`: this is a tree-walker with greedy
|
|
17
|
+
* alignment and lookahead; for-of with continue is the natural shape and
|
|
18
|
+
* array iteration helpers would obscure the control flow.
|
|
19
|
+
* - `@typescript-eslint/no-use-before-define`: helpers are ordered bottom-up
|
|
20
|
+
* (primitives first, public `diff()` last) so the public API anchors the
|
|
21
|
+
* end of the file.
|
|
22
|
+
*/
|
|
23
|
+
import type { DiffOptions, DiffResult } from './types';
|
|
24
|
+
/**
|
|
25
|
+
* Diff two rendered HTML strings.
|
|
26
|
+
*
|
|
27
|
+
* The diff tool is engine-agnostic — `leftHtml` and `rightHtml` are simply
|
|
28
|
+
* the two HTML strings to compare in that order. Common use cases:
|
|
29
|
+
* - MDX vs MDXish (Suite B in this repo)
|
|
30
|
+
* - Before vs after a `@readme/markdown` version bump on the same source
|
|
31
|
+
* - Any two HTML strings a consumer wants to compare
|
|
32
|
+
*
|
|
33
|
+
* Both strings are canonicalized (whitespace collapse, class sort, attribute
|
|
34
|
+
* normalization, noise-attr drop, heading-counter strip, void-tag handling,
|
|
35
|
+
* text-equivalent merging) before comparison.
|
|
36
|
+
*
|
|
37
|
+
* A bottom-up content hash provides a fast-path: if both trees hash identically,
|
|
38
|
+
* `{ status: 'match' }` is returned without a full walk.
|
|
39
|
+
*
|
|
40
|
+
* The returned `changes[]` is ordered by document position and is identical
|
|
41
|
+
* across repeated calls on the same input. Each change carries `left` and
|
|
42
|
+
* `right` fields corresponding to the two inputs.
|
|
43
|
+
*
|
|
44
|
+
* @param leftHtml - First HTML string to compare.
|
|
45
|
+
* @param rightHtml - Second HTML string to compare.
|
|
46
|
+
* @param opts - Optional canonicalization / diff configuration.
|
|
47
|
+
* @returns `{ status: 'match' }` or `{ status: 'differ', severity, changes }`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function diff(leftHtml: string, rightHtml: string, opts?: DiffOptions): DiffResult;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript types for the render-diff HTML diffing tool.
|
|
3
|
+
*
|
|
4
|
+
* These types form the public contract for `lib/render-diff/differ.ts`.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Severity level of a single diff change or aggregate page diff result.
|
|
8
|
+
*
|
|
9
|
+
* - `cosmetic` — minor presentation differences (e.g. `data-*` / `aria-*` attrs)
|
|
10
|
+
* - `structural` — tag, element, or layout differences
|
|
11
|
+
* - `content` — visible text or meaningful attribute value differences
|
|
12
|
+
*/
|
|
13
|
+
export type Severity = 'content' | 'cosmetic' | 'structural';
|
|
14
|
+
/**
|
|
15
|
+
* A single diffed change between two rendered HTML inputs.
|
|
16
|
+
*
|
|
17
|
+
* The diff tool is engine-agnostic — `left` and `right` are simply the two
|
|
18
|
+
* HTML strings passed to `diff()` in that order. Suite B uses them as
|
|
19
|
+
* MDX vs MDXish; other consumers (e.g. before/after a markdown version bump)
|
|
20
|
+
* use them however they like.
|
|
21
|
+
*
|
|
22
|
+
* Each change is emitted at a specific document-position path.
|
|
23
|
+
*/
|
|
24
|
+
export interface Change {
|
|
25
|
+
/**
|
|
26
|
+
* Attribute name — populated only when `kind` is `'attr'`.
|
|
27
|
+
*/
|
|
28
|
+
attrName?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The type of difference detected.
|
|
31
|
+
*
|
|
32
|
+
* - `tag` — element tag mismatch between `left` and `right`
|
|
33
|
+
* - `attr` — attribute present, missing, or different value
|
|
34
|
+
* - `text` — text node content differs
|
|
35
|
+
* - `missing` — node present in `left` but absent in `right`
|
|
36
|
+
* - `extra` — node present in `right` but absent in `left`
|
|
37
|
+
*/
|
|
38
|
+
kind: 'attr' | 'extra' | 'missing' | 'tag' | 'text';
|
|
39
|
+
/**
|
|
40
|
+
* String representation from the `left` input, or `null` when the node is absent.
|
|
41
|
+
*/
|
|
42
|
+
left: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Document-position path of the diffed node (top-to-bottom tree-walk order).
|
|
45
|
+
*/
|
|
46
|
+
path: string;
|
|
47
|
+
/**
|
|
48
|
+
* String representation from the `right` input, or `null` when the node is absent.
|
|
49
|
+
*/
|
|
50
|
+
right: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Severity of this individual change.
|
|
53
|
+
*/
|
|
54
|
+
severity: Severity;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Result returned by `diff(leftHtml, rightHtml)`.
|
|
58
|
+
*
|
|
59
|
+
* Discriminated union on `status`:
|
|
60
|
+
* - `'match'` arm collapses to `{ status: 'match' }` — no `severity` or `changes` keys.
|
|
61
|
+
* - `'differ'` arm carries the full `{ status, severity, changes }` shape.
|
|
62
|
+
*
|
|
63
|
+
* TypeScript narrowing on `result.status === 'differ'` exposes `severity` and `changes`.
|
|
64
|
+
*/
|
|
65
|
+
export type DiffResult = {
|
|
66
|
+
changes: Change[];
|
|
67
|
+
severity: Severity;
|
|
68
|
+
status: 'differ';
|
|
69
|
+
} | {
|
|
70
|
+
status: 'match';
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Canonicalization preset for `diff()`.
|
|
74
|
+
*
|
|
75
|
+
* - `'cross-engine'` — full normalization including span-flatten and adjacent-text
|
|
76
|
+
* merge. Designed for MDX↔MDXish comparison (Suite B).
|
|
77
|
+
* - `'minimal'` — lighter normalization; span-flatten and adjacent-text merge are
|
|
78
|
+
* skipped. Suitable for same-engine before/after comparison where those transforms
|
|
79
|
+
* would mask real structural changes.
|
|
80
|
+
*/
|
|
81
|
+
export type Preset = 'cross-engine' | 'minimal';
|
|
82
|
+
/**
|
|
83
|
+
* Options accepted by `diff()`.
|
|
84
|
+
*
|
|
85
|
+
* All fields are optional. Default behaviour: hash fast-path enabled, all
|
|
86
|
+
* heading counter suffixes stripped, no additional attributes ignored.
|
|
87
|
+
*/
|
|
88
|
+
export interface DiffOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Additional attribute names to drop during canonicalization (lowercased).
|
|
91
|
+
* Added to the built-in noise-attr set (`data-reactroot`, `data-testid`,
|
|
92
|
+
* `suppresshydrationwarning`).
|
|
93
|
+
*/
|
|
94
|
+
attrIgnore?: Set<string>;
|
|
95
|
+
/**
|
|
96
|
+
* When `true`, bypass the content-hash fast-path and always perform the full
|
|
97
|
+
* tree walk. Intended for testing only.
|
|
98
|
+
*/
|
|
99
|
+
noHashFastPath?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* When `true`, skip stripping `-N` counter suffixes from heading `id` attrs.
|
|
102
|
+
* Default: `false` (suffixes are stripped so repeated headings compare equal).
|
|
103
|
+
*/
|
|
104
|
+
preserveHeadingCounters?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Canonicalization preset. Controls which structural-normalization transforms
|
|
107
|
+
* are applied before comparison.
|
|
108
|
+
*
|
|
109
|
+
* - `'cross-engine'` (default) — full normalization including span-flatten
|
|
110
|
+
* and adjacent-text merge. Designed for MDX↔MDXish comparison (Suite B).
|
|
111
|
+
* - `'minimal'` — lighter normalization; span-flatten and adjacent-text merge
|
|
112
|
+
* are skipped. Suitable for same-engine before/after comparison where those
|
|
113
|
+
* transforms would mask real structural changes.
|
|
114
|
+
*
|
|
115
|
+
* Every normalization `'minimal'` performs is also performed by `'cross-engine'`.
|
|
116
|
+
* Default: omitting `preset` is equivalent to `'cross-engine'` (backward compatible).
|
|
117
|
+
*/
|
|
118
|
+
preset?: Preset;
|
|
119
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public barrel for `@readme/markdown/render-fixture`.
|
|
3
|
+
*
|
|
4
|
+
* Pairs with the engine-free `@readme/markdown/render-diff` subpath: this
|
|
5
|
+
* bundle DOES include the engine (it has to — its job is to render). A
|
|
6
|
+
* typical consumer that wants regression coverage across @readme/markdown
|
|
7
|
+
* version bumps pairs the two:
|
|
8
|
+
*
|
|
9
|
+
* import { renderFixture } from '@readme/markdown/render-fixture';
|
|
10
|
+
* import { diff } from '@readme/markdown/render-diff';
|
|
11
|
+
*
|
|
12
|
+
* const oldHtml = readFileSync('snapshot.html', 'utf8');
|
|
13
|
+
* const { html: newHtml } = renderFixture(body, ctx, 'mdxish');
|
|
14
|
+
* const result = diff(oldHtml, newHtml, { preset: 'minimal' });
|
|
15
|
+
*/
|
|
16
|
+
export { loadFixture } from './loadFixture';
|
|
17
|
+
export type { RenderContext } from './loadFixture';
|
|
18
|
+
export { renderFixture } from './renderFixture';
|
|
19
|
+
export type { Engine, FixtureRenderResult } from './renderFixture';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface RenderContext {
|
|
2
|
+
components: {
|
|
3
|
+
source: string;
|
|
4
|
+
tag: string;
|
|
5
|
+
}[];
|
|
6
|
+
glossary: {
|
|
7
|
+
definition: string;
|
|
8
|
+
term: string;
|
|
9
|
+
}[];
|
|
10
|
+
variables: {
|
|
11
|
+
defaults: {
|
|
12
|
+
default: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}[];
|
|
15
|
+
user: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function loadFixture(dir: string): {
|
|
19
|
+
body: string;
|
|
20
|
+
ctx: RenderContext;
|
|
21
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RenderContext } from './loadFixture';
|
|
2
|
+
export type Engine = 'mdx' | 'mdxish';
|
|
3
|
+
export interface FixtureRenderResult {
|
|
4
|
+
error: string | null;
|
|
5
|
+
html: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Renders a fixture body string through the specified engine and returns the
|
|
9
|
+
* serialized HTML. Renders are deterministic — Date.now and Math.random are
|
|
10
|
+
* frozen for the duration of each render call.
|
|
11
|
+
*
|
|
12
|
+
* For MDXish, the components map is passed to BOTH mdxish() and renderMdxish()
|
|
13
|
+
* — both call sites need it for custom-block resolution.
|
|
14
|
+
*
|
|
15
|
+
* The loader's `ctx.glossary` is mapped onto the engine's `terms` parameter.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderFixture(body: string, ctx: RenderContext, engine: Engine): FixtureRenderResult;
|
package/dist/main.js
CHANGED
|
@@ -76783,6 +76783,7 @@ const readmeComponents = (opts) => () => tree => {
|
|
|
76783
76783
|
|
|
76784
76784
|
|
|
76785
76785
|
|
|
76786
|
+
|
|
76786
76787
|
const imageAttrs = ['align', 'alt', 'caption', 'border', 'height', 'src', 'title', 'width', 'lazy', 'className'];
|
|
76787
76788
|
const readmeToMdx = () => tree => {
|
|
76788
76789
|
// Unwrap pinned nodes, replace rdme-pin with its child node
|
|
@@ -76873,7 +76874,11 @@ const readmeToMdx = () => tree => {
|
|
|
76873
76874
|
}
|
|
76874
76875
|
});
|
|
76875
76876
|
visit(tree, NodeTypes.imageBlock, (image, index, parent) => {
|
|
76876
|
-
|
|
76877
|
+
// Special case for caption: Captions are parsed as children of the image node, so we need to extract them explicitly
|
|
76878
|
+
const captionChildren = Array.isArray(image.children) ? image.children : [];
|
|
76879
|
+
// We want to add it back to the attributes, so re-serialize it to string
|
|
76880
|
+
const caption = captionChildren.length ? toMarkdown({ type: 'root', children: captionChildren }).trim() : '';
|
|
76881
|
+
const attributes = toAttributes({ ...image, ...image.data.hProperties, ...(caption && { caption }) }, imageAttrs);
|
|
76877
76882
|
if (hasExtra(attributes)) {
|
|
76878
76883
|
parent.children.splice(index, 1, {
|
|
76879
76884
|
type: 'mdxJsxFlowElement',
|
|
@@ -98611,6 +98616,11 @@ function smartCamelCase(str) {
|
|
|
98611
98616
|
if (str.includes('-')) {
|
|
98612
98617
|
return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
98613
98618
|
}
|
|
98619
|
+
// Keys that already carry casing (e.g. `cardWidth`) are correct as-is; running
|
|
98620
|
+
// boundary matching over them mangles inner matches (e.g. `id` inside `Width`).
|
|
98621
|
+
if (/[A-Z]/.test(str)) {
|
|
98622
|
+
return str;
|
|
98623
|
+
}
|
|
98614
98624
|
const allBoundaries = [...REACT_HTML_PROP_BOUNDARIES, ...CSS_STYLE_PROP_BOUNDARIES, ...CUSTOM_PROP_BOUNDARIES];
|
|
98615
98625
|
// Return as-is if already a boundary word to avoid incorrect splitting
|
|
98616
98626
|
if (allBoundaries.includes(str.toLowerCase())) {
|
|
@@ -101719,6 +101729,43 @@ const parseSibling = (stack, parent, index, sibling, safeMode) => {
|
|
|
101719
101729
|
stack.push(parent);
|
|
101720
101730
|
}
|
|
101721
101731
|
};
|
|
101732
|
+
/**
|
|
101733
|
+
* Advance a point by the substring of source consumed from it.
|
|
101734
|
+
*/
|
|
101735
|
+
const pointAfter = (start, consumed) => {
|
|
101736
|
+
const newlineIndex = consumed.lastIndexOf('\n');
|
|
101737
|
+
const newlineCount = newlineIndex === -1 ? 0 : consumed.split('\n').length - 1;
|
|
101738
|
+
return {
|
|
101739
|
+
line: start.line + newlineCount,
|
|
101740
|
+
column: newlineCount === 0 ? start.column + consumed.length : consumed.length - newlineIndex,
|
|
101741
|
+
offset: start.offset + consumed.length,
|
|
101742
|
+
};
|
|
101743
|
+
};
|
|
101744
|
+
/**
|
|
101745
|
+
* Build a position ending at `consumedLength` into the html node's value, so the
|
|
101746
|
+
* component doesn't claim trailing content the tokenizer swallowed into one node.
|
|
101747
|
+
*/
|
|
101748
|
+
const positionEndingAtConsumed = (nodePosition, value, consumedLength) => {
|
|
101749
|
+
if (!nodePosition?.start)
|
|
101750
|
+
return nodePosition;
|
|
101751
|
+
return { start: nodePosition.start, end: pointAfter(nodePosition.start, value.slice(0, consumedLength)) };
|
|
101752
|
+
};
|
|
101753
|
+
/**
|
|
101754
|
+
* Build a position ending right after the last occurrence of `closingTag` within
|
|
101755
|
+
* this node's span in the original source. Used in the trailing-content path so
|
|
101756
|
+
* the offset is computed against the real source bytes (including blockquote/list
|
|
101757
|
+
* prefixes that were stripped from the html node's value).
|
|
101758
|
+
*/
|
|
101759
|
+
const positionEndingAtClosingTagInSource = (nodePosition, closingTag, source) => {
|
|
101760
|
+
if (!nodePosition?.start || !nodePosition.end)
|
|
101761
|
+
return nodePosition;
|
|
101762
|
+
const nodeSource = source.slice(nodePosition.start.offset, nodePosition.end.offset);
|
|
101763
|
+
const closingTagOffset = nodeSource.lastIndexOf(closingTag);
|
|
101764
|
+
if (closingTagOffset === -1)
|
|
101765
|
+
return nodePosition;
|
|
101766
|
+
const consumed = nodeSource.slice(0, closingTagOffset + closingTag.length);
|
|
101767
|
+
return { start: nodePosition.start, end: pointAfter(nodePosition.start, consumed) };
|
|
101768
|
+
};
|
|
101722
101769
|
/**
|
|
101723
101770
|
* Create an MdxJsxFlowElement node from component data.
|
|
101724
101771
|
*/
|
|
@@ -101762,9 +101809,10 @@ const substituteNodeWithMdxNode = (parent, index, mdxNode) => {
|
|
|
101762
101809
|
* The opening tag, content, and closing tag are all captured in one HTML node
|
|
101763
101810
|
* (guaranteed by the mdx-component tokenizer).
|
|
101764
101811
|
*/
|
|
101765
|
-
const mdxishMdxComponentBlocks = (opts = {}) => tree => {
|
|
101812
|
+
const mdxishMdxComponentBlocks = (opts = {}) => (tree, file) => {
|
|
101766
101813
|
const stack = [tree];
|
|
101767
101814
|
const safeMode = !!opts.safeMode;
|
|
101815
|
+
const source = file?.value ? String(file.value) : null;
|
|
101768
101816
|
const parseOpts = { preserveExpressionsAsText: safeMode };
|
|
101769
101817
|
const processChildNode = (parent, index) => {
|
|
101770
101818
|
const node = parent.children[index];
|
|
@@ -101778,10 +101826,16 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
|
|
|
101778
101826
|
const value = node.value;
|
|
101779
101827
|
if (node.type !== 'html' || typeof value !== 'string')
|
|
101780
101828
|
return;
|
|
101781
|
-
const
|
|
101829
|
+
const trimmed = value.trim();
|
|
101830
|
+
const parsed = parseTag(trimmed, parseOpts);
|
|
101782
101831
|
if (!parsed)
|
|
101783
101832
|
return;
|
|
101784
101833
|
const { tag, attributes, selfClosing, contentAfterTag = '' } = parsed;
|
|
101834
|
+
// Offset of `trimmed` within the (possibly whitespace-padded) html node value,
|
|
101835
|
+
// so consumed-length math maps back onto the node's real source offsets.
|
|
101836
|
+
const leadingWhitespace = value.length - value.trimStart().length;
|
|
101837
|
+
// Index right after the opening tag's `>` within `trimmed`.
|
|
101838
|
+
const openingTagEnd = trimmed.length - contentAfterTag.length;
|
|
101785
101839
|
// Skip tags that have dedicated transformers
|
|
101786
101840
|
if (GENERIC_MDX_COMPONENT_EXCLUDED_TAGS.has(tag))
|
|
101787
101841
|
return;
|
|
@@ -101806,6 +101860,8 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
|
|
|
101806
101860
|
attributes,
|
|
101807
101861
|
children: [],
|
|
101808
101862
|
startPosition: node.position,
|
|
101863
|
+
// End at the self-closing tag, not at any trailing content.
|
|
101864
|
+
endPosition: positionEndingAtConsumed(node.position, value, leadingWhitespace + openingTagEnd),
|
|
101809
101865
|
});
|
|
101810
101866
|
substituteNodeWithMdxNode(parent, index, componentNode);
|
|
101811
101867
|
// Check and parse if there's relevant content after the current closing tag
|
|
@@ -101837,6 +101893,17 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
|
|
|
101837
101893
|
attributes,
|
|
101838
101894
|
children: parsedChildren,
|
|
101839
101895
|
startPosition: node.position,
|
|
101896
|
+
// When trailing content follows the closing tag, compute the end position precisely
|
|
101897
|
+
// within the html node's value so the component doesn't claim that content.
|
|
101898
|
+
// Prefer source-based positioning when the original source is available: the html
|
|
101899
|
+
// node's value has '> '/space prefixes stripped for blockquotes/list items, so
|
|
101900
|
+
// positionEndingAtConsumed would undercount source offsets. When the entire node
|
|
101901
|
+
// is consumed, use the original node position directly.
|
|
101902
|
+
endPosition: contentAfterClose
|
|
101903
|
+
? source
|
|
101904
|
+
? positionEndingAtClosingTagInSource(node.position, closingTagStr, source)
|
|
101905
|
+
: positionEndingAtConsumed(node.position, value, leadingWhitespace + openingTagEnd + closingTagIndex + closingTagStr.length)
|
|
101906
|
+
: node.position,
|
|
101840
101907
|
});
|
|
101841
101908
|
substituteNodeWithMdxNode(parent, index, componentNode);
|
|
101842
101909
|
// After the closing tag, there might be more content to be processed
|
|
@@ -104107,6 +104174,24 @@ function restoreHTMLElements(content, htmlElements) {
|
|
|
104107
104174
|
return content;
|
|
104108
104175
|
return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
|
|
104109
104176
|
}
|
|
104177
|
+
const ESM_DECLARATION_START = /^(?:export|import)\b/;
|
|
104178
|
+
/**
|
|
104179
|
+
* Whether a line begins a top-level `export`/`import` declaration. Its braces
|
|
104180
|
+
* are valid JS owned by the mdxjsEsm tokenizer (which tolerates blank lines),
|
|
104181
|
+
* so escaping them would corrupt the source and break acorn parsing.
|
|
104182
|
+
*/
|
|
104183
|
+
function startsEsmDeclaration(chars, lineStart) {
|
|
104184
|
+
return ESM_DECLARATION_START.test(chars.slice(lineStart, lineStart + 7).join(''));
|
|
104185
|
+
}
|
|
104186
|
+
function isBlankLine(chars, lineStart) {
|
|
104187
|
+
for (let i = lineStart; i < chars.length; i += 1) {
|
|
104188
|
+
if (chars[i] === '\n')
|
|
104189
|
+
return true;
|
|
104190
|
+
if (chars[i] !== ' ' && chars[i] !== '\t')
|
|
104191
|
+
return false;
|
|
104192
|
+
}
|
|
104193
|
+
return true;
|
|
104194
|
+
}
|
|
104110
104195
|
/**
|
|
104111
104196
|
* Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
|
|
104112
104197
|
*/
|
|
@@ -104122,8 +104207,18 @@ function escapeProblematicBraces(content) {
|
|
|
104122
104207
|
// Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
|
|
104123
104208
|
const chars = Array.from(protectedContent);
|
|
104124
104209
|
const openStack = [];
|
|
104210
|
+
// Whether the current top-level statement is an `export`/`import` declaration.
|
|
104211
|
+
let insideEsmDeclaration = false;
|
|
104125
104212
|
for (let i = 0; i < chars.length; i += 1) {
|
|
104126
104213
|
const ch = chars[i];
|
|
104214
|
+
// At a top-level (brace depth 0) line start, decide whether we're inside an
|
|
104215
|
+
// ESM declaration. The flag persists across the declaration's own lines.
|
|
104216
|
+
if (openStack.length === 0 && (i === 0 || chars[i - 1] === '\n')) {
|
|
104217
|
+
if (startsEsmDeclaration(chars, i))
|
|
104218
|
+
insideEsmDeclaration = true;
|
|
104219
|
+
else if (isBlankLine(chars, i))
|
|
104220
|
+
insideEsmDeclaration = false;
|
|
104221
|
+
}
|
|
104127
104222
|
// Track string delimiters inside expressions to ignore braces within them
|
|
104128
104223
|
if (openStack.length > 0) {
|
|
104129
104224
|
if (strDelim) {
|
|
@@ -104181,19 +104276,22 @@ function escapeProblematicBraces(content) {
|
|
|
104181
104276
|
if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
|
|
104182
104277
|
isAttrExpr = true;
|
|
104183
104278
|
}
|
|
104184
|
-
openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
|
|
104279
|
+
openStack.push({ pos: i, hasBlankLine: false, isAttrExpr, isEsmDeclaration: insideEsmDeclaration });
|
|
104185
104280
|
lastNewlinePos = -2;
|
|
104186
104281
|
}
|
|
104187
104282
|
else if (ch === '}') {
|
|
104188
104283
|
if (openStack.length > 0) {
|
|
104189
104284
|
const entry = openStack.pop();
|
|
104285
|
+
// The declaration's braces are closed; later top-level braces are not ESM.
|
|
104286
|
+
if (openStack.length === 0)
|
|
104287
|
+
insideEsmDeclaration = false;
|
|
104190
104288
|
// Pure `{/* ... */}` comments are handled downstream by the jsxComment
|
|
104191
104289
|
// tokenizer — escaping their braces would prevent it from running.
|
|
104192
104290
|
const isPureJsxComment = chars[entry.pos + 1] === '/' &&
|
|
104193
104291
|
chars[entry.pos + 2] === '*' &&
|
|
104194
104292
|
chars[i - 1] === '/' &&
|
|
104195
104293
|
chars[i - 2] === '*';
|
|
104196
|
-
if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
|
|
104294
|
+
if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr && !entry.isEsmDeclaration) {
|
|
104197
104295
|
toEscape.add(entry.pos);
|
|
104198
104296
|
toEscape.add(i);
|
|
104199
104297
|
}
|
|
@@ -104202,9 +104300,16 @@ function escapeProblematicBraces(content) {
|
|
|
104202
104300
|
toEscape.add(i);
|
|
104203
104301
|
}
|
|
104204
104302
|
}
|
|
104303
|
+
else if (ch === ';' && openStack.length === 0) {
|
|
104304
|
+
// A top-level `;` ends the current statement, ESM or otherwise.
|
|
104305
|
+
insideEsmDeclaration = false;
|
|
104306
|
+
}
|
|
104205
104307
|
}
|
|
104206
104308
|
// Anything still open is unbalanced.
|
|
104207
|
-
openStack.forEach(entry =>
|
|
104309
|
+
openStack.forEach(entry => {
|
|
104310
|
+
if (!entry.isEsmDeclaration)
|
|
104311
|
+
toEscape.add(entry.pos);
|
|
104312
|
+
});
|
|
104208
104313
|
// Reconstruct the content with the escaped braces.
|
|
104209
104314
|
const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
|
|
104210
104315
|
return restoreHTMLElements(escapedContent, htmlElements);
|
|
@@ -105703,11 +105808,18 @@ function exportComponentsForRehype(components) {
|
|
|
105703
105808
|
* semantics). That breaks any component prop that is genuinely an array. With
|
|
105704
105809
|
* `passNode: true` we get the original hast node in `props.node` for components
|
|
105705
105810
|
* and can read the raw properties directly.
|
|
105811
|
+
*
|
|
105812
|
+
* Only arrays win over `rest`, overwriting other shapes would undo correct
|
|
105813
|
+
* React conversions like `style="color:red"` → `{ color: 'red' }`.
|
|
105706
105814
|
*/
|
|
105707
105815
|
function createElementPreservingHastProps(type, props, ...children) {
|
|
105708
105816
|
if (props?.node?.properties) {
|
|
105709
105817
|
const { node, ...rest } = props;
|
|
105710
|
-
const mergedProps = { ...rest
|
|
105818
|
+
const mergedProps = { ...rest };
|
|
105819
|
+
Object.entries(node.properties).forEach(([key, value]) => {
|
|
105820
|
+
if (Array.isArray(value))
|
|
105821
|
+
mergedProps[key] = value;
|
|
105822
|
+
});
|
|
105711
105823
|
// Strip undefined so positional args don't shadow node.properties.children
|
|
105712
105824
|
const definedChildren = children.filter(c => c !== undefined);
|
|
105713
105825
|
return external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement(type, mergedProps, ...definedChildren);
|