basalt-ui 1.11.0 → 1.13.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 +32 -0
- package/README.md +63 -51
- package/agent/rules/basalt-agent.md +14 -0
- package/agent/rules/basalt-content.md +16 -3
- package/configs/oxlint-plugin.js +7 -3
- package/configs/oxlint-plugin.test.ts +34 -5
- package/configs/oxlint.json +4 -4
- package/dist/agent/adapter.d.ts +150 -0
- package/dist/agent/adapter.d.ts.map +1 -0
- package/dist/agent/adapter.js +482 -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.map +1 -1
- package/dist/agent/ai-sdk-transport.js +6 -2
- 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 +20 -4
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +76 -8
- package/dist/agent/coalesce.test.d.ts +2 -0
- package/dist/agent/coalesce.test.d.ts.map +1 -0
- package/dist/agent/foreign.test.d.ts +2 -0
- package/dist/agent/foreign.test.d.ts.map +1 -0
- package/dist/agent/id.d.ts +78 -0
- package/dist/agent/id.d.ts.map +1 -1
- 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 +2 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +3 -0
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +19 -7
- 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 +42 -6
- 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 +46 -10
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +16 -6
- 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 +18 -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.test.d.ts +2 -0
- package/dist/agent/transport.test.d.ts.map +1 -0
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +2 -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 +24 -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 +55 -15
- 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 +5 -1
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/message-affordances.d.ts +59 -0
- package/dist/agent-chat/message-affordances.d.ts.map +1 -0
- package/dist/agent-chat/message-affordances.js +7 -0
- package/dist/agent-chat/relative-time.d.ts +27 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -0
- package/dist/agent-chat/relative-time.js +27 -0
- package/dist/agent-chat/relative-time.test.d.ts +2 -0
- package/dist/agent-chat/relative-time.test.d.ts.map +1 -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-feed-row.d.ts +65 -0
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed-row.js +156 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed.d.ts +34 -5
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +52 -19
- package/dist/agent-chat/thread-feed.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-message.d.ts +183 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +329 -37
- 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-outcome-card.d.ts.map +1 -1
- package/dist/agent-chat/thread-outcome-card.js +1 -16
- package/dist/agent-chat/thread-workspace.d.ts +14 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +30 -4
- package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
- package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -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/agent-chat/virtualize.d.ts +123 -0
- package/dist/agent-chat/virtualize.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.js +9 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
- package/dist/agent-chat/virtualize.type-guard.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/data/index.d.ts +5 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual.d.ts +2 -1
- package/dist/data/virtual.d.ts.map +1 -1
- 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/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- 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 +14 -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 +9 -9
- package/package.json +7 -2
- package/src/agent/adapter.test.ts +1640 -0
- package/src/agent/adapter.ts +945 -0
- package/src/agent/ai-sdk-transport.test.ts +117 -26
- package/src/agent/ai-sdk-transport.ts +8 -2
- package/src/agent/coalesce.test.ts +202 -9
- package/src/agent/coalesce.ts +126 -15
- package/src/agent/foreign.test.ts +5 -1
- package/src/agent/id.test.ts +101 -3
- package/src/agent/id.ts +118 -0
- package/src/agent/index.ts +8 -0
- package/src/agent/merge.test.ts +87 -0
- package/src/agent/merge.ts +57 -10
- package/src/agent/part-list.test.tsx +31 -1
- package/src/agent/part-list.tsx +77 -29
- package/src/agent/parts.test.ts +121 -4
- package/src/agent/parts.ts +78 -17
- package/src/agent/thread.test.ts +75 -2
- package/src/agent/thread.ts +25 -1
- package/src/agent/use-agent-stream.test.tsx +31 -5
- package/src/agent/use-agent-stream.ts +6 -3
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +5 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +16 -11
- package/src/agent/use-agent-thread-runs.test.tsx +356 -17
- package/src/agent/use-agent-thread-runs.ts +159 -23
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +93 -7
- package/src/agent-chat/composer.test.tsx +924 -0
- package/src/agent-chat/composer.tsx +521 -52
- package/src/agent-chat/index.ts +13 -1
- package/src/agent-chat/message-affordances.ts +61 -0
- package/src/agent-chat/relative-time.test.ts +95 -0
- package/src/agent-chat/relative-time.ts +55 -0
- package/src/agent-chat/thread-detail-panel.tsx +3 -1
- package/src/agent-chat/thread-feed-row.test.tsx +510 -0
- package/src/agent-chat/thread-feed-row.tsx +274 -0
- package/src/agent-chat/thread-feed.test.tsx +241 -0
- package/src/agent-chat/thread-feed.tsx +119 -18
- package/src/agent-chat/thread-message.test.tsx +1078 -4
- package/src/agent-chat/thread-message.tsx +795 -42
- package/src/agent-chat/thread-outcome-card.tsx +1 -25
- package/src/agent-chat/thread-workspace.test.tsx +169 -0
- package/src/agent-chat/thread-workspace.tsx +70 -6
- package/src/agent-chat/virtualize.ts +132 -0
- package/src/agent-chat/virtualize.type-guard.test.ts +49 -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/data/index.ts +5 -1
- package/src/data/virtual-list.tsx +2 -1
- package/src/data/virtual.ts +2 -1
- package/src/index.ts +18 -1
- package/src/surfaces.ts +13 -3
- 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 +21 -6
- package/src/theme/use-basalt-spacing.test.tsx +2 -1
|
@@ -0,0 +1,510 @@
|
|
|
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
|
+
// ── Schema shape (structural mirror of hast-util-sanitize's `Schema`) ─────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One entry in an `attributes` allow-list. A bare string allows any value for that property; the
|
|
51
|
+
* tuple form allow-lists specific values (a `RegExp` matches a value, e.g. `['className',
|
|
52
|
+
* /^language-./]`). A ONE-element tuple (`['className']`) also allows any value — upstream only
|
|
53
|
+
* treats an entry as an allow-list when `definition.length > 1`.
|
|
54
|
+
*
|
|
55
|
+
* The `null`/`undefined` arms mirror upstream's `PropertyDefinition` exactly (its rest element is
|
|
56
|
+
* `Exclude<Properties[keyof Properties], Array<any>> | RegExp`, which includes both). They are
|
|
57
|
+
* inert: a sanitized value is always a boolean, number or string, so `allowed === value` can never
|
|
58
|
+
* match one.
|
|
59
|
+
*/
|
|
60
|
+
export type SanitizePropertyDefinition =
|
|
61
|
+
| string
|
|
62
|
+
| readonly [string, ...ReadonlyArray<boolean | number | string | RegExp | null | undefined>]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Structural mirror of `hast-util-sanitize`'s `Schema`, defined here rather than imported so the
|
|
66
|
+
* content layer never depends on an OPTIONAL peer's types. This is the OUTPUT shape: no `null`
|
|
67
|
+
* arms, so nothing downstream has to re-check a field the merge already normalized away.
|
|
68
|
+
*/
|
|
69
|
+
export type SanitizeSchema = {
|
|
70
|
+
readonly allowComments?: boolean
|
|
71
|
+
readonly allowDoctypes?: boolean
|
|
72
|
+
readonly ancestors?: Readonly<Record<string, readonly string[]>>
|
|
73
|
+
readonly attributes?: Readonly<Record<string, readonly SanitizePropertyDefinition[]>>
|
|
74
|
+
readonly clobber?: readonly string[]
|
|
75
|
+
readonly clobberPrefix?: string
|
|
76
|
+
readonly protocols?: Readonly<Record<string, readonly string[]>>
|
|
77
|
+
readonly required?: Readonly<Record<string, Readonly<Record<string, unknown>>>>
|
|
78
|
+
readonly strip?: readonly string[]
|
|
79
|
+
readonly tagNames?: readonly string[]
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* INPUT shape for `mergeSanitizeSchema`'s base — `SanitizeSchema` with upstream's `| null |
|
|
83
|
+
* undefined` on every field (and on every `protocols` value), so `rehype-sanitize`'s own
|
|
84
|
+
* `defaultSchema` assigns to it directly with NO cast at the call site.
|
|
85
|
+
*
|
|
86
|
+
* It is deliberately separate from `SanitizeSchema` rather than a widening of it: the null arms
|
|
87
|
+
* belong to what callers may HAND IN, not to what the merge HANDS BACK. `normalizeSchemaInput`
|
|
88
|
+
* drops them, so no `null` ever reaches the exported output type.
|
|
89
|
+
*/
|
|
90
|
+
export type SanitizeSchemaInput = {
|
|
91
|
+
readonly allowComments?: boolean | null | undefined
|
|
92
|
+
readonly allowDoctypes?: boolean | null | undefined
|
|
93
|
+
readonly ancestors?: Readonly<Record<string, readonly string[]>> | null | undefined
|
|
94
|
+
readonly attributes?:
|
|
95
|
+
| Readonly<Record<string, readonly SanitizePropertyDefinition[]>>
|
|
96
|
+
| null
|
|
97
|
+
| undefined
|
|
98
|
+
readonly clobber?: readonly string[] | null | undefined
|
|
99
|
+
readonly clobberPrefix?: string | null | undefined
|
|
100
|
+
readonly protocols?:
|
|
101
|
+
| Readonly<Record<string, readonly string[] | null | undefined>>
|
|
102
|
+
| null
|
|
103
|
+
| undefined
|
|
104
|
+
readonly required?: Readonly<Record<string, Readonly<Record<string, unknown>>>> | null | undefined
|
|
105
|
+
readonly strip?: readonly string[] | null | undefined
|
|
106
|
+
readonly tagNames?: readonly string[] | null | undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ADDITIONS ONLY — the four fields it is safe to widen from outside the package. Removal is
|
|
111
|
+
* deliberately not expressible: there is no way to shrink `tagNames`, drop an attribute allowance,
|
|
112
|
+
* narrow `protocols`, or clear `strip`/`clobber`/`ancestors`/`required`.
|
|
113
|
+
*
|
|
114
|
+
* `clobberPrefix` is the one replace-valued field. Upstream's actual gate is `schema.clobberPrefix`
|
|
115
|
+
* read for TRUTHINESS (`lib/index.js`), not `=== ''` — so `null`, `0`, `false`, `NaN`, and `''` are
|
|
116
|
+
* ALL disarming values, and only a genuine non-empty string keeps the guard live. TypeScript cannot
|
|
117
|
+
* express "a non-empty string" on a plain data field (and the declared `string` type is a
|
|
118
|
+
* compile-time claim a JS caller or an `as` cast can defeat), so `adoptClobberPrefix` re-checks
|
|
119
|
+
* `typeof` at runtime and treats anything that is not a non-empty string as a disarm attempt —
|
|
120
|
+
* ignored with a dev warning, never applied.
|
|
121
|
+
*/
|
|
122
|
+
export type SanitizeSchemaExtension = {
|
|
123
|
+
readonly tagNames?: readonly string[]
|
|
124
|
+
readonly attributes?: Readonly<Record<string, readonly SanitizePropertyDefinition[]>>
|
|
125
|
+
readonly protocols?: Readonly<Record<string, readonly string[]>>
|
|
126
|
+
readonly clobberPrefix?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── basalt's own additions layer ──────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* basalt's own element allowances, layered over `rehype-sanitize`'s `defaultSchema`.
|
|
133
|
+
*
|
|
134
|
+
* THIS IS THE ADDITIONS LAYER, NOT THE FULL BASELINE. It is deliberately NOT a materialized
|
|
135
|
+
* `defaultSchema + basalt` schema: `defaultSchema` lives in `rehype-sanitize`, an OPTIONAL peer, so
|
|
136
|
+
* materializing it at module scope would require a static import that hard-requires the peer for
|
|
137
|
+
* every consumer of `basalt-ui/content` — and vendoring a copy would silently drift from upstream's
|
|
138
|
+
* GitHub-grade baseline, which for a security default is worse. The effective schema is composed
|
|
139
|
+
* where the peer is dynamically imported (see the module JSDoc).
|
|
140
|
+
*
|
|
141
|
+
* IT IS EMPTY, AND THAT IS THE HONEST ANSWER — everything basalt's markdown pipeline emits into the
|
|
142
|
+
* hast tree is already permitted by `defaultSchema`, verified entry by entry:
|
|
143
|
+
* - fenced code — `pre` > `code` with a `language-*` class: `defaultSchema.attributes.code` is
|
|
144
|
+
* `[['className', /^language-./]]`, which covers `language-mermaid` too. The fence's title comes
|
|
145
|
+
* from the `code` node's `data.meta`, and `data` is preserved by the sanitizer verbatim.
|
|
146
|
+
* - headings — plain `h1`..`h6`; the slug `id` and the hover anchor are added by basalt's React
|
|
147
|
+
* renderers AFTER sanitization, so they never pass through the schema.
|
|
148
|
+
* - GFM alerts — an ordinary `blockquote` whose first paragraph carries a `[!NOTE]`-style text
|
|
149
|
+
* marker; `Callout` is chosen in the renderer, again downstream of the hast tree.
|
|
150
|
+
* - GFM tables / task lists / footnotes / strikethrough — `table`+`align`, `input[type=checkbox]
|
|
151
|
+
* [disabled]`, `.task-list-item`, `section[data-footnotes]`, `del` are all explicit
|
|
152
|
+
* `defaultSchema` allowances.
|
|
153
|
+
* - links/images — `href`/`src` survive the schema and are then narrowed further by
|
|
154
|
+
* `./url-hardening`'s prefix allowlist.
|
|
155
|
+
*
|
|
156
|
+
* So: do not "fill this in". Every entry added here widens the security baseline for every consumer
|
|
157
|
+
* and must be justified by output basalt actually emits. `style`, event handlers, and new protocols
|
|
158
|
+
* are out of bounds; an app-specific need belongs in that app's `sanitizeSchema` prop.
|
|
159
|
+
*/
|
|
160
|
+
export const BASALT_SANITIZE_SCHEMA: SanitizeSchemaExtension = {}
|
|
161
|
+
|
|
162
|
+
// ── Input normalization ───────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/** The `attributes` key upstream treats as "applies to every element". */
|
|
165
|
+
const WILDCARD = '*'
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Drops the `null`/`undefined` arms `SanitizeSchemaInput` carries so the merge — and everything it
|
|
169
|
+
* returns — works in the non-null `SanitizeSchema` world. A `null` `protocols` VALUE is dropped
|
|
170
|
+
* with its key: upstream reads a missing key and a `null` one identically ("no protocols defined?
|
|
171
|
+
* then everything is fine"), so this is shape-only.
|
|
172
|
+
*/
|
|
173
|
+
function isPresent<T>(value: T | null | undefined): value is T {
|
|
174
|
+
return value !== null && value !== undefined
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeSchemaInput(base: SanitizeSchemaInput): SanitizeSchema {
|
|
178
|
+
return {
|
|
179
|
+
...(isPresent(base.allowComments) && { allowComments: base.allowComments }),
|
|
180
|
+
...(isPresent(base.allowDoctypes) && { allowDoctypes: base.allowDoctypes }),
|
|
181
|
+
...(isPresent(base.ancestors) && { ancestors: base.ancestors }),
|
|
182
|
+
...(isPresent(base.attributes) && { attributes: base.attributes }),
|
|
183
|
+
...(isPresent(base.clobber) && { clobber: base.clobber }),
|
|
184
|
+
...(isPresent(base.clobberPrefix) && { clobberPrefix: base.clobberPrefix }),
|
|
185
|
+
...(isPresent(base.protocols) && { protocols: normalizeProtocols(base.protocols) }),
|
|
186
|
+
...(isPresent(base.required) && { required: base.required }),
|
|
187
|
+
...(isPresent(base.strip) && { strip: base.strip }),
|
|
188
|
+
...(isPresent(base.tagNames) && { tagNames: base.tagNames }),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeProtocols(
|
|
193
|
+
protocols: Readonly<Record<string, readonly string[] | null | undefined>>,
|
|
194
|
+
): Readonly<Record<string, readonly string[]>> {
|
|
195
|
+
const out = new Map<string, readonly string[]>()
|
|
196
|
+
for (const [key, values] of Object.entries(protocols)) {
|
|
197
|
+
if (isPresent(values)) out.set(key, values)
|
|
198
|
+
}
|
|
199
|
+
return Object.fromEntries(out)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Attribute-definition algebra (mirrors upstream's lookup) ──────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/** The property an entry governs — a bare string IS the name, a tuple carries it at index 0. */
|
|
205
|
+
function definitionName(definition: SanitizePropertyDefinition): string {
|
|
206
|
+
return typeof definition === 'string' ? definition : definition[0]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Upstream only treats an entry as an allow-list when it is a tuple with `length > 1`. */
|
|
210
|
+
function definitionAllowsAnyValue(definition: SanitizePropertyDefinition): boolean {
|
|
211
|
+
return typeof definition === 'string' || definition.length < 2
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function definitionValues(
|
|
215
|
+
definition: SanitizePropertyDefinition,
|
|
216
|
+
): readonly (boolean | number | string | RegExp | null | undefined)[] {
|
|
217
|
+
return typeof definition === 'string' ? [] : definition.slice(1)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* De-duplication key for one allow-list value. Expressions are keyed off `String(expression)`
|
|
222
|
+
* (`/^hljs-/i` → `"/^hljs-/i"`), which encodes both source and flags and — unlike `instanceof` —
|
|
223
|
+
* survives a cross-realm `RegExp`, matching upstream's own duck-typed `'flags' in allowed` test.
|
|
224
|
+
*/
|
|
225
|
+
function definitionValueKey(value: boolean | number | string | RegExp | null | undefined): string {
|
|
226
|
+
if (typeof value === 'object' && value !== null) return `expression${String(value)}`
|
|
227
|
+
return `${typeof value}${String(value)}`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function toDefinition(
|
|
231
|
+
name: string,
|
|
232
|
+
values: readonly (boolean | number | string | RegExp | null | undefined)[],
|
|
233
|
+
): SanitizePropertyDefinition {
|
|
234
|
+
return values.length === 0 ? name : [name, ...values]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Collapses a list of entries to AT MOST ONE ENTRY PER PROPERTY NAME, unioning their allow-lists.
|
|
239
|
+
*
|
|
240
|
+
* This is the merge's core, and it exists because upstream's `findDefinition` returns the FIRST
|
|
241
|
+
* entry matching a property name — so a second entry for that same name is dead code, and merging
|
|
242
|
+
* by concatenation would make every consumer addition to an already-mentioned property a silent
|
|
243
|
+
* no-op. Union semantics, in the only direction an additions-only API may move:
|
|
244
|
+
*
|
|
245
|
+
* - allow-any (a bare string, or a one-element tuple) absorbs everything — union-ing it with a
|
|
246
|
+
* tuple yields allow-any, never the tuple's narrower list. An extension cannot NARROW.
|
|
247
|
+
* - two tuples yield one tuple whose values are the concatenation, de-duplicated by
|
|
248
|
+
* `definitionValueKey` (expressions structurally, primitives by value and type).
|
|
249
|
+
*
|
|
250
|
+
* Order is base-first, by first appearance of each property name, so output is stable.
|
|
251
|
+
*/
|
|
252
|
+
function unionDefinitions(
|
|
253
|
+
definitions: readonly SanitizePropertyDefinition[],
|
|
254
|
+
): readonly SanitizePropertyDefinition[] {
|
|
255
|
+
type Slot = {
|
|
256
|
+
allowAny: boolean
|
|
257
|
+
values: (boolean | number | string | RegExp | null | undefined)[]
|
|
258
|
+
keys: Set<string>
|
|
259
|
+
}
|
|
260
|
+
const order: string[] = []
|
|
261
|
+
const byName = new Map<string, Slot>()
|
|
262
|
+
|
|
263
|
+
for (const definition of definitions) {
|
|
264
|
+
const name = definitionName(definition)
|
|
265
|
+
let slot = byName.get(name)
|
|
266
|
+
if (!slot) {
|
|
267
|
+
slot = { allowAny: false, values: [], keys: new Set<string>() }
|
|
268
|
+
byName.set(name, slot)
|
|
269
|
+
order.push(name)
|
|
270
|
+
}
|
|
271
|
+
if (definitionAllowsAnyValue(definition)) {
|
|
272
|
+
slot.allowAny = true
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
for (const value of definitionValues(definition)) {
|
|
276
|
+
const key = definitionValueKey(value)
|
|
277
|
+
if (slot.keys.has(key)) continue
|
|
278
|
+
slot.keys.add(key)
|
|
279
|
+
slot.values.push(value)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const out: SanitizePropertyDefinition[] = []
|
|
284
|
+
for (const name of order) {
|
|
285
|
+
const slot = byName.get(name)
|
|
286
|
+
if (!slot) continue
|
|
287
|
+
out.push(slot.allowAny ? name : toDefinition(name, slot.values))
|
|
288
|
+
}
|
|
289
|
+
return out
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Upstream's `findDefinition`, mirrored: first exact name match wins, otherwise a `'data*'` entry
|
|
294
|
+
* answers for any `data`-prefixed key. Used to locate what the `'*'` fallback would have granted.
|
|
295
|
+
*/
|
|
296
|
+
function findFallbackDefinition(
|
|
297
|
+
definitions: readonly SanitizePropertyDefinition[] | undefined,
|
|
298
|
+
key: string,
|
|
299
|
+
): SanitizePropertyDefinition | undefined {
|
|
300
|
+
let dataDefault: SanitizePropertyDefinition | undefined
|
|
301
|
+
for (const definition of definitions ?? []) {
|
|
302
|
+
const name = definitionName(definition)
|
|
303
|
+
if (name === key) return definition
|
|
304
|
+
if (name === 'data*') dataDefault = definition
|
|
305
|
+
}
|
|
306
|
+
if (key.length > 4 && key.slice(0, 4).toLowerCase() === 'data') return dataDefault
|
|
307
|
+
return undefined
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Re-points a `'*'` entry (possibly the `'data*'` catch-all) at a concrete property name. */
|
|
311
|
+
function renameDefinition(
|
|
312
|
+
definition: SanitizePropertyDefinition,
|
|
313
|
+
name: string,
|
|
314
|
+
): SanitizePropertyDefinition {
|
|
315
|
+
if (definitionName(definition) === name) return definition
|
|
316
|
+
return definitionAllowsAnyValue(definition)
|
|
317
|
+
? name
|
|
318
|
+
: toDefinition(name, definitionValues(definition))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Materializes the `'*'` allowances an addition would otherwise SHADOW OFF a tag.
|
|
323
|
+
*
|
|
324
|
+
* Upstream consults the `'*'` fallback only when the tag-specific lookup returns `null`/undefined —
|
|
325
|
+
* and for an array-valued property a matching tag-specific entry returns `[]`, which is neither.
|
|
326
|
+
* So the moment an extension names property P on tag T, T stops inheriting `'*'`'s P allowance for
|
|
327
|
+
* array values. Rescuing it here (so the union below carries it) is what keeps a widening from
|
|
328
|
+
* being a removal.
|
|
329
|
+
*
|
|
330
|
+
* Only names the extension INTRODUCES to T are rescued: a name T's own base list already carried
|
|
331
|
+
* was already shadowing `'*'` before the merge, so leaving it alone preserves base behaviour
|
|
332
|
+
* exactly rather than widening it.
|
|
333
|
+
*/
|
|
334
|
+
function rescueWildcardDefinitions({
|
|
335
|
+
wildcard,
|
|
336
|
+
current,
|
|
337
|
+
additions,
|
|
338
|
+
}: {
|
|
339
|
+
readonly wildcard: readonly SanitizePropertyDefinition[] | undefined
|
|
340
|
+
readonly current: readonly SanitizePropertyDefinition[] | undefined
|
|
341
|
+
readonly additions: readonly SanitizePropertyDefinition[]
|
|
342
|
+
}): readonly SanitizePropertyDefinition[] {
|
|
343
|
+
if (!wildcard || wildcard.length === 0) return []
|
|
344
|
+
|
|
345
|
+
const shadowed = new Set((current ?? []).map(definitionName))
|
|
346
|
+
const rescued: SanitizePropertyDefinition[] = []
|
|
347
|
+
for (const definition of additions) {
|
|
348
|
+
const name = definitionName(definition)
|
|
349
|
+
if (shadowed.has(name)) continue
|
|
350
|
+
shadowed.add(name)
|
|
351
|
+
const fallback = findFallbackDefinition(wildcard, name)
|
|
352
|
+
if (fallback !== undefined) rescued.push(renameDefinition(fallback, name))
|
|
353
|
+
}
|
|
354
|
+
return rescued
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── Additive merge ────────────────────────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
/** Base entries first in their original order, then each addition not already present. */
|
|
360
|
+
function concatUniqueStrings(
|
|
361
|
+
base: readonly string[] | undefined,
|
|
362
|
+
additions: readonly string[],
|
|
363
|
+
): readonly string[] {
|
|
364
|
+
const out = base ? [...base] : []
|
|
365
|
+
const seen = new Set(out)
|
|
366
|
+
for (const value of additions) {
|
|
367
|
+
if (seen.has(value)) continue
|
|
368
|
+
seen.add(value)
|
|
369
|
+
out.push(value)
|
|
370
|
+
}
|
|
371
|
+
return out
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Merges a record-of-lists PER KEY: a key in both ends up with the base list AND the addition's
|
|
376
|
+
* list concatenated, never the addition's replacing the base's. Built through a `Map` +
|
|
377
|
+
* `Object.fromEntries` so key order stays base-first and a `__proto__` key in an addition defines
|
|
378
|
+
* an own property rather than reaching the `Object.prototype` setter.
|
|
379
|
+
*/
|
|
380
|
+
function mergeRecordOfLists<T>(
|
|
381
|
+
base: Readonly<Record<string, readonly T[]>> | undefined,
|
|
382
|
+
addition: Readonly<Record<string, readonly T[]>>,
|
|
383
|
+
concat: (base: readonly T[] | undefined, additions: readonly T[]) => readonly T[],
|
|
384
|
+
): Readonly<Record<string, readonly T[]>> {
|
|
385
|
+
const merged = new Map<string, readonly T[]>(Object.entries(base ?? {}))
|
|
386
|
+
for (const [key, values] of Object.entries(addition)) {
|
|
387
|
+
merged.set(key, concat(merged.get(key), values))
|
|
388
|
+
}
|
|
389
|
+
return Object.fromEntries(merged)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Merges `attributes` per tag. Unlike the plain record-of-lists merge this has to model two
|
|
394
|
+
* upstream lookup rules at once — one entry per property name (`unionDefinitions`) and the `'*'`
|
|
395
|
+
* fallback that a tag-specific entry switches off (`rescueWildcardDefinitions`).
|
|
396
|
+
*
|
|
397
|
+
* `'*'` is processed FIRST so a wildcard widened by the same extension object is already visible to
|
|
398
|
+
* the per-tag rescue, making the result independent of the addition's key order.
|
|
399
|
+
*/
|
|
400
|
+
function mergeAttributes(
|
|
401
|
+
base: Readonly<Record<string, readonly SanitizePropertyDefinition[]>> | undefined,
|
|
402
|
+
addition: Readonly<Record<string, readonly SanitizePropertyDefinition[]>>,
|
|
403
|
+
): Readonly<Record<string, readonly SanitizePropertyDefinition[]>> {
|
|
404
|
+
const merged = new Map<string, readonly SanitizePropertyDefinition[]>(Object.entries(base ?? {}))
|
|
405
|
+
const tags = Object.keys(addition)
|
|
406
|
+
const ordered = tags.includes(WILDCARD)
|
|
407
|
+
? [WILDCARD, ...tags.filter((tag) => tag !== WILDCARD)]
|
|
408
|
+
: tags
|
|
409
|
+
|
|
410
|
+
for (const tag of ordered) {
|
|
411
|
+
const additions = addition[tag] ?? []
|
|
412
|
+
const current = merged.get(tag)
|
|
413
|
+
if (tag === WILDCARD) {
|
|
414
|
+
merged.set(tag, unionDefinitions([...(current ?? []), ...additions]))
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
const rescued = rescueWildcardDefinitions({
|
|
418
|
+
wildcard: merged.get(WILDCARD),
|
|
419
|
+
current,
|
|
420
|
+
additions,
|
|
421
|
+
})
|
|
422
|
+
merged.set(tag, unionDefinitions([...(current ?? []), ...rescued, ...additions]))
|
|
423
|
+
}
|
|
424
|
+
return Object.fromEntries(merged)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Applies an extension's `clobberPrefix`, IGNORING anything that is not a non-empty string.
|
|
429
|
+
*
|
|
430
|
+
* Upstream gates prefixing on `schema.clobber && schema.clobberPrefix && …` — a TRUTHINESS check,
|
|
431
|
+
* not `=== ''`. So `''` is not the only disarming value: `null`, `0`, `false`, and `NaN` all read as
|
|
432
|
+
* falsy too and switch clobber protection off entirely, letting `id`/`name` back out unprefixed.
|
|
433
|
+
* That is the DOM-clobbering vector (`<a id="body">`) the default prefix exists to close, and none
|
|
434
|
+
* of its spellings may be reachable through an additions-only API.
|
|
435
|
+
*
|
|
436
|
+
* `next` is typed `unknown` rather than trusting `SanitizeSchemaExtension`'s declared `string` —
|
|
437
|
+
* the type is a compile-time claim, and this is a runtime security boundary: a plain-JS caller (no
|
|
438
|
+
* type checker in the loop) or an `as` cast can hand this function `null`/`0`/`false`/`NaN`/an
|
|
439
|
+
* object despite the type, so the enforcement has to `typeof`-check rather than assume. Only a
|
|
440
|
+
* genuine non-empty string is adopted; everything else — including non-string truthy values, which
|
|
441
|
+
* upstream would coerce/misuse rather than treat as a prefix — is refused with the existing
|
|
442
|
+
* dev-warn and the previous prefix is kept.
|
|
443
|
+
*/
|
|
444
|
+
function adoptClobberPrefix(current: string | undefined, next: unknown): string | undefined {
|
|
445
|
+
if (typeof next === 'string' && next !== '') return next
|
|
446
|
+
if (process.env['NODE_ENV'] !== 'production') {
|
|
447
|
+
console.warn(
|
|
448
|
+
'[basalt] mergeSanitizeSchema: ignoring an invalid `clobberPrefix`. hast-util-sanitize gates ' +
|
|
449
|
+
'clobber prefixing on truthiness, so a falsy value (or a non-string) disables it entirely ' +
|
|
450
|
+
'and emits id/name unprefixed — the DOM-clobbering hole "user-content-" exists to close. ' +
|
|
451
|
+
'The previous prefix is kept.',
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
return current
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Deep-merges additions into a base schema. Additive ONLY — and "additive" is measured by what the
|
|
459
|
+
* SANITIZER then allows, not by array lengths (see the module JSDoc for the three upstream lookup
|
|
460
|
+
* rules this has to model):
|
|
461
|
+
*
|
|
462
|
+
* - `tagNames` and `protocols` are concatenated per key, de-duplicated.
|
|
463
|
+
* - `attributes` are merged per tag into ONE entry per property name whose allow-list is the union
|
|
464
|
+
* of base and addition — and a tag gaining its first entry for a property keeps whatever `'*'`
|
|
465
|
+
* granted it, so a widening never shadows the wildcard off.
|
|
466
|
+
* - `clobberPrefix` is the single replace-valued field (a later extension wins; an omitted one
|
|
467
|
+
* leaves the previous value intact) — except the empty string, which is ignored.
|
|
468
|
+
*
|
|
469
|
+
* Never removes anything from `base`: keys no extension mentions (`strip`, `clobber`, `ancestors`,
|
|
470
|
+
* `required`, `allowComments`, `allowDoctypes`) pass through untouched.
|
|
471
|
+
*
|
|
472
|
+
* The `base` parameter takes the nullable `SanitizeSchemaInput` so `rehype-sanitize`'s own
|
|
473
|
+
* `defaultSchema` assigns with no cast; the RETURN type stays non-null.
|
|
474
|
+
*
|
|
475
|
+
* Pure and order-stable: no input is mutated, and the output ordering is a deterministic function
|
|
476
|
+
* of the inputs, so snapshots don't churn.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* mergeSanitizeSchema(defaultSchema, BASALT_SANITIZE_SCHEMA, { tagNames: ['hermes-badge'] })
|
|
480
|
+
*/
|
|
481
|
+
export function mergeSanitizeSchema(
|
|
482
|
+
base: SanitizeSchemaInput,
|
|
483
|
+
...extensions: readonly (SanitizeSchemaExtension | undefined)[]
|
|
484
|
+
): SanitizeSchema {
|
|
485
|
+
const normalized = normalizeSchemaInput(base)
|
|
486
|
+
let tagNames = normalized.tagNames
|
|
487
|
+
let attributes = normalized.attributes
|
|
488
|
+
let protocols = normalized.protocols
|
|
489
|
+
let clobberPrefix = normalized.clobberPrefix
|
|
490
|
+
|
|
491
|
+
for (const extension of extensions) {
|
|
492
|
+
if (!extension) continue
|
|
493
|
+
if (extension.tagNames) tagNames = concatUniqueStrings(tagNames, extension.tagNames)
|
|
494
|
+
if (extension.attributes) attributes = mergeAttributes(attributes, extension.attributes)
|
|
495
|
+
if (extension.protocols) {
|
|
496
|
+
protocols = mergeRecordOfLists(protocols, extension.protocols, concatUniqueStrings)
|
|
497
|
+
}
|
|
498
|
+
if (extension.clobberPrefix !== undefined) {
|
|
499
|
+
clobberPrefix = adoptClobberPrefix(clobberPrefix, extension.clobberPrefix)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
...normalized,
|
|
505
|
+
...(tagNames !== undefined && { tagNames }),
|
|
506
|
+
...(attributes !== undefined && { attributes }),
|
|
507
|
+
...(protocols !== undefined && { protocols }),
|
|
508
|
+
...(clobberPrefix !== undefined && { clobberPrefix }),
|
|
509
|
+
}
|
|
510
|
+
}
|
|
@@ -28,7 +28,12 @@ import type { StatCardTone } from './stat-card'
|
|
|
28
28
|
function render(tone?: StatCardTone) {
|
|
29
29
|
return renderToStaticMarkup(
|
|
30
30
|
<MantineProvider>
|
|
31
|
-
|
|
31
|
+
{/* exactOptionalPropertyTypes forbids an explicit tone={undefined} — spread only when set */}
|
|
32
|
+
<StatCard
|
|
33
|
+
label="Downtime · last 24h"
|
|
34
|
+
value="0 min"
|
|
35
|
+
{...(tone !== undefined ? { tone } : {})}
|
|
36
|
+
/>
|
|
32
37
|
</MantineProvider>,
|
|
33
38
|
)
|
|
34
39
|
}
|
package/src/data/index.ts
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Optional peers:
|
|
9
9
|
* - @tanstack/react-table >=8 <9 (BasaltDataTable)
|
|
10
|
-
* - @tanstack/react-virtual >=3 <4 (BasaltVirtualList)
|
|
10
|
+
* - @tanstack/react-virtual >=3.13.26 <4 (BasaltVirtualList)
|
|
11
|
+
*
|
|
12
|
+
* BasaltVirtualList itself works on any 3.x; the declared floor is set by ./agent-chat's
|
|
13
|
+
* ThreadTranscript virtualize mode (it calls scrollToEnd/anchorTo, added in virtual-core 3.16.0,
|
|
14
|
+
* first pinned by react-virtual 3.13.26). npm has one peer range per package, so do not lower it.
|
|
11
15
|
*
|
|
12
16
|
* Install with:
|
|
13
17
|
* bun add @tanstack/react-table @tanstack/react-virtual
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ./data — BasaltVirtualList: a windowed virtual list over @tanstack/react-virtual,
|
|
3
3
|
* rendered with a Mantine Box scroll container.
|
|
4
|
-
* Optional peer: @tanstack/react-virtual >=3 <4.
|
|
4
|
+
* Optional peer: @tanstack/react-virtual >=3.13.26 <4. This list works on any 3.x — the floor is
|
|
5
|
+
* set by ./agent-chat's virtualize mode (see ./data/index.ts), so do not lower it.
|
|
5
6
|
*
|
|
6
7
|
* @example
|
|
7
8
|
* import { BasaltVirtualList } from 'basalt-ui/data'
|
package/src/data/virtual.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* ./data/virtual — BasaltVirtualList: a windowed virtual list over @tanstack/react-virtual,
|
|
3
3
|
* rendered with a Mantine Box scroll container.
|
|
4
4
|
*
|
|
5
|
-
* Optional peer: @tanstack/react-virtual >=3 <4.
|
|
5
|
+
* Optional peer: @tanstack/react-virtual >=3.13.26 <4. This list works on any 3.x — the floor is
|
|
6
|
+
* set by ./agent-chat's virtualize mode (see ./data/index.ts), so do not lower it.
|
|
6
7
|
*
|
|
7
8
|
* Use this fine subpath instead of the `./data` barrel when your app only needs the virtual list
|
|
8
9
|
* — it does NOT value-import @tanstack/react-table, so the data-table peer is never required.
|
package/src/index.ts
CHANGED
|
@@ -113,6 +113,7 @@ export type { GuardKind } from './guard/types'
|
|
|
113
113
|
export {
|
|
114
114
|
ThreadWorkspace,
|
|
115
115
|
ThreadFeed,
|
|
116
|
+
ThreadFeedRow,
|
|
116
117
|
ThreadOutcomeCard,
|
|
117
118
|
ThreadDetailPanel,
|
|
118
119
|
Composer,
|
|
@@ -123,18 +124,34 @@ export {
|
|
|
123
124
|
export type {
|
|
124
125
|
ThreadWorkspaceProps,
|
|
125
126
|
ThreadFeedProps,
|
|
127
|
+
ThreadFeedRowProps,
|
|
126
128
|
ThreadOutcomeCardProps,
|
|
127
129
|
ThreadDetailPanelProps,
|
|
128
130
|
ComposerProps,
|
|
131
|
+
ComposerSubmit,
|
|
132
|
+
ComposerAttachment,
|
|
133
|
+
ComposerHandle,
|
|
129
134
|
ThreadTranscriptProps,
|
|
135
|
+
MessageAffordances,
|
|
136
|
+
VirtualizeOptions,
|
|
137
|
+
VirtualizeProps,
|
|
130
138
|
ToolChipProps,
|
|
131
139
|
} from './agent-chat'
|
|
132
|
-
export {
|
|
140
|
+
export {
|
|
141
|
+
createAdapterThreadsStore,
|
|
142
|
+
createThreadsStore,
|
|
143
|
+
heuristicOutcome,
|
|
144
|
+
threadsStoreAdapterContract,
|
|
145
|
+
useAgentThreadRuns,
|
|
146
|
+
} from './agent'
|
|
133
147
|
export type {
|
|
148
|
+
AdapterThreadsStoreOptions,
|
|
134
149
|
AgentThread,
|
|
135
150
|
AgentOutcome,
|
|
136
151
|
ThreadStatus,
|
|
137
152
|
ThreadsStore,
|
|
153
|
+
ThreadsStoreAdapter,
|
|
154
|
+
ThreadsStoreAdapterContractCase,
|
|
138
155
|
ThreadsStoreOptions,
|
|
139
156
|
OutcomeResolver,
|
|
140
157
|
ChatMessage,
|
package/src/surfaces.ts
CHANGED
|
@@ -195,7 +195,14 @@ export const SURFACES = {
|
|
|
195
195
|
],
|
|
196
196
|
description:
|
|
197
197
|
'BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, SettingsSection/SettingsRow/DangerZone)',
|
|
198
|
-
optionalPeers: [
|
|
198
|
+
optionalPeers: [
|
|
199
|
+
'react-markdown',
|
|
200
|
+
'remark-gfm',
|
|
201
|
+
'rehype-sanitize',
|
|
202
|
+
'remend',
|
|
203
|
+
'shiki',
|
|
204
|
+
'beautiful-mermaid',
|
|
205
|
+
],
|
|
199
206
|
forbiddenImports: [], // the no-charts/tokens-reexport invariant is comment-only today; Phase-4 plugin
|
|
200
207
|
},
|
|
201
208
|
'./charts': {
|
|
@@ -378,7 +385,7 @@ export const SURFACES = {
|
|
|
378
385
|
skill: ['basalt-app'],
|
|
379
386
|
guardKinds: [],
|
|
380
387
|
description:
|
|
381
|
-
'Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript (open part-renderer registry via its renderers/fallbackRenderer props), threadPartRenderers, ToolChip (Mantine-coupled).
|
|
388
|
+
'Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed (variant/renderRow), ThreadFeedRow (inline-expanding Slack row, lazily mounted + kept mounted), ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript (open part-renderer registry via its renderers/fallbackRenderer props, per-message MessageAffordances, groupConsecutive, and an optional virtualize/height windowing mode whose VirtualizeOptions carry overscan/estimateSize/initialScroll — a virtualized transcript opens scrolled to the newest message unless initialScroll is "start"), threadPartRenderers, ToolChip (Mantine-coupled). motion is required, not optional — ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without it installed even though peerDependenciesMeta marks it optional (npm has no per-subpath optionality). remend is genuinely optional here — ThreadTranscript reaches it only through the lazy dynamic import() inside content/markdown.tsx, and @tanstack/react-virtual the same way through the lazy import() behind ThreadTranscript virtualize (absent peer degrades to an unwindowed, height-bound pane).',
|
|
382
389
|
optionalPeers: [
|
|
383
390
|
'ai',
|
|
384
391
|
'motion',
|
|
@@ -390,6 +397,7 @@ export const SURFACES = {
|
|
|
390
397
|
'@shikijs/langs',
|
|
391
398
|
'@shikijs/themes',
|
|
392
399
|
'beautiful-mermaid',
|
|
400
|
+
'@tanstack/react-virtual',
|
|
393
401
|
],
|
|
394
402
|
forbiddenImports: [],
|
|
395
403
|
},
|
|
@@ -400,7 +408,7 @@ export const SURFACES = {
|
|
|
400
408
|
skill: ['basalt-design'],
|
|
401
409
|
guardKinds: [],
|
|
402
410
|
description:
|
|
403
|
-
'Prose (article/chat typography), CodeBlock (shiki, optional peer), Callout, TableOfContents, ReadingProgress, Markdown (react-markdown + remark-gfm, optional peers, streaming-
|
|
411
|
+
'Prose (article/chat typography), CodeBlock (shiki, optional peer), Callout, TableOfContents, ReadingProgress, Markdown (react-markdown + remark-gfm, optional peers; `streaming` is a rendering mode ONLY — `contentTrust` is the independent security input, and any surface rendering agent/model output must pin `contentTrust="untrusted"`, the sole input to the image-origin allowlist; a `fenceRenderers` registry — settledOnly/FenceRenderer/FenceRenderers/FenceRenderContext; `sanitizeSchema`, an additions-only SanitizeSchemaExtension merged over BASALT_SANITIZE_SCHEMA via mergeSanitizeSchema; the remend streaming-repair pass is now a lazy optional peer), MermaidDiagram (beautiful-mermaid, optional peer), mdxComponents/createMdxComponents, ArticleLayout (docs-page frame), ArticleCard/ArticleGrid (overview cards), Article model (sortArticles/filterArticles/formatArticleDate), ArticleFilterBar (category/tags filter UI), toArticleActions (Spotlight projector, @mantine/spotlight type-only), GuideLink/GuideDrawer (contextual-help drawer) — the content/prose surface',
|
|
404
412
|
optionalPeers: [
|
|
405
413
|
'shiki',
|
|
406
414
|
'@shikijs/langs',
|
|
@@ -408,6 +416,8 @@ export const SURFACES = {
|
|
|
408
416
|
'beautiful-mermaid',
|
|
409
417
|
'react-markdown',
|
|
410
418
|
'remark-gfm',
|
|
419
|
+
'rehype-sanitize',
|
|
420
|
+
'remend',
|
|
411
421
|
'@mantine/spotlight',
|
|
412
422
|
],
|
|
413
423
|
forbiddenImports: [],
|