basalt-ui 1.10.0 → 1.12.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/AGENTS.md +21 -21
- package/CHANGELOG.md +25 -0
- package/README.md +36 -27
- package/agent/rules/basalt-content.md +16 -3
- package/dist/agent/adapter.d.ts +146 -0
- package/dist/agent/adapter.d.ts.map +1 -0
- package/dist/agent/adapter.js +473 -0
- package/dist/agent/adapter.test.d.ts +2 -0
- package/dist/agent/adapter.test.d.ts.map +1 -0
- package/dist/agent/ai-sdk-transport.d.ts +3 -52
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +108 -24
- package/dist/agent/ai-sdk-transport.test.d.ts +2 -0
- package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
- package/dist/agent/coalesce.d.ts +42 -0
- package/dist/agent/coalesce.d.ts.map +1 -0
- package/dist/agent/coalesce.js +137 -0
- package/dist/agent/coalesce.test.d.ts +2 -0
- package/dist/agent/coalesce.test.d.ts.map +1 -0
- package/dist/agent/foreign.d.ts +97 -0
- package/dist/agent/foreign.d.ts.map +1 -0
- package/dist/agent/foreign.js +18 -0
- package/dist/agent/foreign.test.d.ts +2 -0
- package/dist/agent/foreign.test.d.ts.map +1 -0
- package/dist/agent/history.d.ts +2 -0
- package/dist/agent/history.d.ts.map +1 -1
- package/dist/agent/id.d.ts +65 -0
- package/dist/agent/id.d.ts.map +1 -0
- package/dist/agent/id.js +30 -0
- package/dist/agent/id.test.d.ts +2 -0
- package/dist/agent/id.test.d.ts.map +1 -0
- package/dist/agent/index.d.ts +11 -4
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +18 -3
- package/dist/agent/merge.d.ts +8 -0
- package/dist/agent/merge.d.ts.map +1 -0
- package/dist/agent/merge.js +35 -0
- package/dist/agent/merge.test.d.ts +2 -0
- package/dist/agent/merge.test.d.ts.map +1 -0
- package/dist/agent/part-list.d.ts +49 -34
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +53 -7
- package/dist/agent/part-list.test.d.ts +2 -0
- package/dist/agent/part-list.test.d.ts.map +1 -0
- package/dist/agent/parts.d.ts +135 -20
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +138 -11
- package/dist/agent/parts.test.d.ts +2 -0
- package/dist/agent/parts.test.d.ts.map +1 -0
- package/dist/agent/thread.d.ts +14 -0
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/thread.js +8 -2
- package/dist/agent/thread.test.d.ts +2 -0
- package/dist/agent/thread.test.d.ts.map +1 -0
- package/dist/agent/transport.d.ts +39 -3
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/transport.js +20 -17
- package/dist/agent/transport.test.d.ts +2 -0
- package/dist/agent/transport.test.d.ts.map +1 -0
- package/dist/agent/use-agent-stream.d.ts +2 -1
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +5 -2
- package/dist/agent/use-agent-stream.test.d.ts +2 -0
- package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.d.ts +9 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.js +75 -23
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
- package/dist/agent-chat/composer.d.ts +130 -8
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +189 -23
- package/dist/agent-chat/composer.test.d.ts +2 -0
- package/dist/agent-chat/composer.test.d.ts.map +1 -0
- package/dist/agent-chat/index.d.ts +3 -1
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- package/dist/agent-chat/thread-message.d.ts +24 -7
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +115 -45
- package/dist/agent-chat/thread-message.test.d.ts +2 -0
- package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-workspace.js +1 -1
- package/dist/agent-chat/tool-chip.d.ts +21 -0
- package/dist/agent-chat/tool-chip.d.ts.map +1 -0
- package/dist/agent-chat/tool-chip.js +120 -0
- package/dist/agent-chat/tool-chip.test.d.ts +2 -0
- package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
- package/dist/charts/utils/domain.test.d.ts +2 -0
- package/dist/charts/utils/domain.test.d.ts.map +1 -0
- package/dist/cli/check-theme.test.d.ts +2 -0
- package/dist/cli/check-theme.test.d.ts.map +1 -0
- package/dist/cli/help.test.d.ts +2 -0
- package/dist/cli/help.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/init-sync.test.d.ts +2 -0
- package/dist/cli/init-sync.test.d.ts.map +1 -0
- package/dist/cli/placement-engine.test.d.ts +2 -0
- package/dist/cli/placement-engine.test.d.ts.map +1 -0
- package/dist/cli/tarball-parity.test.d.ts +2 -0
- package/dist/cli/tarball-parity.test.d.ts.map +1 -0
- package/dist/cli/template-format.test.d.ts +2 -0
- package/dist/cli/template-format.test.d.ts.map +1 -0
- package/dist/cli/template-ledger.test.d.ts +2 -0
- package/dist/cli/template-ledger.test.d.ts.map +1 -0
- package/dist/cli/tokens-css.test.d.ts +2 -0
- package/dist/cli/tokens-css.test.d.ts.map +1 -0
- package/dist/content/article-actions.test.d.ts +2 -0
- package/dist/content/article-actions.test.d.ts.map +1 -0
- package/dist/content/article-model.test.d.ts +2 -0
- package/dist/content/article-model.test.d.ts.map +1 -0
- package/dist/content/block-split.test.d.ts +2 -0
- package/dist/content/block-split.test.d.ts.map +1 -0
- package/dist/content/fence-block.d.ts +37 -10
- package/dist/content/fence-block.d.ts.map +1 -1
- package/dist/content/fence-block.js +80 -14
- package/dist/content/heading-components.d.ts.map +1 -1
- package/dist/content/heading-components.js +7 -4
- package/dist/content/heading-components.test.d.ts +2 -0
- package/dist/content/heading-components.test.d.ts.map +1 -0
- package/dist/content/index.d.ts +6 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +5 -1
- package/dist/content/markdown.d.ts +101 -14
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +141 -28
- package/dist/content/markdown.test.d.ts +2 -0
- package/dist/content/markdown.test.d.ts.map +1 -0
- package/dist/content/prose.module.css.test.d.ts +2 -0
- package/dist/content/prose.module.css.test.d.ts.map +1 -0
- package/dist/content/sanitize.d.ts +170 -0
- package/dist/content/sanitize.d.ts.map +1 -0
- package/dist/content/sanitize.js +178 -0
- package/dist/content/sanitize.test.d.ts +2 -0
- package/dist/content/sanitize.test.d.ts.map +1 -0
- package/dist/content/slug.test.d.ts +2 -0
- package/dist/content/slug.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.test.d.ts +2 -0
- package/dist/dashboard/stat-card.test.d.ts.map +1 -0
- package/dist/guard/check-source.test.d.ts +2 -0
- package/dist/guard/check-source.test.d.ts.map +1 -0
- package/dist/guard/guard-hook.test.d.ts +2 -0
- package/dist/guard/guard-hook.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +1 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/provider/index.test.d.ts +2 -0
- package/dist/provider/index.test.d.ts.map +1 -0
- package/dist/provider/lab-theme.test.d.ts +2 -0
- package/dist/provider/lab-theme.test.d.ts.map +1 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
- package/dist/styles.floor.test.d.ts +2 -0
- package/dist/styles.floor.test.d.ts.map +1 -0
- package/dist/surfaces.d.ts +5 -5
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +13 -4
- package/dist/theme/border-coverage.test.d.ts +18 -0
- package/dist/theme/border-coverage.test.d.ts.map +1 -0
- package/dist/theme/contrast.test.d.ts +2 -0
- package/dist/theme/contrast.test.d.ts.map +1 -0
- package/dist/theme/density-option.test.d.ts +2 -0
- package/dist/theme/density-option.test.d.ts.map +1 -0
- package/dist/theme/density-relations.test.d.ts +2 -0
- package/dist/theme/density-relations.test.d.ts.map +1 -0
- package/dist/theme/derive-theme.test.d.ts +2 -0
- package/dist/theme/derive-theme.test.d.ts.map +1 -0
- package/dist/theme/fonts-option.test.d.ts +2 -0
- package/dist/theme/fonts-option.test.d.ts.map +1 -0
- package/dist/theme/radius-option.test.d.ts +2 -0
- package/dist/theme/radius-option.test.d.ts.map +1 -0
- package/dist/theme/radius.test.d.ts +2 -0
- package/dist/theme/radius.test.d.ts.map +1 -0
- package/dist/theme/shadow-surfaces.test.d.ts +26 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
- package/dist/theme/spacing.test.d.ts +2 -0
- package/dist/theme/spacing.test.d.ts.map +1 -0
- package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
- package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-controls.test.d.ts +2 -0
- package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-state.test.d.ts +2 -0
- package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
- package/dist/theme-lab/theme-lab.test.d.ts +2 -0
- package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
- package/dist/tokens/build-fonts-css.test.d.ts +2 -0
- package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
- package/dist/tokens/build-radius-css.test.d.ts +2 -0
- package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
- package/dist/tokens/density.test.d.ts +2 -0
- package/dist/tokens/density.test.d.ts.map +1 -0
- package/dist/tokens/derive-radius.test.d.ts +2 -0
- package/dist/tokens/derive-radius.test.d.ts.map +1 -0
- package/dist/tokens/derive.test.d.ts +2 -0
- package/dist/tokens/derive.test.d.ts.map +1 -0
- package/dist/tokens/palette.test.d.ts +2 -0
- package/dist/tokens/palette.test.d.ts.map +1 -0
- package/dist/vite-color-mix.test.d.ts +2 -0
- package/dist/vite-color-mix.test.d.ts.map +1 -0
- package/dist/vite.test.d.ts +2 -0
- package/dist/vite.test.d.ts.map +1 -0
- package/llms.txt +7 -7
- package/package.json +6 -1
- package/src/agent/adapter.test.ts +1403 -0
- package/src/agent/adapter.ts +887 -0
- package/src/agent/ai-sdk-transport.test.ts +380 -55
- package/src/agent/ai-sdk-transport.ts +206 -53
- package/src/agent/coalesce.test.ts +387 -0
- package/src/agent/coalesce.ts +229 -0
- package/src/agent/foreign.test.ts +51 -0
- package/src/agent/foreign.ts +140 -0
- package/src/agent/history.ts +2 -0
- package/src/agent/id.test.ts +75 -0
- package/src/agent/id.ts +91 -0
- package/src/agent/index.ts +34 -3
- package/src/agent/merge.test.ts +206 -0
- package/src/agent/merge.ts +114 -0
- package/src/agent/part-list.test.tsx +112 -0
- package/src/agent/part-list.tsx +108 -35
- package/src/agent/parts.test.ts +516 -57
- package/src/agent/parts.ts +323 -32
- package/src/agent/thread.test.ts +75 -2
- package/src/agent/thread.ts +21 -1
- package/src/agent/transport.test.ts +70 -0
- package/src/agent/transport.ts +72 -20
- package/src/agent/use-agent-stream.test.tsx +53 -19
- package/src/agent/use-agent-stream.ts +13 -3
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +138 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +44 -11
- package/src/agent/use-agent-thread-runs.test.tsx +418 -22
- package/src/agent/use-agent-thread-runs.ts +170 -26
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +15 -7
- package/src/agent-chat/composer.test.tsx +924 -0
- package/src/agent-chat/composer.tsx +521 -52
- package/src/agent-chat/index.ts +5 -1
- package/src/agent-chat/thread-detail-panel.tsx +3 -1
- package/src/agent-chat/thread-message.test.tsx +351 -0
- package/src/agent-chat/thread-message.tsx +262 -64
- package/src/agent-chat/thread-workspace.tsx +1 -1
- package/src/agent-chat/tool-chip.test.tsx +139 -0
- package/src/agent-chat/tool-chip.tsx +219 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/template-format.test.ts +2 -2
- package/src/content/fence-block.tsx +242 -15
- package/src/content/heading-components.test.tsx +65 -0
- package/src/content/heading-components.tsx +26 -5
- package/src/content/index.ts +19 -3
- package/src/content/markdown.test.tsx +603 -0
- package/src/content/markdown.tsx +355 -35
- package/src/content/sanitize.test.ts +659 -0
- package/src/content/sanitize.ts +510 -0
- package/src/dashboard/stat-card.test.tsx +6 -1
- package/src/guard/check-source.test.ts +8 -4
- package/src/guard/index.ts +1 -3
- package/src/index.ts +15 -1
- package/src/surfaces.ts +13 -4
- package/src/theme/contrast.test.ts +1 -4
- package/src/theme/density-relations.test.ts +4 -2
- package/src/theme/fonts-option.test.ts +7 -1
- package/src/theme/shadow-surfaces.test.ts +12 -6
- package/src/theme/use-basalt-spacing.test.tsx +2 -1
package/dist/content/markdown.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { lazy, memo, Suspense, useMemo } from "react";
|
|
3
|
-
import remend from "remend";
|
|
2
|
+
import { lazy, memo, Suspense, useEffect, useMemo, useState } from "react";
|
|
4
3
|
import { ALERT_CALLOUT_KIND, ALERT_TITLE, detectAlert, stripAlertMarker } from "./callout-alert.js";
|
|
5
4
|
import { FenceBlock } from "./fence-block.js";
|
|
6
5
|
import { createHeadingComponents } from "./heading-components.js";
|
|
7
6
|
import { Callout } from "./callout.js";
|
|
8
7
|
import { Prose } from "./prose.js";
|
|
8
|
+
import { BASALT_SANITIZE_SCHEMA, mergeSanitizeSchema } from "./sanitize.js";
|
|
9
9
|
import { SlugTracker } from "./slug.js";
|
|
10
10
|
import { blockSplit } from "./block-split.js";
|
|
11
11
|
import { createUrlTransform } from "./url-hardening.js";
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import { settledOnly } from "./fence-block.js";
|
|
13
|
+
function LinkRenderer({ href, children, node: _node, ...rest }) {
|
|
14
|
+
if (href === void 0) return /* @__PURE__ */ jsx("span", { ...rest, children });
|
|
14
15
|
const external = href.startsWith("http://") || href.startsWith("https://");
|
|
15
|
-
return /* @__PURE__ */ jsx("a", { href, ...external && { target: "_blank", rel: "noreferrer noopener" }, children });
|
|
16
|
+
return /* @__PURE__ */ jsx("a", { href, ...rest, ...external && { target: "_blank", rel: "noreferrer noopener" }, children });
|
|
16
17
|
}
|
|
17
18
|
function ImageRenderer({ src, alt }) {
|
|
18
19
|
if (typeof src !== "string" || src === "") return /* @__PURE__ */ jsx("span", { "aria-label": alt });
|
|
@@ -23,45 +24,92 @@ function BlockquoteRenderer({ children }) {
|
|
|
23
24
|
if (!alert) return /* @__PURE__ */ jsx("blockquote", { children });
|
|
24
25
|
return /* @__PURE__ */ jsx(Callout, { kind: ALERT_CALLOUT_KIND[alert.kind], title: ALERT_TITLE[alert.kind], children: stripAlertMarker(children, alert.marker) });
|
|
25
26
|
}
|
|
26
|
-
function createPreRenderer(settled) {
|
|
27
|
+
function createPreRenderer(settled, fenceRenderers) {
|
|
27
28
|
return function PreRenderer({ node }) {
|
|
28
|
-
return /* @__PURE__ */ jsx(
|
|
29
|
+
return /* @__PURE__ */ jsx(
|
|
30
|
+
FenceBlock,
|
|
31
|
+
{
|
|
32
|
+
settled,
|
|
33
|
+
...node !== void 0 && { node },
|
|
34
|
+
...fenceRenderers !== void 0 && { renderers: fenceRenderers }
|
|
35
|
+
}
|
|
36
|
+
);
|
|
29
37
|
};
|
|
30
38
|
}
|
|
31
|
-
function buildMarkdownComponents(tracker, settled) {
|
|
39
|
+
function buildMarkdownComponents(tracker, settled, fenceRenderers) {
|
|
32
40
|
return {
|
|
33
41
|
...createHeadingComponents(tracker),
|
|
34
|
-
pre: createPreRenderer(settled),
|
|
42
|
+
pre: createPreRenderer(settled, fenceRenderers),
|
|
35
43
|
blockquote: BlockquoteRenderer,
|
|
36
44
|
a: LinkRenderer,
|
|
37
45
|
img: ImageRenderer
|
|
38
46
|
};
|
|
39
47
|
}
|
|
48
|
+
let sanitizerPromise = null;
|
|
49
|
+
function loadSanitizer() {
|
|
50
|
+
sanitizerPromise ??= import("rehype-sanitize").then((mod) => ({
|
|
51
|
+
plugin: mod.default,
|
|
52
|
+
// No cast: `SanitizeSchemaInput` mirrors hast-util-sanitize's `Schema` INCLUDING its
|
|
53
|
+
// `| null | undefined` field arms, so `defaultSchema` assigns straight across;
|
|
54
|
+
// `mergeSanitizeSchema` normalizes the nulls away and hands back a `SanitizeSchema`.
|
|
55
|
+
baseSchema: mod.defaultSchema
|
|
56
|
+
})).catch(() => {
|
|
57
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
58
|
+
console.warn(
|
|
59
|
+
'[basalt] Markdown: optional peer "rehype-sanitize" is not installed \u2014 the sanitize pass is skipped. react-markdown does not parse raw HTML by default, so this only opens a hole if a rehypePlugin (rehype-raw and friends) puts untrusted nodes into the tree. Install it with: bun add rehype-sanitize'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
});
|
|
64
|
+
return sanitizerPromise;
|
|
65
|
+
}
|
|
66
|
+
function withSingleClobberLayer(schema) {
|
|
67
|
+
return { ...schema, clobberPrefix: "" };
|
|
68
|
+
}
|
|
69
|
+
function composeRehypePlugins({
|
|
70
|
+
sanitizer,
|
|
71
|
+
consumerPlugins,
|
|
72
|
+
sanitizeSchema
|
|
73
|
+
}) {
|
|
74
|
+
const consumer = consumerPlugins ?? [];
|
|
75
|
+
if (sanitizer === null) return consumer;
|
|
76
|
+
const merged = mergeSanitizeSchema(sanitizer.baseSchema, BASALT_SANITIZE_SCHEMA, sanitizeSchema);
|
|
77
|
+
return [...consumer, [sanitizer.plugin, withSingleClobberLayer(merged)]];
|
|
78
|
+
}
|
|
40
79
|
function PlainTextFallback({ text }) {
|
|
41
80
|
return /* @__PURE__ */ jsx("span", { style: { whiteSpace: "pre-wrap" }, children: text });
|
|
42
81
|
}
|
|
43
82
|
const LazyReactMarkdown = lazy(
|
|
44
|
-
() => Promise.all([import("react-markdown"), import("remark-gfm")]).then(([{ default: ReactMarkdown }, { default: remarkGfm }]) => {
|
|
83
|
+
() => Promise.all([import("react-markdown"), import("remark-gfm"), loadSanitizer()]).then(([{ default: ReactMarkdown }, { default: remarkGfm }, sanitizer]) => {
|
|
45
84
|
function Bridge({
|
|
46
85
|
text,
|
|
47
86
|
components,
|
|
48
87
|
urlTransform,
|
|
49
88
|
remarkPlugins,
|
|
50
|
-
rehypePlugins
|
|
89
|
+
rehypePlugins,
|
|
90
|
+
sanitizeSchema
|
|
51
91
|
}) {
|
|
52
92
|
const remarkPluginsList = [
|
|
53
93
|
remarkGfm,
|
|
54
94
|
...remarkPlugins ?? []
|
|
55
95
|
];
|
|
96
|
+
const rehypePluginsList = useMemo(
|
|
97
|
+
() => composeRehypePlugins({
|
|
98
|
+
sanitizer,
|
|
99
|
+
...rehypePlugins !== void 0 && { consumerPlugins: rehypePlugins },
|
|
100
|
+
...sanitizeSchema !== void 0 && { sanitizeSchema }
|
|
101
|
+
}),
|
|
102
|
+
// `sanitizer` is a constant of the enclosing load closure — resolved once, before this
|
|
103
|
+
// component exists, so it is deliberately not a dependency.
|
|
104
|
+
[rehypePlugins, sanitizeSchema]
|
|
105
|
+
);
|
|
56
106
|
return /* @__PURE__ */ jsx(
|
|
57
107
|
ReactMarkdown,
|
|
58
108
|
{
|
|
59
109
|
components,
|
|
60
110
|
urlTransform,
|
|
61
111
|
remarkPlugins: remarkPluginsList,
|
|
62
|
-
|
|
63
|
-
rehypePlugins
|
|
64
|
-
},
|
|
112
|
+
rehypePlugins: rehypePluginsList,
|
|
65
113
|
children: text
|
|
66
114
|
}
|
|
67
115
|
);
|
|
@@ -73,14 +121,16 @@ function BlockRenderer({
|
|
|
73
121
|
text,
|
|
74
122
|
settled,
|
|
75
123
|
components,
|
|
124
|
+
fenceRenderers,
|
|
76
125
|
urlTransform,
|
|
77
126
|
remarkPlugins,
|
|
78
|
-
rehypePlugins
|
|
127
|
+
rehypePlugins,
|
|
128
|
+
sanitizeSchema
|
|
79
129
|
}) {
|
|
80
130
|
const tracker = useMemo(() => new SlugTracker(), [text]);
|
|
81
131
|
const componentsMap = useMemo(
|
|
82
|
-
() => ({ ...buildMarkdownComponents(tracker, settled), ...components }),
|
|
83
|
-
[tracker, settled, components]
|
|
132
|
+
() => ({ ...buildMarkdownComponents(tracker, settled, fenceRenderers), ...components }),
|
|
133
|
+
[tracker, settled, fenceRenderers, components]
|
|
84
134
|
);
|
|
85
135
|
return /* @__PURE__ */ jsx(
|
|
86
136
|
LazyReactMarkdown,
|
|
@@ -89,16 +139,53 @@ function BlockRenderer({
|
|
|
89
139
|
components: componentsMap,
|
|
90
140
|
urlTransform,
|
|
91
141
|
...remarkPlugins !== void 0 && { remarkPlugins },
|
|
92
|
-
...rehypePlugins !== void 0 && { rehypePlugins }
|
|
142
|
+
...rehypePlugins !== void 0 && { rehypePlugins },
|
|
143
|
+
...sanitizeSchema !== void 0 && { sanitizeSchema }
|
|
93
144
|
}
|
|
94
145
|
);
|
|
95
146
|
}
|
|
96
147
|
const MemoizedBlock = memo(BlockRenderer);
|
|
148
|
+
let remendPromise = null;
|
|
149
|
+
let resolvedRemend = null;
|
|
150
|
+
function loadRemend() {
|
|
151
|
+
remendPromise ??= import("remend").then((mod) => {
|
|
152
|
+
resolvedRemend = mod.default;
|
|
153
|
+
return resolvedRemend;
|
|
154
|
+
}).catch(() => null);
|
|
155
|
+
return remendPromise;
|
|
156
|
+
}
|
|
157
|
+
function useRemend() {
|
|
158
|
+
const [repair, setRepair] = useState(() => resolvedRemend);
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (repair !== null) return;
|
|
161
|
+
let cancelled = false;
|
|
162
|
+
loadRemend().then((loaded) => {
|
|
163
|
+
if (!cancelled && loaded !== null) setRepair(() => loaded);
|
|
164
|
+
return void 0;
|
|
165
|
+
});
|
|
166
|
+
return () => {
|
|
167
|
+
cancelled = true;
|
|
168
|
+
};
|
|
169
|
+
}, [repair]);
|
|
170
|
+
return repair;
|
|
171
|
+
}
|
|
97
172
|
function StreamingTailBlock(props) {
|
|
98
|
-
const
|
|
173
|
+
const repair = useRemend();
|
|
174
|
+
const repaired = useMemo(
|
|
175
|
+
() => repair === null ? props.text : repair(props.text),
|
|
176
|
+
[repair, props.text]
|
|
177
|
+
);
|
|
99
178
|
return /* @__PURE__ */ jsx(BlockRenderer, { ...props, text: repaired, settled: false });
|
|
100
179
|
}
|
|
101
|
-
function SettledBody({
|
|
180
|
+
function SettledBody({
|
|
181
|
+
text,
|
|
182
|
+
components,
|
|
183
|
+
fenceRenderers,
|
|
184
|
+
urlTransform,
|
|
185
|
+
remarkPlugins,
|
|
186
|
+
rehypePlugins,
|
|
187
|
+
sanitizeSchema
|
|
188
|
+
}) {
|
|
102
189
|
return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(PlainTextFallback, { text }), children: /* @__PURE__ */ jsx(
|
|
103
190
|
BlockRenderer,
|
|
104
191
|
{
|
|
@@ -106,17 +193,21 @@ function SettledBody({ text, components, urlTransform, remarkPlugins, rehypePlug
|
|
|
106
193
|
settled: true,
|
|
107
194
|
urlTransform,
|
|
108
195
|
...components !== void 0 && { components },
|
|
196
|
+
...fenceRenderers !== void 0 && { fenceRenderers },
|
|
109
197
|
...remarkPlugins !== void 0 && { remarkPlugins },
|
|
110
|
-
...rehypePlugins !== void 0 && { rehypePlugins }
|
|
198
|
+
...rehypePlugins !== void 0 && { rehypePlugins },
|
|
199
|
+
...sanitizeSchema !== void 0 && { sanitizeSchema }
|
|
111
200
|
}
|
|
112
201
|
) });
|
|
113
202
|
}
|
|
114
203
|
function StreamingBody({
|
|
115
204
|
text,
|
|
116
205
|
components,
|
|
206
|
+
fenceRenderers,
|
|
117
207
|
urlTransform,
|
|
118
208
|
remarkPlugins,
|
|
119
|
-
rehypePlugins
|
|
209
|
+
rehypePlugins,
|
|
210
|
+
sanitizeSchema
|
|
120
211
|
}) {
|
|
121
212
|
const blocks = useMemo(() => blockSplit(text), [text]);
|
|
122
213
|
const lastIndex = blocks.length - 1;
|
|
@@ -125,23 +216,41 @@ function StreamingBody({
|
|
|
125
216
|
text: block,
|
|
126
217
|
urlTransform,
|
|
127
218
|
...components !== void 0 && { components },
|
|
219
|
+
...fenceRenderers !== void 0 && { fenceRenderers },
|
|
128
220
|
...remarkPlugins !== void 0 && { remarkPlugins },
|
|
129
|
-
...rehypePlugins !== void 0 && { rehypePlugins }
|
|
221
|
+
...rehypePlugins !== void 0 && { rehypePlugins },
|
|
222
|
+
...sanitizeSchema !== void 0 && { sanitizeSchema }
|
|
130
223
|
};
|
|
131
224
|
return index === lastIndex ? /* @__PURE__ */ jsx(StreamingTailBlock, { ...shared }, index) : /* @__PURE__ */ jsx(MemoizedBlock, { ...shared, settled: true }, index);
|
|
132
225
|
}) });
|
|
133
226
|
}
|
|
134
227
|
const DEFAULT_LINK_PREFIXES = ["https://", "mailto:", "#", "/"];
|
|
135
|
-
const
|
|
136
|
-
const
|
|
228
|
+
const TRUSTED_IMAGE_PREFIXES = ["https://", "/"];
|
|
229
|
+
const UNTRUSTED_IMAGE_PREFIXES = ["/"];
|
|
230
|
+
const IMAGE_PREFIXES_BY_TRUST = {
|
|
231
|
+
trusted: TRUSTED_IMAGE_PREFIXES,
|
|
232
|
+
untrusted: UNTRUSTED_IMAGE_PREFIXES
|
|
233
|
+
};
|
|
234
|
+
function isKnownContentTrust(value) {
|
|
235
|
+
return Object.hasOwn(IMAGE_PREFIXES_BY_TRUST, value);
|
|
236
|
+
}
|
|
237
|
+
function imagePrefixesForTrust(contentTrust) {
|
|
238
|
+
return isKnownContentTrust(contentTrust) ? IMAGE_PREFIXES_BY_TRUST[contentTrust] : UNTRUSTED_IMAGE_PREFIXES;
|
|
239
|
+
}
|
|
137
240
|
function Markdown({
|
|
138
241
|
children,
|
|
139
242
|
density = "article",
|
|
140
243
|
measure,
|
|
141
244
|
streaming = false,
|
|
245
|
+
// `streaming` as the trust DEFAULT is backward compatibility only (pre-1.12 the image policy was
|
|
246
|
+
// read straight off `streaming`, so dropping it would silently widen every existing streaming
|
|
247
|
+
// call site). It is a floor, never the policy — see the prop's JSDoc.
|
|
248
|
+
contentTrust = streaming ? "untrusted" : "trusted",
|
|
142
249
|
components,
|
|
250
|
+
fenceRenderers,
|
|
251
|
+
sanitizeSchema,
|
|
143
252
|
allowedLinkPrefixes = DEFAULT_LINK_PREFIXES,
|
|
144
|
-
allowedImagePrefixes =
|
|
253
|
+
allowedImagePrefixes = imagePrefixesForTrust(contentTrust),
|
|
145
254
|
defaultOrigin,
|
|
146
255
|
remarkPlugins,
|
|
147
256
|
rehypePlugins,
|
|
@@ -161,8 +270,10 @@ function Markdown({
|
|
|
161
270
|
text: children,
|
|
162
271
|
urlTransform,
|
|
163
272
|
...components !== void 0 && { components },
|
|
273
|
+
...fenceRenderers !== void 0 && { fenceRenderers },
|
|
164
274
|
...remarkPlugins !== void 0 && { remarkPlugins },
|
|
165
|
-
...rehypePlugins !== void 0 && { rehypePlugins }
|
|
275
|
+
...rehypePlugins !== void 0 && { rehypePlugins },
|
|
276
|
+
...sanitizeSchema !== void 0 && { sanitizeSchema }
|
|
166
277
|
};
|
|
167
278
|
return /* @__PURE__ */ jsx(
|
|
168
279
|
Prose,
|
|
@@ -176,5 +287,7 @@ function Markdown({
|
|
|
176
287
|
);
|
|
177
288
|
}
|
|
178
289
|
export {
|
|
179
|
-
Markdown
|
|
290
|
+
Markdown,
|
|
291
|
+
composeRehypePlugins,
|
|
292
|
+
settledOnly
|
|
180
293
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.test.d.ts","sourceRoot":"","sources":["../../src/content/markdown.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prose.module.css.test.d.ts","sourceRoot":"","sources":["../../src/content/prose.module.css.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize-schema composition for `Markdown` (docs/AGENT-CHAT-SPEC.md §7). Owns three things: the
|
|
3
|
+
* additions-only extension type a consumer hands to `Markdown`'s `sanitizeSchema` prop, basalt's
|
|
4
|
+
* own additions layer, and the additive deep merge that composes both over `rehype-sanitize`'s
|
|
5
|
+
* `defaultSchema`.
|
|
6
|
+
*
|
|
7
|
+
* WHY A DATA EXTENSION, NOT A `(base) => Schema` CALLBACK: a callback can return `{}` and silently
|
|
8
|
+
* disable sanitization, and a "never replace" comment is not enforcement. `SanitizeSchemaExtension`
|
|
9
|
+
* makes removal UNREPRESENTABLE — there is no code path in `mergeSanitizeSchema` that shrinks the
|
|
10
|
+
* base. That, plus `./markdown` appending its sanitize pass AFTER the consumer's `rehypePlugins`,
|
|
11
|
+
* is what keeps the baseline un-defeatable from outside the package.
|
|
12
|
+
*
|
|
13
|
+
* The `Schema` shape is MIRRORED here rather than imported from `hast-util-sanitize`, so the
|
|
14
|
+
* content layer's types never depend on an optional peer. `rehype-sanitize` is dynamically imported
|
|
15
|
+
* by `./markdown`, which composes the effective schema at load time:
|
|
16
|
+
*
|
|
17
|
+
* mergeSanitizeSchema(defaultSchema, BASALT_SANITIZE_SCHEMA, props.sanitizeSchema)
|
|
18
|
+
*
|
|
19
|
+
* THE TRAP THIS MODULE EXISTS TO AVOID: `hast-util-sanitize` merges a supplied schema with a
|
|
20
|
+
* shallow top-level spread (`{...defaultSchema, ...options}`). Passing an `attributes` object
|
|
21
|
+
* therefore REPLACES every default attribute allowance rather than adding to it — one consumer
|
|
22
|
+
* addition would silently strip `href` from links, `className` from code fences, and the whole
|
|
23
|
+
* `'*'` fallback. Upstream's answer is the third-party `deepmerge` package; basalt owns the small
|
|
24
|
+
* merge below instead of taking the dependency.
|
|
25
|
+
*
|
|
26
|
+
* "ADDITIONS ONLY" IS A STATEMENT ABOUT THE SANITIZER'S BEHAVIOUR, NOT ABOUT THE SCHEMA'S SHAPE.
|
|
27
|
+
* A merge that only ever grows arrays still removes allowances, because of three things
|
|
28
|
+
* `hast-util-sanitize@5` does when it CONSUMES the schema (`lib/index.js`, verified against the
|
|
29
|
+
* installed copy). Each is load-bearing for the code below:
|
|
30
|
+
*
|
|
31
|
+
* 1. `clobberPrefix` is gated on TRUTHINESS — `schema.clobber && schema.clobberPrefix &&
|
|
32
|
+
* schema.clobber.includes(key)`. ANY falsy prefix (`''`, but equally `null`, `0`, `false`,
|
|
33
|
+
* `NaN` — whatever an untyped caller or a cast smuggles through) therefore turns clobber
|
|
34
|
+
* protection OFF entirely and emits `id`/`name`/`ariaLabelledBy`/`ariaDescribedBy` unprefixed,
|
|
35
|
+
* which is the DOM-clobbering hole `user-content-` exists to close. See `adoptClobberPrefix`.
|
|
36
|
+
* 2. The `'*'` fallback is consulted ONLY when the tag-specific lookup yields `null`/`undefined`.
|
|
37
|
+
* For an ARRAY-valued property (`className`, `headers`, …) a matching tag-specific entry
|
|
38
|
+
* returns `[]` — neither — so the fallback is skipped. Introducing a tag-specific entry for a
|
|
39
|
+
* property the base only allowed via `'*'` therefore DELETES that allowance. See
|
|
40
|
+
* `rescueWildcardDefinitions`.
|
|
41
|
+
* 3. `findDefinition` returns the FIRST entry whose name matches, so any later entry for the same
|
|
42
|
+
* property name is dead code (it also treats a `'data*'` entry as the fallback for any
|
|
43
|
+
* `data`-prefixed key). Concatenating a consumer's entry after the base's is a silent no-op.
|
|
44
|
+
* See `unionDefinitions`, which collapses to ONE entry per property name.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* One entry in an `attributes` allow-list. A bare string allows any value for that property; the
|
|
48
|
+
* tuple form allow-lists specific values (a `RegExp` matches a value, e.g. `['className',
|
|
49
|
+
* /^language-./]`). A ONE-element tuple (`['className']`) also allows any value — upstream only
|
|
50
|
+
* treats an entry as an allow-list when `definition.length > 1`.
|
|
51
|
+
*
|
|
52
|
+
* The `null`/`undefined` arms mirror upstream's `PropertyDefinition` exactly (its rest element is
|
|
53
|
+
* `Exclude<Properties[keyof Properties], Array<any>> | RegExp`, which includes both). They are
|
|
54
|
+
* inert: a sanitized value is always a boolean, number or string, so `allowed === value` can never
|
|
55
|
+
* match one.
|
|
56
|
+
*/
|
|
57
|
+
export type SanitizePropertyDefinition = string | readonly [string, ...ReadonlyArray<boolean | number | string | RegExp | null | undefined>];
|
|
58
|
+
/**
|
|
59
|
+
* Structural mirror of `hast-util-sanitize`'s `Schema`, defined here rather than imported so the
|
|
60
|
+
* content layer never depends on an OPTIONAL peer's types. This is the OUTPUT shape: no `null`
|
|
61
|
+
* arms, so nothing downstream has to re-check a field the merge already normalized away.
|
|
62
|
+
*/
|
|
63
|
+
export type SanitizeSchema = {
|
|
64
|
+
readonly allowComments?: boolean;
|
|
65
|
+
readonly allowDoctypes?: boolean;
|
|
66
|
+
readonly ancestors?: Readonly<Record<string, readonly string[]>>;
|
|
67
|
+
readonly attributes?: Readonly<Record<string, readonly SanitizePropertyDefinition[]>>;
|
|
68
|
+
readonly clobber?: readonly string[];
|
|
69
|
+
readonly clobberPrefix?: string;
|
|
70
|
+
readonly protocols?: Readonly<Record<string, readonly string[]>>;
|
|
71
|
+
readonly required?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
72
|
+
readonly strip?: readonly string[];
|
|
73
|
+
readonly tagNames?: readonly string[];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* INPUT shape for `mergeSanitizeSchema`'s base — `SanitizeSchema` with upstream's `| null |
|
|
77
|
+
* undefined` on every field (and on every `protocols` value), so `rehype-sanitize`'s own
|
|
78
|
+
* `defaultSchema` assigns to it directly with NO cast at the call site.
|
|
79
|
+
*
|
|
80
|
+
* It is deliberately separate from `SanitizeSchema` rather than a widening of it: the null arms
|
|
81
|
+
* belong to what callers may HAND IN, not to what the merge HANDS BACK. `normalizeSchemaInput`
|
|
82
|
+
* drops them, so no `null` ever reaches the exported output type.
|
|
83
|
+
*/
|
|
84
|
+
export type SanitizeSchemaInput = {
|
|
85
|
+
readonly allowComments?: boolean | null | undefined;
|
|
86
|
+
readonly allowDoctypes?: boolean | null | undefined;
|
|
87
|
+
readonly ancestors?: Readonly<Record<string, readonly string[]>> | null | undefined;
|
|
88
|
+
readonly attributes?: Readonly<Record<string, readonly SanitizePropertyDefinition[]>> | null | undefined;
|
|
89
|
+
readonly clobber?: readonly string[] | null | undefined;
|
|
90
|
+
readonly clobberPrefix?: string | null | undefined;
|
|
91
|
+
readonly protocols?: Readonly<Record<string, readonly string[] | null | undefined>> | null | undefined;
|
|
92
|
+
readonly required?: Readonly<Record<string, Readonly<Record<string, unknown>>>> | null | undefined;
|
|
93
|
+
readonly strip?: readonly string[] | null | undefined;
|
|
94
|
+
readonly tagNames?: readonly string[] | null | undefined;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* ADDITIONS ONLY — the four fields it is safe to widen from outside the package. Removal is
|
|
98
|
+
* deliberately not expressible: there is no way to shrink `tagNames`, drop an attribute allowance,
|
|
99
|
+
* narrow `protocols`, or clear `strip`/`clobber`/`ancestors`/`required`.
|
|
100
|
+
*
|
|
101
|
+
* `clobberPrefix` is the one replace-valued field. Upstream's actual gate is `schema.clobberPrefix`
|
|
102
|
+
* read for TRUTHINESS (`lib/index.js`), not `=== ''` — so `null`, `0`, `false`, `NaN`, and `''` are
|
|
103
|
+
* ALL disarming values, and only a genuine non-empty string keeps the guard live. TypeScript cannot
|
|
104
|
+
* express "a non-empty string" on a plain data field (and the declared `string` type is a
|
|
105
|
+
* compile-time claim a JS caller or an `as` cast can defeat), so `adoptClobberPrefix` re-checks
|
|
106
|
+
* `typeof` at runtime and treats anything that is not a non-empty string as a disarm attempt —
|
|
107
|
+
* ignored with a dev warning, never applied.
|
|
108
|
+
*/
|
|
109
|
+
export type SanitizeSchemaExtension = {
|
|
110
|
+
readonly tagNames?: readonly string[];
|
|
111
|
+
readonly attributes?: Readonly<Record<string, readonly SanitizePropertyDefinition[]>>;
|
|
112
|
+
readonly protocols?: Readonly<Record<string, readonly string[]>>;
|
|
113
|
+
readonly clobberPrefix?: string;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* basalt's own element allowances, layered over `rehype-sanitize`'s `defaultSchema`.
|
|
117
|
+
*
|
|
118
|
+
* THIS IS THE ADDITIONS LAYER, NOT THE FULL BASELINE. It is deliberately NOT a materialized
|
|
119
|
+
* `defaultSchema + basalt` schema: `defaultSchema` lives in `rehype-sanitize`, an OPTIONAL peer, so
|
|
120
|
+
* materializing it at module scope would require a static import that hard-requires the peer for
|
|
121
|
+
* every consumer of `basalt-ui/content` — and vendoring a copy would silently drift from upstream's
|
|
122
|
+
* GitHub-grade baseline, which for a security default is worse. The effective schema is composed
|
|
123
|
+
* where the peer is dynamically imported (see the module JSDoc).
|
|
124
|
+
*
|
|
125
|
+
* IT IS EMPTY, AND THAT IS THE HONEST ANSWER — everything basalt's markdown pipeline emits into the
|
|
126
|
+
* hast tree is already permitted by `defaultSchema`, verified entry by entry:
|
|
127
|
+
* - fenced code — `pre` > `code` with a `language-*` class: `defaultSchema.attributes.code` is
|
|
128
|
+
* `[['className', /^language-./]]`, which covers `language-mermaid` too. The fence's title comes
|
|
129
|
+
* from the `code` node's `data.meta`, and `data` is preserved by the sanitizer verbatim.
|
|
130
|
+
* - headings — plain `h1`..`h6`; the slug `id` and the hover anchor are added by basalt's React
|
|
131
|
+
* renderers AFTER sanitization, so they never pass through the schema.
|
|
132
|
+
* - GFM alerts — an ordinary `blockquote` whose first paragraph carries a `[!NOTE]`-style text
|
|
133
|
+
* marker; `Callout` is chosen in the renderer, again downstream of the hast tree.
|
|
134
|
+
* - GFM tables / task lists / footnotes / strikethrough — `table`+`align`, `input[type=checkbox]
|
|
135
|
+
* [disabled]`, `.task-list-item`, `section[data-footnotes]`, `del` are all explicit
|
|
136
|
+
* `defaultSchema` allowances.
|
|
137
|
+
* - links/images — `href`/`src` survive the schema and are then narrowed further by
|
|
138
|
+
* `./url-hardening`'s prefix allowlist.
|
|
139
|
+
*
|
|
140
|
+
* So: do not "fill this in". Every entry added here widens the security baseline for every consumer
|
|
141
|
+
* and must be justified by output basalt actually emits. `style`, event handlers, and new protocols
|
|
142
|
+
* are out of bounds; an app-specific need belongs in that app's `sanitizeSchema` prop.
|
|
143
|
+
*/
|
|
144
|
+
export declare const BASALT_SANITIZE_SCHEMA: SanitizeSchemaExtension;
|
|
145
|
+
/**
|
|
146
|
+
* Deep-merges additions into a base schema. Additive ONLY — and "additive" is measured by what the
|
|
147
|
+
* SANITIZER then allows, not by array lengths (see the module JSDoc for the three upstream lookup
|
|
148
|
+
* rules this has to model):
|
|
149
|
+
*
|
|
150
|
+
* - `tagNames` and `protocols` are concatenated per key, de-duplicated.
|
|
151
|
+
* - `attributes` are merged per tag into ONE entry per property name whose allow-list is the union
|
|
152
|
+
* of base and addition — and a tag gaining its first entry for a property keeps whatever `'*'`
|
|
153
|
+
* granted it, so a widening never shadows the wildcard off.
|
|
154
|
+
* - `clobberPrefix` is the single replace-valued field (a later extension wins; an omitted one
|
|
155
|
+
* leaves the previous value intact) — except the empty string, which is ignored.
|
|
156
|
+
*
|
|
157
|
+
* Never removes anything from `base`: keys no extension mentions (`strip`, `clobber`, `ancestors`,
|
|
158
|
+
* `required`, `allowComments`, `allowDoctypes`) pass through untouched.
|
|
159
|
+
*
|
|
160
|
+
* The `base` parameter takes the nullable `SanitizeSchemaInput` so `rehype-sanitize`'s own
|
|
161
|
+
* `defaultSchema` assigns with no cast; the RETURN type stays non-null.
|
|
162
|
+
*
|
|
163
|
+
* Pure and order-stable: no input is mutated, and the output ordering is a deterministic function
|
|
164
|
+
* of the inputs, so snapshots don't churn.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* mergeSanitizeSchema(defaultSchema, BASALT_SANITIZE_SCHEMA, { tagNames: ['hermes-badge'] })
|
|
168
|
+
*/
|
|
169
|
+
export declare function mergeSanitizeSchema(base: SanitizeSchemaInput, ...extensions: readonly (SanitizeSchemaExtension | undefined)[]): SanitizeSchema;
|
|
170
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/content/sanitize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const BASALT_SANITIZE_SCHEMA = {};
|
|
2
|
+
const WILDCARD = "*";
|
|
3
|
+
function isPresent(value) {
|
|
4
|
+
return value !== null && value !== void 0;
|
|
5
|
+
}
|
|
6
|
+
function normalizeSchemaInput(base) {
|
|
7
|
+
return {
|
|
8
|
+
...isPresent(base.allowComments) && { allowComments: base.allowComments },
|
|
9
|
+
...isPresent(base.allowDoctypes) && { allowDoctypes: base.allowDoctypes },
|
|
10
|
+
...isPresent(base.ancestors) && { ancestors: base.ancestors },
|
|
11
|
+
...isPresent(base.attributes) && { attributes: base.attributes },
|
|
12
|
+
...isPresent(base.clobber) && { clobber: base.clobber },
|
|
13
|
+
...isPresent(base.clobberPrefix) && { clobberPrefix: base.clobberPrefix },
|
|
14
|
+
...isPresent(base.protocols) && { protocols: normalizeProtocols(base.protocols) },
|
|
15
|
+
...isPresent(base.required) && { required: base.required },
|
|
16
|
+
...isPresent(base.strip) && { strip: base.strip },
|
|
17
|
+
...isPresent(base.tagNames) && { tagNames: base.tagNames }
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function normalizeProtocols(protocols) {
|
|
21
|
+
const out = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const [key, values] of Object.entries(protocols)) {
|
|
23
|
+
if (isPresent(values)) out.set(key, values);
|
|
24
|
+
}
|
|
25
|
+
return Object.fromEntries(out);
|
|
26
|
+
}
|
|
27
|
+
function definitionName(definition) {
|
|
28
|
+
return typeof definition === "string" ? definition : definition[0];
|
|
29
|
+
}
|
|
30
|
+
function definitionAllowsAnyValue(definition) {
|
|
31
|
+
return typeof definition === "string" || definition.length < 2;
|
|
32
|
+
}
|
|
33
|
+
function definitionValues(definition) {
|
|
34
|
+
return typeof definition === "string" ? [] : definition.slice(1);
|
|
35
|
+
}
|
|
36
|
+
function definitionValueKey(value) {
|
|
37
|
+
if (typeof value === "object" && value !== null) return `expression\0${String(value)}`;
|
|
38
|
+
return `${typeof value}\0${String(value)}`;
|
|
39
|
+
}
|
|
40
|
+
function toDefinition(name, values) {
|
|
41
|
+
return values.length === 0 ? name : [name, ...values];
|
|
42
|
+
}
|
|
43
|
+
function unionDefinitions(definitions) {
|
|
44
|
+
const order = [];
|
|
45
|
+
const byName = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const definition of definitions) {
|
|
47
|
+
const name = definitionName(definition);
|
|
48
|
+
let slot = byName.get(name);
|
|
49
|
+
if (!slot) {
|
|
50
|
+
slot = { allowAny: false, values: [], keys: /* @__PURE__ */ new Set() };
|
|
51
|
+
byName.set(name, slot);
|
|
52
|
+
order.push(name);
|
|
53
|
+
}
|
|
54
|
+
if (definitionAllowsAnyValue(definition)) {
|
|
55
|
+
slot.allowAny = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const value of definitionValues(definition)) {
|
|
59
|
+
const key = definitionValueKey(value);
|
|
60
|
+
if (slot.keys.has(key)) continue;
|
|
61
|
+
slot.keys.add(key);
|
|
62
|
+
slot.values.push(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const name of order) {
|
|
67
|
+
const slot = byName.get(name);
|
|
68
|
+
if (!slot) continue;
|
|
69
|
+
out.push(slot.allowAny ? name : toDefinition(name, slot.values));
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function findFallbackDefinition(definitions, key) {
|
|
74
|
+
let dataDefault;
|
|
75
|
+
for (const definition of definitions ?? []) {
|
|
76
|
+
const name = definitionName(definition);
|
|
77
|
+
if (name === key) return definition;
|
|
78
|
+
if (name === "data*") dataDefault = definition;
|
|
79
|
+
}
|
|
80
|
+
if (key.length > 4 && key.slice(0, 4).toLowerCase() === "data") return dataDefault;
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
function renameDefinition(definition, name) {
|
|
84
|
+
if (definitionName(definition) === name) return definition;
|
|
85
|
+
return definitionAllowsAnyValue(definition) ? name : toDefinition(name, definitionValues(definition));
|
|
86
|
+
}
|
|
87
|
+
function rescueWildcardDefinitions({
|
|
88
|
+
wildcard,
|
|
89
|
+
current,
|
|
90
|
+
additions
|
|
91
|
+
}) {
|
|
92
|
+
if (!wildcard || wildcard.length === 0) return [];
|
|
93
|
+
const shadowed = new Set((current ?? []).map(definitionName));
|
|
94
|
+
const rescued = [];
|
|
95
|
+
for (const definition of additions) {
|
|
96
|
+
const name = definitionName(definition);
|
|
97
|
+
if (shadowed.has(name)) continue;
|
|
98
|
+
shadowed.add(name);
|
|
99
|
+
const fallback = findFallbackDefinition(wildcard, name);
|
|
100
|
+
if (fallback !== void 0) rescued.push(renameDefinition(fallback, name));
|
|
101
|
+
}
|
|
102
|
+
return rescued;
|
|
103
|
+
}
|
|
104
|
+
function concatUniqueStrings(base, additions) {
|
|
105
|
+
const out = base ? [...base] : [];
|
|
106
|
+
const seen = new Set(out);
|
|
107
|
+
for (const value of additions) {
|
|
108
|
+
if (seen.has(value)) continue;
|
|
109
|
+
seen.add(value);
|
|
110
|
+
out.push(value);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function mergeRecordOfLists(base, addition, concat) {
|
|
115
|
+
const merged = new Map(Object.entries(base ?? {}));
|
|
116
|
+
for (const [key, values] of Object.entries(addition)) {
|
|
117
|
+
merged.set(key, concat(merged.get(key), values));
|
|
118
|
+
}
|
|
119
|
+
return Object.fromEntries(merged);
|
|
120
|
+
}
|
|
121
|
+
function mergeAttributes(base, addition) {
|
|
122
|
+
const merged = new Map(Object.entries(base ?? {}));
|
|
123
|
+
const tags = Object.keys(addition);
|
|
124
|
+
const ordered = tags.includes(WILDCARD) ? [WILDCARD, ...tags.filter((tag) => tag !== WILDCARD)] : tags;
|
|
125
|
+
for (const tag of ordered) {
|
|
126
|
+
const additions = addition[tag] ?? [];
|
|
127
|
+
const current = merged.get(tag);
|
|
128
|
+
if (tag === WILDCARD) {
|
|
129
|
+
merged.set(tag, unionDefinitions([...current ?? [], ...additions]));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const rescued = rescueWildcardDefinitions({
|
|
133
|
+
wildcard: merged.get(WILDCARD),
|
|
134
|
+
current,
|
|
135
|
+
additions
|
|
136
|
+
});
|
|
137
|
+
merged.set(tag, unionDefinitions([...current ?? [], ...rescued, ...additions]));
|
|
138
|
+
}
|
|
139
|
+
return Object.fromEntries(merged);
|
|
140
|
+
}
|
|
141
|
+
function adoptClobberPrefix(current, next) {
|
|
142
|
+
if (typeof next === "string" && next !== "") return next;
|
|
143
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
144
|
+
console.warn(
|
|
145
|
+
'[basalt] mergeSanitizeSchema: ignoring an invalid `clobberPrefix`. hast-util-sanitize gates clobber prefixing on truthiness, so a falsy value (or a non-string) disables it entirely and emits id/name unprefixed \u2014 the DOM-clobbering hole "user-content-" exists to close. The previous prefix is kept.'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return current;
|
|
149
|
+
}
|
|
150
|
+
function mergeSanitizeSchema(base, ...extensions) {
|
|
151
|
+
const normalized = normalizeSchemaInput(base);
|
|
152
|
+
let tagNames = normalized.tagNames;
|
|
153
|
+
let attributes = normalized.attributes;
|
|
154
|
+
let protocols = normalized.protocols;
|
|
155
|
+
let clobberPrefix = normalized.clobberPrefix;
|
|
156
|
+
for (const extension of extensions) {
|
|
157
|
+
if (!extension) continue;
|
|
158
|
+
if (extension.tagNames) tagNames = concatUniqueStrings(tagNames, extension.tagNames);
|
|
159
|
+
if (extension.attributes) attributes = mergeAttributes(attributes, extension.attributes);
|
|
160
|
+
if (extension.protocols) {
|
|
161
|
+
protocols = mergeRecordOfLists(protocols, extension.protocols, concatUniqueStrings);
|
|
162
|
+
}
|
|
163
|
+
if (extension.clobberPrefix !== void 0) {
|
|
164
|
+
clobberPrefix = adoptClobberPrefix(clobberPrefix, extension.clobberPrefix);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
...normalized,
|
|
169
|
+
...tagNames !== void 0 && { tagNames },
|
|
170
|
+
...attributes !== void 0 && { attributes },
|
|
171
|
+
...protocols !== void 0 && { protocols },
|
|
172
|
+
...clobberPrefix !== void 0 && { clobberPrefix }
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
BASALT_SANITIZE_SCHEMA,
|
|
177
|
+
mergeSanitizeSchema
|
|
178
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.test.d.ts","sourceRoot":"","sources":["../../src/content/sanitize.test.ts"],"names":[],"mappings":""}
|