@readme/markdown 14.10.1 → 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 +29 -3
- package/dist/main.node.js +29 -3
- 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
|
@@ -101750,6 +101750,22 @@ const positionEndingAtConsumed = (nodePosition, value, consumedLength) => {
|
|
|
101750
101750
|
return nodePosition;
|
|
101751
101751
|
return { start: nodePosition.start, end: pointAfter(nodePosition.start, value.slice(0, consumedLength)) };
|
|
101752
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
|
+
};
|
|
101753
101769
|
/**
|
|
101754
101770
|
* Create an MdxJsxFlowElement node from component data.
|
|
101755
101771
|
*/
|
|
@@ -101793,9 +101809,10 @@ const substituteNodeWithMdxNode = (parent, index, mdxNode) => {
|
|
|
101793
101809
|
* The opening tag, content, and closing tag are all captured in one HTML node
|
|
101794
101810
|
* (guaranteed by the mdx-component tokenizer).
|
|
101795
101811
|
*/
|
|
101796
|
-
const mdxishMdxComponentBlocks = (opts = {}) => tree => {
|
|
101812
|
+
const mdxishMdxComponentBlocks = (opts = {}) => (tree, file) => {
|
|
101797
101813
|
const stack = [tree];
|
|
101798
101814
|
const safeMode = !!opts.safeMode;
|
|
101815
|
+
const source = file?.value ? String(file.value) : null;
|
|
101799
101816
|
const parseOpts = { preserveExpressionsAsText: safeMode };
|
|
101800
101817
|
const processChildNode = (parent, index) => {
|
|
101801
101818
|
const node = parent.children[index];
|
|
@@ -101876,8 +101893,17 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
|
|
|
101876
101893
|
attributes,
|
|
101877
101894
|
children: parsedChildren,
|
|
101878
101895
|
startPosition: node.position,
|
|
101879
|
-
//
|
|
101880
|
-
|
|
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,
|
|
101881
101907
|
});
|
|
101882
101908
|
substituteNodeWithMdxNode(parent, index, componentNode);
|
|
101883
101909
|
// After the closing tag, there might be more content to be processed
|
package/dist/main.node.js
CHANGED
|
@@ -121974,6 +121974,22 @@ const positionEndingAtConsumed = (nodePosition, value, consumedLength) => {
|
|
|
121974
121974
|
return nodePosition;
|
|
121975
121975
|
return { start: nodePosition.start, end: pointAfter(nodePosition.start, value.slice(0, consumedLength)) };
|
|
121976
121976
|
};
|
|
121977
|
+
/**
|
|
121978
|
+
* Build a position ending right after the last occurrence of `closingTag` within
|
|
121979
|
+
* this node's span in the original source. Used in the trailing-content path so
|
|
121980
|
+
* the offset is computed against the real source bytes (including blockquote/list
|
|
121981
|
+
* prefixes that were stripped from the html node's value).
|
|
121982
|
+
*/
|
|
121983
|
+
const positionEndingAtClosingTagInSource = (nodePosition, closingTag, source) => {
|
|
121984
|
+
if (!nodePosition?.start || !nodePosition.end)
|
|
121985
|
+
return nodePosition;
|
|
121986
|
+
const nodeSource = source.slice(nodePosition.start.offset, nodePosition.end.offset);
|
|
121987
|
+
const closingTagOffset = nodeSource.lastIndexOf(closingTag);
|
|
121988
|
+
if (closingTagOffset === -1)
|
|
121989
|
+
return nodePosition;
|
|
121990
|
+
const consumed = nodeSource.slice(0, closingTagOffset + closingTag.length);
|
|
121991
|
+
return { start: nodePosition.start, end: pointAfter(nodePosition.start, consumed) };
|
|
121992
|
+
};
|
|
121977
121993
|
/**
|
|
121978
121994
|
* Create an MdxJsxFlowElement node from component data.
|
|
121979
121995
|
*/
|
|
@@ -122017,9 +122033,10 @@ const substituteNodeWithMdxNode = (parent, index, mdxNode) => {
|
|
|
122017
122033
|
* The opening tag, content, and closing tag are all captured in one HTML node
|
|
122018
122034
|
* (guaranteed by the mdx-component tokenizer).
|
|
122019
122035
|
*/
|
|
122020
|
-
const mdxishMdxComponentBlocks = (opts = {}) => tree => {
|
|
122036
|
+
const mdxishMdxComponentBlocks = (opts = {}) => (tree, file) => {
|
|
122021
122037
|
const stack = [tree];
|
|
122022
122038
|
const safeMode = !!opts.safeMode;
|
|
122039
|
+
const source = file?.value ? String(file.value) : null;
|
|
122023
122040
|
const parseOpts = { preserveExpressionsAsText: safeMode };
|
|
122024
122041
|
const processChildNode = (parent, index) => {
|
|
122025
122042
|
const node = parent.children[index];
|
|
@@ -122100,8 +122117,17 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
|
|
|
122100
122117
|
attributes,
|
|
122101
122118
|
children: parsedChildren,
|
|
122102
122119
|
startPosition: node.position,
|
|
122103
|
-
//
|
|
122104
|
-
|
|
122120
|
+
// When trailing content follows the closing tag, compute the end position precisely
|
|
122121
|
+
// within the html node's value so the component doesn't claim that content.
|
|
122122
|
+
// Prefer source-based positioning when the original source is available: the html
|
|
122123
|
+
// node's value has '> '/space prefixes stripped for blockquotes/list items, so
|
|
122124
|
+
// positionEndingAtConsumed would undercount source offsets. When the entire node
|
|
122125
|
+
// is consumed, use the original node position directly.
|
|
122126
|
+
endPosition: contentAfterClose
|
|
122127
|
+
? source
|
|
122128
|
+
? positionEndingAtClosingTagInSource(node.position, closingTagStr, source)
|
|
122129
|
+
: positionEndingAtConsumed(node.position, value, leadingWhitespace + openingTagEnd + closingTagIndex + closingTagStr.length)
|
|
122130
|
+
: node.position,
|
|
122105
122131
|
});
|
|
122106
122132
|
substituteNodeWithMdxNode(parent, index, componentNode);
|
|
122107
122133
|
// After the closing tag, there might be more content to be processed
|