wiki-formant 0.20.0 → 0.22.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.
Files changed (50) hide show
  1. package/README.md +84 -7
  2. package/dist/block-views.d.ts +8 -11
  3. package/dist/block-views.js +10 -8
  4. package/dist/blocks.d.ts +31 -1
  5. package/dist/blocks.js +66 -7
  6. package/dist/conformance.d.ts +13 -1
  7. package/dist/conformance.js +23 -1
  8. package/dist/corpus.d.ts +65 -0
  9. package/dist/corpus.js +82 -0
  10. package/dist/crawlers.d.ts +8 -1
  11. package/dist/crawlers.js +14 -1
  12. package/dist/editor.d.ts +5 -0
  13. package/dist/editor.js +24 -0
  14. package/dist/freshness.d.ts +14 -0
  15. package/dist/freshness.js +13 -0
  16. package/dist/headings.d.ts +7 -0
  17. package/dist/headings.js +15 -7
  18. package/dist/http.d.ts +28 -1
  19. package/dist/http.js +29 -4
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/license.d.ts +9 -0
  23. package/dist/license.js +8 -0
  24. package/dist/link-check.d.ts +25 -0
  25. package/dist/link-check.js +56 -1
  26. package/dist/maps.d.ts +25 -2
  27. package/dist/maps.js +91 -5
  28. package/dist/mcp.d.ts +37 -0
  29. package/dist/mcp.js +61 -0
  30. package/dist/metadata.d.ts +69 -0
  31. package/dist/metadata.js +45 -0
  32. package/dist/react-server.d.ts +21 -1
  33. package/dist/react-server.js +43 -13
  34. package/dist/react.d.ts +12 -2
  35. package/dist/react.js +31 -0
  36. package/dist/revisions.d.ts +7 -3
  37. package/dist/revisions.js +5 -3
  38. package/dist/sanitize.d.ts +44 -0
  39. package/dist/sanitize.js +191 -0
  40. package/dist/search.d.ts +20 -0
  41. package/dist/search.js +22 -0
  42. package/dist/text.d.ts +18 -1
  43. package/dist/text.js +30 -0
  44. package/dist/tiptap.d.ts +14 -1
  45. package/dist/tiptap.js +41 -1
  46. package/dist/validation.d.ts +19 -1
  47. package/dist/validation.js +82 -28
  48. package/dist/well-known.d.ts +47 -8
  49. package/dist/well-known.js +58 -2
  50. package/package.json +24 -2
package/dist/mcp.js CHANGED
@@ -80,6 +80,67 @@ export class McpToolError extends Error {
80
80
  this.name = 'McpToolError';
81
81
  }
82
82
  }
83
+ /**
84
+ * Typed, clamped reads over a tool's raw arguments, recording every value it
85
+ * had to override.
86
+ *
87
+ * Defaults belong in the tool's description, where a model reads them; what a
88
+ * result should echo is only the case where the server overrode what the
89
+ * caller actually asked for, so `adjustments` stays signal. The three servers
90
+ * here had one of these, one inline `clamp`, and one server coercing per tool,
91
+ * where `Math.min(Number(limit) || 12, 50)` let a negative limit through.
92
+ */
93
+ export function readArgs(raw) {
94
+ const adjustments = [];
95
+ // A numeric string is read as its number without comment — the transport
96
+ // accepts them, and parsing is not overriding what the caller asked for.
97
+ const number = (name, def, min, max, whole) => {
98
+ const value = raw[name];
99
+ if (value === undefined || value === null || value === '')
100
+ return def;
101
+ const parsed = Number(value);
102
+ if (!Number.isFinite(parsed)) {
103
+ adjustments.push({ param: name, requested: value, used: def, reason: 'not a number' });
104
+ return def;
105
+ }
106
+ const clamped = Math.min(max, Math.max(min, whole ? Math.trunc(parsed) : parsed));
107
+ if (clamped !== parsed) {
108
+ adjustments.push({
109
+ param: name,
110
+ requested: value,
111
+ used: clamped,
112
+ reason: `must be a ${whole ? 'whole number' : 'number'} between ${min} and ${max}`,
113
+ });
114
+ }
115
+ return clamped;
116
+ };
117
+ return {
118
+ adjustments,
119
+ note(a) {
120
+ adjustments.push(a);
121
+ },
122
+ /** A whole number in `[min, max]`; `def` when absent or not a number at all. */
123
+ num: (name, def, min, max) => number(name, def, min, max, true),
124
+ /** Any number in `[min, max]` — an age that is a cohort mean, a price; `def` (often null) when absent. */
125
+ decimal: (name, def, min, max) => number(name, def, min, max, false),
126
+ /** Trimmed text; `def` when absent. */
127
+ str(name, def = '') {
128
+ const value = raw[name];
129
+ return value === undefined || value === null ? def : String(value).trim();
130
+ },
131
+ /** True only for a JSON `true`. */
132
+ bool(name) {
133
+ return raw[name] === true;
134
+ },
135
+ /** The string members of an array argument; `[]` when absent. */
136
+ list(name) {
137
+ const value = raw[name];
138
+ return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
139
+ },
140
+ };
141
+ }
142
+ /** `result`, plus `adjustments` when `readArgs` had to override anything. */
143
+ export const withAdjustments = (args, result) => args.adjustments.length ? { ...result, adjustments: args.adjustments } : result;
83
144
  /** How many entries one JSON-RPC batch may carry. Quoted by the conformance suite. */
84
145
  export const DEFAULT_MAX_BATCH = 20;
85
146
  const quote = (list) => list.map(s => `"${s}"`).join(', ');
@@ -0,0 +1,69 @@
1
+ export interface ArticleMeta {
2
+ publishedTime?: string;
3
+ modifiedTime?: string;
4
+ section?: string;
5
+ tags?: string[];
6
+ }
7
+ export interface PageMetadataOptions {
8
+ /** The social title. */
9
+ title: string;
10
+ description?: string;
11
+ /** Absolute canonical URL. Omit on a page that should not declare one (a layout default). */
12
+ url?: string;
13
+ type?: 'website' | 'article';
14
+ /** Absolute URL of the card. Omit to let a file-convention `opengraph-image` supply it. */
15
+ image?: string;
16
+ /** Its intrinsic size. Defaults to the 1200×630 of a summary_large_image card. */
17
+ imageSize?: {
18
+ width: number;
19
+ height: number;
20
+ };
21
+ /** Defaults to `title`. */
22
+ imageAlt?: string;
23
+ siteName?: string;
24
+ locale?: string;
25
+ /** `@handle`, as both site and creator. */
26
+ handle?: string;
27
+ /**
28
+ * Advertise the page's markdown twin at `${url}.md`, beside the canonical —
29
+ * the only place it can go, since a separate `alternates` would replace this
30
+ * one. Pages only: a section or index has no twin.
31
+ */
32
+ markdownTwin?: boolean;
33
+ /** Open Graph article fields, for `type: 'article'`. */
34
+ article?: ArticleMeta;
35
+ }
36
+ export declare function pageMetadata(o: PageMetadataOptions): {
37
+ openGraph: {
38
+ publishedTime?: string;
39
+ modifiedTime?: string;
40
+ section?: string;
41
+ tags?: string[];
42
+ images?: {
43
+ alt: string;
44
+ width: number;
45
+ height: number;
46
+ url: string;
47
+ }[] | undefined;
48
+ locale?: string | undefined;
49
+ siteName?: string | undefined;
50
+ url?: string | undefined;
51
+ description?: string | undefined;
52
+ type: "article" | "website";
53
+ title: string;
54
+ };
55
+ twitter: {
56
+ images?: string[] | undefined;
57
+ site?: string | undefined;
58
+ creator?: string | undefined;
59
+ description?: string | undefined;
60
+ card: "summary_large_image";
61
+ title: string;
62
+ };
63
+ alternates?: {
64
+ types?: {
65
+ 'text/markdown': string;
66
+ } | undefined;
67
+ canonical: string;
68
+ } | undefined;
69
+ };
@@ -0,0 +1,45 @@
1
+ // metadata.ts — one page's canonical, markdown twin, Open Graph and Twitter
2
+ // card, from one input.
3
+ //
4
+ // Next *replaces* rather than merges these objects per route segment. It does
5
+ // fill a missing twitter title, description and image from `openGraph`, but
6
+ // only when no `twitter` object was inherited — so under a layout that sets its
7
+ // own card, a page that sets openGraph and forgets twitter shows the layout's
8
+ // generic card, and a page that restates `alternates` for its twin clobbers its
9
+ // canonical. Two
10
+ // wikis wrote a helper around exactly that and covered different halves: one
11
+ // had the twin and no siteName, the other the siteName and no twin. The third
12
+ // wrote each page's metadata by hand.
13
+ //
14
+ // Plain objects in the shape Next's `Metadata` wants, so no `next` import.
15
+ export function pageMetadata(o) {
16
+ const { title, description, url, type = 'website', image, imageSize = { width: 1200, height: 630 }, siteName, locale, handle, markdownTwin, article } = o;
17
+ const images = image ? [{ url: image, ...imageSize, alt: o.imageAlt ?? title }] : undefined;
18
+ return {
19
+ ...(url
20
+ ? {
21
+ alternates: {
22
+ canonical: url,
23
+ ...(markdownTwin ? { types: { 'text/markdown': `${url}.md` } } : {}),
24
+ },
25
+ }
26
+ : {}),
27
+ openGraph: {
28
+ type,
29
+ title,
30
+ ...(description ? { description } : {}),
31
+ ...(url ? { url } : {}),
32
+ ...(siteName ? { siteName } : {}),
33
+ ...(locale ? { locale } : {}),
34
+ ...(images ? { images } : {}),
35
+ ...(type === 'article' ? article : {}),
36
+ },
37
+ twitter: {
38
+ card: 'summary_large_image',
39
+ title,
40
+ ...(description ? { description } : {}),
41
+ ...(handle ? { site: handle, creator: handle } : {}),
42
+ ...(images ? { images: images.map(i => i.url) } : {}),
43
+ },
44
+ };
45
+ }
@@ -37,6 +37,8 @@ export interface FacetBarClassNames {
37
37
  label?: string;
38
38
  control?: string;
39
39
  controlActive?: string;
40
+ /** Set it and each count is its own `<span>` with this class, not ` (n)` text. */
41
+ count?: string;
40
42
  }
41
43
  export interface FacetBarProps {
42
44
  link: WikiLinkComponent;
@@ -44,6 +46,8 @@ export interface FacetBarProps {
44
46
  letters: Control[];
45
47
  /** The label on the A–Z row. */
46
48
  alphaLabel?: string;
49
+ /** Lead with the A–Z row: two lines against a facet block's twenty. */
50
+ alphaFirst?: boolean;
47
51
  classNames?: FacetBarClassNames;
48
52
  }
49
53
  /**
@@ -56,7 +60,23 @@ export interface FacetBarProps {
56
60
  * `aria-current`, not `aria-pressed`: a link is not a toggle button and does not
57
61
  * take that attribute.
58
62
  */
59
- export declare function FacetBar({ link: Link, facets, letters, alphaLabel, classNames, }: FacetBarProps): import("react").JSX.Element | null;
63
+ export declare function FacetBar({ link: Link, facets, letters, alphaLabel, alphaFirst, classNames, }: FacetBarProps): import("react").JSX.Element | null;
64
+ /**
65
+ * A JSON-LD payload, safe to place inside `<script type="application/ld+json">`.
66
+ *
67
+ * The body has to go in through `dangerouslySetInnerHTML` — React escapes a
68
+ * text child's `<`, which breaks the parser Google reads — and these payloads
69
+ * carry authored strings: page titles, display names, excerpts. An authored
70
+ * `</script>` closes the tag and the rest parses as markup. Re-encoding every
71
+ * `<` as its JSON escape closes that; every JSON parser decodes it back, so the
72
+ * data a crawler reads is unchanged. One of the three wikis did this; the
73
+ * other two, and this package's own breadcrumb trail, did not.
74
+ */
75
+ export declare function jsonLdScript(data: unknown): string;
76
+ /** A `<script type="application/ld+json">` for `data`, escaped by `jsonLdScript`. */
77
+ export declare function JsonLd({ data }: {
78
+ data: unknown;
79
+ }): import("react").JSX.Element;
60
80
  export interface BreadcrumbItem {
61
81
  label: string;
62
82
  /**
@@ -36,11 +36,41 @@ export const Anchor = ({ href, className, children }) => _jsx("a", { href: href,
36
36
  * `aria-current`, not `aria-pressed`: a link is not a toggle button and does not
37
37
  * take that attribute.
38
38
  */
39
- export function FacetBar({ link: Link, facets, letters, alphaLabel = 'A–Z', classNames = {}, }) {
39
+ export function FacetBar({ link: Link, facets, letters, alphaLabel = 'A–Z', alphaFirst = false, classNames = {}, }) {
40
40
  if (!facets.length && !letters.length)
41
41
  return null;
42
- const { root = 'stack tight', row = 'cluster', label = 'form-label', control = 'tag', controlActive = 'tag tag-removable', } = classNames;
43
- return (_jsxs("div", { className: root, children: [facets.map(facet => (_jsxs("div", { className: row, children: [_jsx("span", { className: label, children: facet.label }), facet.options.map(option => (_jsxs(Link, { href: option.href, className: option.active ? controlActive : control, "aria-current": option.active ? 'true' : undefined, children: [option.value, " (", option.count, ")"] }, option.value)))] }, facet.key))), letters.length > 0 && (_jsxs("div", { className: row, children: [_jsx("span", { className: label, children: alphaLabel }), letters.map(letter => (_jsxs(Link, { href: letter.href, className: letter.active ? controlActive : control, "aria-current": letter.active ? 'true' : undefined, children: [letter.label, " (", letter.count, ")"] }, letter.value || 'all')))] }))] }));
42
+ const { root = 'stack tight', row = 'cluster', label = 'form-label', control = 'tag', controlActive = 'tag tag-removable', count, } = classNames;
43
+ const chip = (c) => (_jsxs(Link, { href: c.href, className: c.active ? controlActive : control, "aria-current": c.active ? 'true' : undefined, ...(c.title ? { title: c.title } : {}), children: [c.text, count ? _jsx("span", { className: count, children: c.n }) : ` (${c.n})`] }, c.key));
44
+ const facetRows = facets.map(facet => (_jsxs("div", { className: row, children: [_jsx("span", { className: label, children: facet.label }), facet.options.map(o => chip({ key: o.value, href: o.href, active: o.active, text: o.value, n: o.count }))] }, facet.key)));
45
+ // The reset control leads the letters and says what it resets to.
46
+ const alphaRow = letters.length > 0 && (_jsxs("div", { className: row, children: [_jsx("span", { className: label, children: alphaLabel }), letters.map(l => chip({
47
+ key: l.value || 'all',
48
+ href: l.href,
49
+ active: l.active,
50
+ text: l.label,
51
+ n: l.count,
52
+ ...(l.reset ? { title: `All ${l.count} pages` } : {}),
53
+ }))] }, "alpha"));
54
+ return _jsx("div", { className: root, children: alphaFirst ? [alphaRow, ...facetRows] : [...facetRows, alphaRow] });
55
+ }
56
+ // ---- structured data --------------------------------------------------------
57
+ /**
58
+ * A JSON-LD payload, safe to place inside `<script type="application/ld+json">`.
59
+ *
60
+ * The body has to go in through `dangerouslySetInnerHTML` — React escapes a
61
+ * text child's `<`, which breaks the parser Google reads — and these payloads
62
+ * carry authored strings: page titles, display names, excerpts. An authored
63
+ * `</script>` closes the tag and the rest parses as markup. Re-encoding every
64
+ * `<` as its JSON escape closes that; every JSON parser decodes it back, so the
65
+ * data a crawler reads is unchanged. One of the three wikis did this; the
66
+ * other two, and this package's own breadcrumb trail, did not.
67
+ */
68
+ export function jsonLdScript(data) {
69
+ return JSON.stringify(data).replace(/</g, '\\u003c');
70
+ }
71
+ /** A `<script type="application/ld+json">` for `data`, escaped by `jsonLdScript`. */
72
+ export function JsonLd({ data }) {
73
+ return _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: jsonLdScript(data) } });
44
74
  }
45
75
  /**
46
76
  * The trail, plus the `BreadcrumbList` that makes it a rich result.
@@ -70,16 +100,16 @@ export function Breadcrumbs({ items, base, className = '', link: Link = Anchor,
70
100
  const isLast = i === items.length - 1;
71
101
  const Last = lastAs === 'h1' ? 'h1' : 'span';
72
102
  return (_jsxs(Fragment, { children: [i > 0 && _jsx("li", { className: "separator", "aria-hidden": "true", children: "/" }), _jsx("li", { children: isLast ? (_jsx(Last, { className: lastAs === 'h1' ? 'breadcrumb-h1' : 'breadcrumb-current', "aria-current": "page", children: item.label })) : item.href ? (_jsx(Link, { href: item.href, className: "breadcrumb-link", title: item.label, children: item.label })) : (_jsx("span", { className: "breadcrumb-current", children: item.label })) })] }, item.href ?? `${item.label}-${i}`));
73
- }) }) }), structured && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify({
74
- '@context': 'https://schema.org',
75
- '@type': 'BreadcrumbList',
76
- itemListElement: items.map((item, i) => ({
77
- '@type': 'ListItem',
78
- position: i + 1,
79
- name: item.label,
80
- ...(item.href ? { item: absolute(item.href) } : {}),
81
- })),
82
- }) } }))] }));
103
+ }) }) }), structured && (_jsx(JsonLd, { data: {
104
+ '@context': 'https://schema.org',
105
+ '@type': 'BreadcrumbList',
106
+ itemListElement: items.map((item, i) => ({
107
+ '@type': 'ListItem',
108
+ position: i + 1,
109
+ name: item.label,
110
+ ...(item.href ? { item: absolute(item.href) } : {}),
111
+ })),
112
+ } }))] }));
83
113
  }
84
114
  /**
85
115
  * The standard page-top row: the trail, plus optional right-aligned actions.
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ComboboxAria } from './combobox.js';
2
- import { Component, type CSSProperties, type KeyboardEvent, type ReactNode } from 'react';
2
+ import { Component, type CSSProperties, type DependencyList, type RefObject, type KeyboardEvent, type ReactNode } from 'react';
3
3
  export interface SidebarOptions {
4
4
  /**
5
5
  * Where the reader's choice is remembered, in `localStorage`. Give each wiki
@@ -110,7 +110,7 @@ export declare function TableOfContents({ containerSelector, headings: providedH
110
110
  * `onClose` is an effect dependency: wrap it in `useCallback` or hoist it, or
111
111
  * the listener is torn down and rebuilt on every render.
112
112
  */
113
- export declare function useClickOutside<T extends HTMLElement>(onClose: () => void): import("react").RefObject<T | null>;
113
+ export declare function useClickOutside<T extends HTMLElement>(onClose: () => void): RefObject<T | null>;
114
114
  export interface TypeaheadOptions<T> {
115
115
  /**
116
116
  * Runs the search. MUST be referentially stable — a module function, a server
@@ -344,3 +344,13 @@ export declare function RailShell({ children, prefix, label, className }: RailSh
344
344
  */
345
345
  export declare function isRailLinkActive(activePath: string, href: string, tree?: boolean): boolean;
346
346
  export type { WikiLinkComponent, WikiLinkProps } from './react-server.js';
347
+ /**
348
+ * The `wiki-formant/dom` passes over a rendered article: tab groups, copy
349
+ * buttons, sortable tables. All three renderers ran the same three in an
350
+ * effect, and they disagreed on when: two ran it once on mount, so a page
351
+ * swapped in without a remount kept its old tables unsortable. Each pass is
352
+ * idempotent, so running on every `deps` change is safe.
353
+ */
354
+ export declare function useArticlePasses(ref: RefObject<HTMLElement | null>, deps: DependencyList): void;
355
+ /** Tweet placeholders hydrated, and their iframes kept sized to the posted height. */
356
+ export declare function useTweetEmbeds(ref: RefObject<HTMLElement | null>, deps: DependencyList): void;
package/dist/react.js CHANGED
@@ -27,6 +27,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
27
27
  // state on <html> before first paint. The hook keeps that attribute in
28
28
  // sync afterwards, so CSS has one source of truth either side of hydration.
29
29
  import { resolveSidebarOpen, SIDEBAR_ATTRIBUTE } from './sidebar.js';
30
+ import { activateTabGroups, addCopyButtons, hydrateTweetEmbeds, onTweetResize, sizeTweetEmbeds, sortTables } from './dom.js';
30
31
  import { comboboxAria } from './combobox.js';
31
32
  import { Component, createContext, createElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
32
33
  const readStored = (key) => {
@@ -622,3 +623,33 @@ export function RailShell({ children, prefix, label, className }) {
622
623
  export function isRailLinkActive(activePath, href, tree = false) {
623
624
  return tree ? activePath === href || activePath.startsWith(`${href}/`) : activePath === href;
624
625
  }
626
+ // ---- rendered-article passes --------------------------------------------------
627
+ /**
628
+ * The `wiki-formant/dom` passes over a rendered article: tab groups, copy
629
+ * buttons, sortable tables. All three renderers ran the same three in an
630
+ * effect, and they disagreed on when: two ran it once on mount, so a page
631
+ * swapped in without a remount kept its old tables unsortable. Each pass is
632
+ * idempotent, so running on every `deps` change is safe.
633
+ */
634
+ export function useArticlePasses(ref, deps) {
635
+ useEffect(() => {
636
+ const root = ref.current;
637
+ if (!root)
638
+ return;
639
+ activateTabGroups(root);
640
+ addCopyButtons(root);
641
+ sortTables(root);
642
+ // eslint-disable-next-line react-hooks/exhaustive-deps
643
+ }, deps);
644
+ }
645
+ /** Tweet placeholders hydrated, and their iframes kept sized to the posted height. */
646
+ export function useTweetEmbeds(ref, deps) {
647
+ useEffect(() => {
648
+ const root = ref.current;
649
+ if (!root)
650
+ return;
651
+ hydrateTweetEmbeds(root);
652
+ return onTweetResize(height => sizeTweetEmbeds(root, height));
653
+ // eslint-disable-next-line react-hooks/exhaustive-deps
654
+ }, deps);
655
+ }
@@ -39,8 +39,12 @@ export interface RevisionDiff<L = unknown> {
39
39
  summary: string;
40
40
  }
41
41
  export interface DiffOptions<B, L> {
42
- /** Nested groups in document order, or `null` for a leaf. */
43
- containers: (block: B) => BlockGroup<B>[] | null;
42
+ /**
43
+ * Nested groups in document order, or `null` for a leaf. Defaults to
44
+ * `coreBlockGroups` — `columns.i.blocks` and an infobox's `blocks` — which is
45
+ * every container any wiki here stores.
46
+ */
47
+ containers?: (block: B) => BlockGroup<B>[] | null;
44
48
  /**
45
49
  * A richer diff for one leaf, when the consumer has one to give. `from` is
46
50
  * null for an addition, `to` is null for a removal. Return `undefined` to
@@ -83,7 +87,7 @@ export declare function computeRevisionDiff<B extends DiffBlock, L = unknown>(op
83
87
  newMeta?: unknown;
84
88
  /** What to call that value in the summary. Defaults to `banner`. */
85
89
  metaLabel?: string;
86
- containers: DiffOptions<B, L>['containers'];
90
+ containers?: DiffOptions<B, L>['containers'];
87
91
  leafDiff?: DiffOptions<B, L>['leafDiff'];
88
92
  }): RevisionDiff<L>;
89
93
  export {};
package/dist/revisions.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // page's banner, and a `patch` classification for when only that flag moved.
6
6
  // Matching is by id.
7
7
  import { incrementVersion, parseVersion } from './versioning.js';
8
+ import { coreBlockGroups } from './blocks.js';
8
9
  /** Every block in the tree, flattened, each with the path that addresses it. */
9
10
  export function extractBlocks(blocks, containers, basePath = 'root') {
10
11
  const out = [];
@@ -52,8 +53,9 @@ function diffAttributes(oldBlock, newBlock, containers) {
52
53
  */
53
54
  export function diffBlocks(oldBlocks, newBlocks, opts) {
54
55
  const changes = [];
55
- const oldFlat = extractBlocks(oldBlocks, opts.containers);
56
- const newFlat = extractBlocks(newBlocks, opts.containers);
56
+ const containers = opts.containers ?? coreBlockGroups;
57
+ const oldFlat = extractBlocks(oldBlocks, containers);
58
+ const newFlat = extractBlocks(newBlocks, containers);
57
59
  const matchedOld = new Set();
58
60
  const matchedNew = new Set();
59
61
  const oldById = new Map(oldFlat.map(item => [item.block.id, item]));
@@ -73,7 +75,7 @@ export function diffBlocks(oldBlocks, newBlocks, opts) {
73
75
  attributes: { position: { from: oldItem.path, to: newItem.path } },
74
76
  });
75
77
  }
76
- const attributes = diffAttributes(oldItem.block, newItem.block, opts.containers);
78
+ const attributes = diffAttributes(oldItem.block, newItem.block, containers);
77
79
  if (attributes) {
78
80
  changes.push({
79
81
  id,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The embed hosts the editor's iframe, YouTube, tweet and map nodes produce.
3
+ * This is half of a pair: the CSP `frame-src` in each `next.config.ts` must
4
+ * allow the same hosts, or an iframe survives sanitising and is then blocked.
5
+ */
6
+ export declare const DEFAULT_IFRAME_HOSTS: readonly string[];
7
+ export interface HtmlSanitizerOptions {
8
+ /** Hosts an `<iframe src>` may point at. Defaults to `DEFAULT_IFRAME_HOSTS`. */
9
+ iframeHosts?: readonly string[];
10
+ /** Keep the presentational SVG subset. Defaults to true. */
11
+ svg?: boolean;
12
+ /** Tags this wiki's stored HTML needs beyond the prose set. */
13
+ tags?: readonly string[];
14
+ /** Attributes to add, per tag. Merged with the defaults, never replacing them. Not `class`: see `classes`. */
15
+ attributes?: Readonly<Record<string, readonly string[]>>;
16
+ /** Class names to allow, per tag (`'language-*'` globs work). Merged with the editor's own. */
17
+ classes?: Readonly<Record<string, readonly string[]>>;
18
+ /** URL schemes per tag, where one tag needs more than http/https/mailto. `img` already takes data:. */
19
+ schemesByTag?: Readonly<Record<string, readonly string[]>>;
20
+ }
21
+ /**
22
+ * A sanitiser bound to one wiki's allowlist.
23
+ *
24
+ * Derive `tags`, `attributes` and `classes` from the HTML actually stored in
25
+ * your pages and revisions, not from guesswork: that is how the SVG set above
26
+ * got here, and a list written from memory erases content on the first render.
27
+ */
28
+ export declare function createHtmlSanitizer(options?: HtmlSanitizerOptions): (html: string) => string;
29
+ /**
30
+ * The core leaf types' HTML fields, cleaned: `content.text`, `codeTabs` code,
31
+ * `linkGrid` group descriptions and `references` item text. Every other field
32
+ * those blocks carry renders as a React text node and is escaped already.
33
+ *
34
+ * Returns any other block untouched, so it drops straight into a
35
+ * `mapBlockTree` pass — a repo whose own types render HTML cleans those itself.
36
+ *
37
+ * `codeTabs` code is treated as stored HTML, which is what the editor writes.
38
+ * A wiki whose highlighter takes the code as SOURCE and escapes it on render
39
+ * must not pass it through here — every `<T>` in a signature would go — and
40
+ * cleans its other three fields with its own switch.
41
+ */
42
+ export declare function sanitizeCoreLeaf<B extends {
43
+ type: string;
44
+ }>(block: B, clean: (html: string) => string): B;