@readme/markdown 14.10.1 → 14.10.3
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/components/Embed/index.tsx +4 -1
- package/components/Embed/normalizeYouTubeUrl.ts +41 -0
- package/dist/components/Embed/normalizeYouTubeUrl.d.ts +1 -0
- package/dist/lib/micromark/mdx-component/syntax.d.ts +7 -6
- 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 +162 -43
- package/dist/main.node.js +162 -43
- package/dist/main.node.js.map +1 -1
- package/dist/processor/transform/mdxish/components/inline-html.d.ts +6 -5
- 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 +126511 -0
- package/dist/render-fixture.node.js.map +1 -0
- package/package.json +30 -2
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
/* eslint-disable react/jsx-props-no-spreading */
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
5
|
+
import { normalizeYouTubeUrlToEmbedUrl } from './normalizeYouTubeUrl';
|
|
6
|
+
|
|
5
7
|
interface FaviconProps {
|
|
6
8
|
alt?: string;
|
|
7
9
|
src: string;
|
|
@@ -72,7 +74,8 @@ const Embed = ({
|
|
|
72
74
|
!html && !explicitOptOut && url && typeOfEmbed && IFRAME_DERIVABLE_TYPES.has(typeOfEmbed);
|
|
73
75
|
|
|
74
76
|
if (iframe || renderTypeAsIframe) {
|
|
75
|
-
|
|
77
|
+
const src = normalizeYouTubeUrlToEmbedUrl(url);
|
|
78
|
+
return <iframe {...spreadAttrs} src={src} style={iframeStyle} title={title} />;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
if (!providerUrl && url)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// YouTube watch/share URLs (youtube.com/watch?v=ID, youtu.be/ID) set X-Frame-Options
|
|
2
|
+
// and refuse to be framed. The player only allows embedding via youtube.com/embed/ID.
|
|
3
|
+
const YOUTUBE_HOSTS = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtu.be']);
|
|
4
|
+
|
|
5
|
+
// /embed/ accepts `start` (integer seconds), not the watch-page `t` value which may carry units (e.g. 596s, 1h2m3s).
|
|
6
|
+
const toStartSeconds = (timestamp: string): string | null => {
|
|
7
|
+
const [, hours, minutes, seconds] = timestamp.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/) ?? [];
|
|
8
|
+
const total = (Number(hours) || 0) * 3600 + (Number(minutes) || 0) * 60 + (Number(seconds) || 0);
|
|
9
|
+
return total > 0 ? String(total) : null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const extractVideoId = (parsed: URL): string | null => {
|
|
13
|
+
if (parsed.hostname.endsWith('youtu.be')) return parsed.pathname.slice(1).split('/')[0] || null;
|
|
14
|
+
if (parsed.pathname.startsWith('/watch')) return parsed.searchParams.get('v');
|
|
15
|
+
if (parsed.pathname.startsWith('/shorts/')) return parsed.pathname.slice('/shorts/'.length).split('/')[0] || null;
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Most youtube links we get from the browser are watch URLs, like https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
20
|
+
// These /watch URLs won't work in an iframe, so we need to convert them to the embed URL if iframe is used
|
|
21
|
+
export const normalizeYouTubeUrlToEmbedUrl = (url: string): string => {
|
|
22
|
+
let parsed: URL;
|
|
23
|
+
try {
|
|
24
|
+
parsed = new URL(url);
|
|
25
|
+
} catch {
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!YOUTUBE_HOSTS.has(parsed.hostname)) return url;
|
|
30
|
+
if (parsed.pathname.startsWith('/embed/')) return url;
|
|
31
|
+
|
|
32
|
+
const videoId = extractVideoId(parsed);
|
|
33
|
+
if (!videoId) return url;
|
|
34
|
+
|
|
35
|
+
const embed = new URL(`https://www.youtube.com/embed/${videoId}`);
|
|
36
|
+
const timestamp = parsed.searchParams.get('start') || parsed.searchParams.get('t');
|
|
37
|
+
const start = timestamp ? toStartSeconds(timestamp) : null;
|
|
38
|
+
if (start) embed.searchParams.set('start', start);
|
|
39
|
+
|
|
40
|
+
return embed.toString();
|
|
41
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const normalizeYouTubeUrlToEmbedUrl: (url: string) => string;
|
|
@@ -13,12 +13,13 @@ declare module 'micromark-util-types' {
|
|
|
13
13
|
* self-closing `<Component />`). Prevents CommonMark from fragmenting them
|
|
14
14
|
* across multiple HTML / paragraph nodes.
|
|
15
15
|
*
|
|
16
|
-
* **Text (inline)** — registers
|
|
17
|
-
* (
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* **Text (inline)** — registers for lowercase tags and inline PascalCase
|
|
17
|
+
* components (Anchor, Glossary) that carry brace attrs (e.g.
|
|
18
|
+
* `Start <a href={url}>here</a> end`, `<Anchor href={url}>x</Anchor>`). Picks
|
|
19
|
+
* them up during inline parsing so they render inline inside their paragraph,
|
|
20
|
+
* then are rewritten to `mdxJsxTextElement` by the `components/inline-html`
|
|
21
|
+
* transformer. All other PascalCase is flow-only; ReadMe's custom components
|
|
22
|
+
* are authored as block-level elements.
|
|
22
23
|
*
|
|
23
24
|
* Excludes tags handled by dedicated tokenizers: Table, HTMLBlock, Glossary,
|
|
24
25
|
* Anchor.
|
|
@@ -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;
|