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.
Files changed (273) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +36 -27
  4. package/agent/rules/basalt-content.md +16 -3
  5. package/dist/agent/adapter.d.ts +146 -0
  6. package/dist/agent/adapter.d.ts.map +1 -0
  7. package/dist/agent/adapter.js +473 -0
  8. package/dist/agent/adapter.test.d.ts +2 -0
  9. package/dist/agent/adapter.test.d.ts.map +1 -0
  10. package/dist/agent/ai-sdk-transport.d.ts +3 -52
  11. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  12. package/dist/agent/ai-sdk-transport.js +108 -24
  13. package/dist/agent/ai-sdk-transport.test.d.ts +2 -0
  14. package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
  15. package/dist/agent/coalesce.d.ts +42 -0
  16. package/dist/agent/coalesce.d.ts.map +1 -0
  17. package/dist/agent/coalesce.js +137 -0
  18. package/dist/agent/coalesce.test.d.ts +2 -0
  19. package/dist/agent/coalesce.test.d.ts.map +1 -0
  20. package/dist/agent/foreign.d.ts +97 -0
  21. package/dist/agent/foreign.d.ts.map +1 -0
  22. package/dist/agent/foreign.js +18 -0
  23. package/dist/agent/foreign.test.d.ts +2 -0
  24. package/dist/agent/foreign.test.d.ts.map +1 -0
  25. package/dist/agent/history.d.ts +2 -0
  26. package/dist/agent/history.d.ts.map +1 -1
  27. package/dist/agent/id.d.ts +65 -0
  28. package/dist/agent/id.d.ts.map +1 -0
  29. package/dist/agent/id.js +30 -0
  30. package/dist/agent/id.test.d.ts +2 -0
  31. package/dist/agent/id.test.d.ts.map +1 -0
  32. package/dist/agent/index.d.ts +11 -4
  33. package/dist/agent/index.d.ts.map +1 -1
  34. package/dist/agent/index.js +18 -3
  35. package/dist/agent/merge.d.ts +8 -0
  36. package/dist/agent/merge.d.ts.map +1 -0
  37. package/dist/agent/merge.js +35 -0
  38. package/dist/agent/merge.test.d.ts +2 -0
  39. package/dist/agent/merge.test.d.ts.map +1 -0
  40. package/dist/agent/part-list.d.ts +49 -34
  41. package/dist/agent/part-list.d.ts.map +1 -1
  42. package/dist/agent/part-list.js +53 -7
  43. package/dist/agent/part-list.test.d.ts +2 -0
  44. package/dist/agent/part-list.test.d.ts.map +1 -0
  45. package/dist/agent/parts.d.ts +135 -20
  46. package/dist/agent/parts.d.ts.map +1 -1
  47. package/dist/agent/parts.js +138 -11
  48. package/dist/agent/parts.test.d.ts +2 -0
  49. package/dist/agent/parts.test.d.ts.map +1 -0
  50. package/dist/agent/thread.d.ts +14 -0
  51. package/dist/agent/thread.d.ts.map +1 -1
  52. package/dist/agent/thread.js +8 -2
  53. package/dist/agent/thread.test.d.ts +2 -0
  54. package/dist/agent/thread.test.d.ts.map +1 -0
  55. package/dist/agent/transport.d.ts +39 -3
  56. package/dist/agent/transport.d.ts.map +1 -1
  57. package/dist/agent/transport.js +20 -17
  58. package/dist/agent/transport.test.d.ts +2 -0
  59. package/dist/agent/transport.test.d.ts.map +1 -0
  60. package/dist/agent/use-agent-stream.d.ts +2 -1
  61. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  62. package/dist/agent/use-agent-stream.js +5 -2
  63. package/dist/agent/use-agent-stream.test.d.ts +2 -0
  64. package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
  65. package/dist/agent/use-agent-thread-runs.d.ts +9 -3
  66. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  67. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +2 -0
  68. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
  69. package/dist/agent/use-agent-thread-runs.js +75 -23
  70. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
  71. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
  72. package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
  73. package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
  74. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
  75. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
  76. package/dist/agent-chat/composer.d.ts +130 -8
  77. package/dist/agent-chat/composer.d.ts.map +1 -1
  78. package/dist/agent-chat/composer.js +189 -23
  79. package/dist/agent-chat/composer.test.d.ts +2 -0
  80. package/dist/agent-chat/composer.test.d.ts.map +1 -0
  81. package/dist/agent-chat/index.d.ts +3 -1
  82. package/dist/agent-chat/index.d.ts.map +1 -1
  83. package/dist/agent-chat/index.js +2 -0
  84. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  85. package/dist/agent-chat/thread-detail-panel.js +1 -1
  86. package/dist/agent-chat/thread-message.d.ts +24 -7
  87. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  88. package/dist/agent-chat/thread-message.js +115 -45
  89. package/dist/agent-chat/thread-message.test.d.ts +2 -0
  90. package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
  91. package/dist/agent-chat/thread-workspace.js +1 -1
  92. package/dist/agent-chat/tool-chip.d.ts +21 -0
  93. package/dist/agent-chat/tool-chip.d.ts.map +1 -0
  94. package/dist/agent-chat/tool-chip.js +120 -0
  95. package/dist/agent-chat/tool-chip.test.d.ts +2 -0
  96. package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
  97. package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
  98. package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
  99. package/dist/charts/utils/domain.test.d.ts +2 -0
  100. package/dist/charts/utils/domain.test.d.ts.map +1 -0
  101. package/dist/cli/check-theme.test.d.ts +2 -0
  102. package/dist/cli/check-theme.test.d.ts.map +1 -0
  103. package/dist/cli/help.test.d.ts +2 -0
  104. package/dist/cli/help.test.d.ts.map +1 -0
  105. package/dist/cli/index.d.ts.map +1 -1
  106. package/dist/cli/init-sync.test.d.ts +2 -0
  107. package/dist/cli/init-sync.test.d.ts.map +1 -0
  108. package/dist/cli/placement-engine.test.d.ts +2 -0
  109. package/dist/cli/placement-engine.test.d.ts.map +1 -0
  110. package/dist/cli/tarball-parity.test.d.ts +2 -0
  111. package/dist/cli/tarball-parity.test.d.ts.map +1 -0
  112. package/dist/cli/template-format.test.d.ts +2 -0
  113. package/dist/cli/template-format.test.d.ts.map +1 -0
  114. package/dist/cli/template-ledger.test.d.ts +2 -0
  115. package/dist/cli/template-ledger.test.d.ts.map +1 -0
  116. package/dist/cli/tokens-css.test.d.ts +2 -0
  117. package/dist/cli/tokens-css.test.d.ts.map +1 -0
  118. package/dist/content/article-actions.test.d.ts +2 -0
  119. package/dist/content/article-actions.test.d.ts.map +1 -0
  120. package/dist/content/article-model.test.d.ts +2 -0
  121. package/dist/content/article-model.test.d.ts.map +1 -0
  122. package/dist/content/block-split.test.d.ts +2 -0
  123. package/dist/content/block-split.test.d.ts.map +1 -0
  124. package/dist/content/fence-block.d.ts +37 -10
  125. package/dist/content/fence-block.d.ts.map +1 -1
  126. package/dist/content/fence-block.js +80 -14
  127. package/dist/content/heading-components.d.ts.map +1 -1
  128. package/dist/content/heading-components.js +7 -4
  129. package/dist/content/heading-components.test.d.ts +2 -0
  130. package/dist/content/heading-components.test.d.ts.map +1 -0
  131. package/dist/content/index.d.ts +6 -3
  132. package/dist/content/index.d.ts.map +1 -1
  133. package/dist/content/index.js +5 -1
  134. package/dist/content/markdown.d.ts +101 -14
  135. package/dist/content/markdown.d.ts.map +1 -1
  136. package/dist/content/markdown.js +141 -28
  137. package/dist/content/markdown.test.d.ts +2 -0
  138. package/dist/content/markdown.test.d.ts.map +1 -0
  139. package/dist/content/prose.module.css.test.d.ts +2 -0
  140. package/dist/content/prose.module.css.test.d.ts.map +1 -0
  141. package/dist/content/sanitize.d.ts +170 -0
  142. package/dist/content/sanitize.d.ts.map +1 -0
  143. package/dist/content/sanitize.js +178 -0
  144. package/dist/content/sanitize.test.d.ts +2 -0
  145. package/dist/content/sanitize.test.d.ts.map +1 -0
  146. package/dist/content/slug.test.d.ts +2 -0
  147. package/dist/content/slug.test.d.ts.map +1 -0
  148. package/dist/dashboard/stat-card.test.d.ts +2 -0
  149. package/dist/dashboard/stat-card.test.d.ts.map +1 -0
  150. package/dist/guard/check-source.test.d.ts +2 -0
  151. package/dist/guard/check-source.test.d.ts.map +1 -0
  152. package/dist/guard/guard-hook.test.d.ts +2 -0
  153. package/dist/guard/guard-hook.test.d.ts.map +1 -0
  154. package/dist/guard/index.d.ts.map +1 -1
  155. package/dist/guard/index.js +1 -3
  156. package/dist/index.d.ts +4 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +12 -2
  159. package/dist/provider/index.test.d.ts +2 -0
  160. package/dist/provider/index.test.d.ts.map +1 -0
  161. package/dist/provider/lab-theme.test.d.ts +2 -0
  162. package/dist/provider/lab-theme.test.d.ts.map +1 -0
  163. package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
  164. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
  165. package/dist/styles.floor.test.d.ts +2 -0
  166. package/dist/styles.floor.test.d.ts.map +1 -0
  167. package/dist/surfaces.d.ts +5 -5
  168. package/dist/surfaces.d.ts.map +1 -1
  169. package/dist/surfaces.js +13 -4
  170. package/dist/theme/border-coverage.test.d.ts +18 -0
  171. package/dist/theme/border-coverage.test.d.ts.map +1 -0
  172. package/dist/theme/contrast.test.d.ts +2 -0
  173. package/dist/theme/contrast.test.d.ts.map +1 -0
  174. package/dist/theme/density-option.test.d.ts +2 -0
  175. package/dist/theme/density-option.test.d.ts.map +1 -0
  176. package/dist/theme/density-relations.test.d.ts +2 -0
  177. package/dist/theme/density-relations.test.d.ts.map +1 -0
  178. package/dist/theme/derive-theme.test.d.ts +2 -0
  179. package/dist/theme/derive-theme.test.d.ts.map +1 -0
  180. package/dist/theme/fonts-option.test.d.ts +2 -0
  181. package/dist/theme/fonts-option.test.d.ts.map +1 -0
  182. package/dist/theme/radius-option.test.d.ts +2 -0
  183. package/dist/theme/radius-option.test.d.ts.map +1 -0
  184. package/dist/theme/radius.test.d.ts +2 -0
  185. package/dist/theme/radius.test.d.ts.map +1 -0
  186. package/dist/theme/shadow-surfaces.test.d.ts +26 -0
  187. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
  188. package/dist/theme/spacing.test.d.ts +2 -0
  189. package/dist/theme/spacing.test.d.ts.map +1 -0
  190. package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
  191. package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
  192. package/dist/theme-lab/derive-controls.test.d.ts +2 -0
  193. package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
  194. package/dist/theme-lab/derive-state.test.d.ts +2 -0
  195. package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
  196. package/dist/theme-lab/theme-lab.test.d.ts +2 -0
  197. package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
  198. package/dist/tokens/build-fonts-css.test.d.ts +2 -0
  199. package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
  200. package/dist/tokens/build-radius-css.test.d.ts +2 -0
  201. package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
  202. package/dist/tokens/density.test.d.ts +2 -0
  203. package/dist/tokens/density.test.d.ts.map +1 -0
  204. package/dist/tokens/derive-radius.test.d.ts +2 -0
  205. package/dist/tokens/derive-radius.test.d.ts.map +1 -0
  206. package/dist/tokens/derive.test.d.ts +2 -0
  207. package/dist/tokens/derive.test.d.ts.map +1 -0
  208. package/dist/tokens/palette.test.d.ts +2 -0
  209. package/dist/tokens/palette.test.d.ts.map +1 -0
  210. package/dist/vite-color-mix.test.d.ts +2 -0
  211. package/dist/vite-color-mix.test.d.ts.map +1 -0
  212. package/dist/vite.test.d.ts +2 -0
  213. package/dist/vite.test.d.ts.map +1 -0
  214. package/llms.txt +7 -7
  215. package/package.json +6 -1
  216. package/src/agent/adapter.test.ts +1403 -0
  217. package/src/agent/adapter.ts +887 -0
  218. package/src/agent/ai-sdk-transport.test.ts +380 -55
  219. package/src/agent/ai-sdk-transport.ts +206 -53
  220. package/src/agent/coalesce.test.ts +387 -0
  221. package/src/agent/coalesce.ts +229 -0
  222. package/src/agent/foreign.test.ts +51 -0
  223. package/src/agent/foreign.ts +140 -0
  224. package/src/agent/history.ts +2 -0
  225. package/src/agent/id.test.ts +75 -0
  226. package/src/agent/id.ts +91 -0
  227. package/src/agent/index.ts +34 -3
  228. package/src/agent/merge.test.ts +206 -0
  229. package/src/agent/merge.ts +114 -0
  230. package/src/agent/part-list.test.tsx +112 -0
  231. package/src/agent/part-list.tsx +108 -35
  232. package/src/agent/parts.test.ts +516 -57
  233. package/src/agent/parts.ts +323 -32
  234. package/src/agent/thread.test.ts +75 -2
  235. package/src/agent/thread.ts +21 -1
  236. package/src/agent/transport.test.ts +70 -0
  237. package/src/agent/transport.ts +72 -20
  238. package/src/agent/use-agent-stream.test.tsx +53 -19
  239. package/src/agent/use-agent-stream.ts +13 -3
  240. package/src/agent/use-agent-thread-runs.foreign.test.tsx +138 -0
  241. package/src/agent/use-agent-thread-runs.resume.test.tsx +44 -11
  242. package/src/agent/use-agent-thread-runs.test.tsx +418 -22
  243. package/src/agent/use-agent-thread-runs.ts +170 -26
  244. package/src/agent/use-agent-thread-runs.wedge.test.tsx +15 -7
  245. package/src/agent-chat/composer.test.tsx +924 -0
  246. package/src/agent-chat/composer.tsx +521 -52
  247. package/src/agent-chat/index.ts +5 -1
  248. package/src/agent-chat/thread-detail-panel.tsx +3 -1
  249. package/src/agent-chat/thread-message.test.tsx +351 -0
  250. package/src/agent-chat/thread-message.tsx +262 -64
  251. package/src/agent-chat/thread-workspace.tsx +1 -1
  252. package/src/agent-chat/tool-chip.test.tsx +139 -0
  253. package/src/agent-chat/tool-chip.tsx +219 -0
  254. package/src/cli/index.ts +0 -1
  255. package/src/cli/template-format.test.ts +2 -2
  256. package/src/content/fence-block.tsx +242 -15
  257. package/src/content/heading-components.test.tsx +65 -0
  258. package/src/content/heading-components.tsx +26 -5
  259. package/src/content/index.ts +19 -3
  260. package/src/content/markdown.test.tsx +603 -0
  261. package/src/content/markdown.tsx +355 -35
  262. package/src/content/sanitize.test.ts +659 -0
  263. package/src/content/sanitize.ts +510 -0
  264. package/src/dashboard/stat-card.test.tsx +6 -1
  265. package/src/guard/check-source.test.ts +8 -4
  266. package/src/guard/index.ts +1 -3
  267. package/src/index.ts +15 -1
  268. package/src/surfaces.ts +13 -4
  269. package/src/theme/contrast.test.ts +1 -4
  270. package/src/theme/density-relations.test.ts +4 -2
  271. package/src/theme/fonts-option.test.ts +7 -1
  272. package/src/theme/shadow-surfaces.test.ts +12 -6
  273. package/src/theme/use-basalt-spacing.test.tsx +2 -1
@@ -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
- function LinkRenderer({ href, children }) {
13
- if (href === void 0) return /* @__PURE__ */ jsx("span", { children });
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(FenceBlock, { settled, ...node !== void 0 && { node } });
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
- ...rehypePlugins !== void 0 && {
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 repaired = useMemo(() => remend(props.text), [props.text]);
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({ text, components, urlTransform, remarkPlugins, rehypePlugins }) {
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 DEFAULT_IMAGE_PREFIXES = ["https://", "/"];
136
- const STREAMING_IMAGE_PREFIXES = ["/"];
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 = streaming ? STREAMING_IMAGE_PREFIXES : DEFAULT_IMAGE_PREFIXES,
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=markdown.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.test.d.ts","sourceRoot":"","sources":["../../src/content/markdown.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=prose.module.css.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sanitize.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.test.d.ts","sourceRoot":"","sources":["../../src/content/sanitize.test.ts"],"names":[],"mappings":""}