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.
- package/README.md +84 -7
- package/dist/block-views.d.ts +8 -11
- package/dist/block-views.js +10 -8
- package/dist/blocks.d.ts +31 -1
- package/dist/blocks.js +66 -7
- package/dist/conformance.d.ts +13 -1
- package/dist/conformance.js +23 -1
- package/dist/corpus.d.ts +65 -0
- package/dist/corpus.js +82 -0
- package/dist/crawlers.d.ts +8 -1
- package/dist/crawlers.js +14 -1
- package/dist/editor.d.ts +5 -0
- package/dist/editor.js +24 -0
- package/dist/freshness.d.ts +14 -0
- package/dist/freshness.js +13 -0
- package/dist/headings.d.ts +7 -0
- package/dist/headings.js +15 -7
- package/dist/http.d.ts +28 -1
- package/dist/http.js +29 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/license.d.ts +9 -0
- package/dist/license.js +8 -0
- package/dist/link-check.d.ts +25 -0
- package/dist/link-check.js +56 -1
- package/dist/maps.d.ts +25 -2
- package/dist/maps.js +91 -5
- package/dist/mcp.d.ts +37 -0
- package/dist/mcp.js +61 -0
- package/dist/metadata.d.ts +69 -0
- package/dist/metadata.js +45 -0
- package/dist/react-server.d.ts +21 -1
- package/dist/react-server.js +43 -13
- package/dist/react.d.ts +12 -2
- package/dist/react.js +31 -0
- package/dist/revisions.d.ts +7 -3
- package/dist/revisions.js +5 -3
- package/dist/sanitize.d.ts +44 -0
- package/dist/sanitize.js +191 -0
- package/dist/search.d.ts +20 -0
- package/dist/search.js +22 -0
- package/dist/text.d.ts +18 -1
- package/dist/text.js +30 -0
- package/dist/tiptap.d.ts +14 -1
- package/dist/tiptap.js +41 -1
- package/dist/validation.d.ts +19 -1
- package/dist/validation.js +82 -28
- package/dist/well-known.d.ts +47 -8
- package/dist/well-known.js +58 -2
- 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
|
+
};
|
package/dist/metadata.js
ADDED
|
@@ -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
|
+
}
|
package/dist/react-server.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/react-server.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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):
|
|
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
|
+
}
|
package/dist/revisions.d.ts
CHANGED
|
@@ -39,8 +39,12 @@ export interface RevisionDiff<L = unknown> {
|
|
|
39
39
|
summary: string;
|
|
40
40
|
}
|
|
41
41
|
export interface DiffOptions<B, L> {
|
|
42
|
-
/**
|
|
43
|
-
|
|
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
|
|
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
|
|
56
|
-
const
|
|
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,
|
|
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;
|