@ethisyscore/plugin-ui 1.110.0 → 1.112.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.
@@ -0,0 +1,524 @@
1
+ import { useMemo } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import rehypeSanitize from 'rehype-sanitize';
4
+ import remarkGfm from 'remark-gfm';
5
+ import remarkBreaks from 'remark-breaks';
6
+ import { defaultSchema } from 'hast-util-sanitize';
7
+ import { jsx } from 'react/jsx-runtime';
8
+
9
+ // src/markdown/MarkdownView.tsx
10
+
11
+ // src/markdown/guards/attributeNames.ts
12
+ function attributeNameOf(entry) {
13
+ return Array.isArray(entry) ? String(entry[0]) : String(entry);
14
+ }
15
+ var XLINK_PREFIX = /^xlink/;
16
+ function normalisedAttributeName(name) {
17
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "").replace(XLINK_PREFIX, "");
18
+ }
19
+
20
+ // src/markdown/guards/schemaMaps.ts
21
+ function ownMapEntry(map, key) {
22
+ if (map === void 0 || map === null) return void 0;
23
+ return Object.prototype.hasOwnProperty.call(map, key) ? map[key] : void 0;
24
+ }
25
+ function ownSchemaKey(schema, key) {
26
+ return ownMapEntry(schema, key);
27
+ }
28
+
29
+ // src/markdown/guards/attributes.ts
30
+ var FORBIDDEN_ATTRIBUTE_NAMES = /* @__PURE__ */ new Set([
31
+ "style",
32
+ "srcset",
33
+ "poster",
34
+ "formaction",
35
+ "ping",
36
+ "background",
37
+ "lowsrc",
38
+ "dynsrc"
39
+ ]);
40
+ var REMOTE_SOURCE_ATTRIBUTE_NAMES = /* @__PURE__ */ new Set([
41
+ "src",
42
+ "data",
43
+ "href"
44
+ ]);
45
+ var FUNCIRI_ATTRIBUTE_NAMES = /* @__PURE__ */ new Set([
46
+ "fill",
47
+ "stroke",
48
+ "filter",
49
+ "clippath",
50
+ "mask",
51
+ "marker",
52
+ "markerstart",
53
+ "markermid",
54
+ "markerend",
55
+ "cursor",
56
+ "colorprofile"
57
+ ]);
58
+ function grantedNames(capabilityId, tag, entries) {
59
+ if (entries === null || entries === void 0) return [];
60
+ if (Array.isArray(entries)) return entries;
61
+ throw new Error(
62
+ `Markdown capability "${capabilityId}" returned a sanitize schema whose attributes entry for <${tag}> is not a list. hast-util-sanitize reads each entry as an array of property definitions \u2014 "href" or ["className", /^hljs-/] \u2014 and every rule here reads it the same way. Write attributes: { ${tag}: ["\u2026"] }.`
63
+ );
64
+ }
65
+ function assertAttributesNotWidened(capabilityId, schemas, trust) {
66
+ const previousAttributes = ownSchemaKey(schemas.previous, "attributes") ?? {};
67
+ const nextAttributes = ownSchemaKey(schemas.next, "attributes") ?? {};
68
+ for (const [tag, entries] of Object.entries(nextAttributes)) {
69
+ const inherited = new Set((ownMapEntry(previousAttributes, tag) ?? []).map(attributeNameOf));
70
+ for (const entry of grantedNames(capabilityId, tag, entries)) {
71
+ const name = attributeNameOf(entry);
72
+ if (/^on/i.test(name)) {
73
+ throw new Error(
74
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow the event-handler attribute "${name}" on <${tag}>, which is never permitted.`
75
+ );
76
+ }
77
+ if (inherited.has(name)) continue;
78
+ const normalised = normalisedAttributeName(name);
79
+ if (FORBIDDEN_ATTRIBUTE_NAMES.has(normalised)) {
80
+ throw new Error(
81
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow the attribute "${name}" on <${tag}>, which fetches or retargets with no user action and is never permitted.`
82
+ );
83
+ }
84
+ if (trust === "untrusted" && REMOTE_SOURCE_ATTRIBUTE_NAMES.has(normalised)) {
85
+ throw new Error(
86
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow the attribute "${name}" on <${tag}> under trust="untrusted", which is never permitted: untrusted content must fire no outbound request on view, and the image-blocking transform only covers <img>. If you meant a link, allow it on <a>, which already carries "href".`
87
+ );
88
+ }
89
+ if (trust === "untrusted" && FUNCIRI_ATTRIBUTE_NAMES.has(normalised)) {
90
+ throw new Error(
91
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow the attribute "${name}" on <${tag}> under trust="untrusted", which is never permitted: its value may be a \`url(\u2026)\` reference, and the browser fetches that on paint exactly as it fetches an image. A schema grants the NAME, so there is no way from here to allow the safe values and not the remote one.`
92
+ );
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ // src/markdown/guards/attributeAliases.ts
99
+ var attributesOf = (schema) => ownSchemaKey(schema, "attributes") ?? {};
100
+ var SPELLING_SOURCES = [
101
+ (schema) => Object.keys(ownSchemaKey(schema, "protocols") ?? {}),
102
+ (schema) => ownSchemaKey(schema, "clobber") ?? [],
103
+ // Own entries only, the way the sanitizer resolves these maps — see `./schemaMaps`.
104
+ (schema) => (ownMapEntry(attributesOf(schema), "*") ?? []).map(attributeNameOf),
105
+ (schema, tag) => (ownMapEntry(attributesOf(schema), tag) ?? []).map(attributeNameOf)
106
+ ];
107
+ function policedSpellings(schemas, tag) {
108
+ const canonical = /* @__PURE__ */ new Map();
109
+ for (const spellingsOf of SPELLING_SOURCES) {
110
+ for (const name of [...spellingsOf(schemas.previous, tag), ...spellingsOf(schemas.next, tag)]) {
111
+ const normalised = normalisedAttributeName(name);
112
+ if (!canonical.has(normalised)) canonical.set(normalised, name);
113
+ }
114
+ }
115
+ return canonical;
116
+ }
117
+ function assertNoAliasedAttributeNames(capabilityId, schemas) {
118
+ for (const [tag, entries] of Object.entries(attributesOf(schemas.next))) {
119
+ const canonical = policedSpellings(schemas, tag);
120
+ for (const entry of entries ?? []) {
121
+ const name = attributeNameOf(entry);
122
+ const policed = canonical.get(normalisedAttributeName(name));
123
+ if (policed === void 0 || policed === name) continue;
124
+ throw new Error(
125
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow the attribute "${name}" on <${tag}>, which is "${policed}" spelled differently. hast-util-sanitize keys its URL-scheme allowlist and its DOM-clobbering prefix on the exact property name, while the DOM lower-cases it \u2014 so the second spelling reaches the browser as the same attribute with none of the restrictions attached to the first. Write "${policed}".`
126
+ );
127
+ }
128
+ }
129
+ }
130
+
131
+ // src/markdown/guards/protocols.ts
132
+ var SAFE_PROTOCOLS = /* @__PURE__ */ new Set([
133
+ "http",
134
+ "https",
135
+ "mailto",
136
+ "tel",
137
+ "ftp",
138
+ "irc",
139
+ "ircs",
140
+ "xmpp"
141
+ ]);
142
+ function restrictionsOf(schema) {
143
+ const restrictions = /* @__PURE__ */ new Map();
144
+ const map = ownSchemaKey(schema, "protocols") ?? {};
145
+ for (const [property, protocols] of Object.entries(map)) {
146
+ if (protocols && protocols.length > 0) restrictions.set(property, protocols);
147
+ }
148
+ return restrictions;
149
+ }
150
+ function assertProtocolsNotWidened(capabilityId, schemas) {
151
+ const next = restrictionsOf(schemas.next);
152
+ for (const [property, protocols] of next) {
153
+ for (const protocol of protocols) {
154
+ if (SAFE_PROTOCOLS.has(String(protocol).toLowerCase())) continue;
155
+ throw new Error(
156
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow the "${protocol}" URL scheme on "${property}", which is never permitted. Allowed schemes: ${[...SAFE_PROTOCOLS].join(", ")}.`
157
+ );
158
+ }
159
+ }
160
+ for (const property of restrictionsOf(schemas.previous).keys()) {
161
+ if (next.has(property)) continue;
162
+ throw new Error(
163
+ `Markdown capability "${capabilityId}" removed the URL-scheme restriction on "${property}". hast-util-sanitize treats a missing or empty protocol list as no restriction at all, so this admits every scheme, javascript: included. Spread the protocols you were given: (s) => ({ ...s, protocols: { ...s.protocols, ... } }).`
164
+ );
165
+ }
166
+ }
167
+
168
+ // src/markdown/guards/mutableKeyShapes.ts
169
+ var MUTABLE_KEY_SHAPES = {
170
+ tagNames: {
171
+ shape: "list",
172
+ whenFalsy: 'hast-util-sanitize reads a missing, null or otherwise falsy tagNames as "every element name is allowed" rather than as an empty allowlist, so this does not narrow the schema, it removes the element allowlist entirely \u2014 <script> included'
173
+ },
174
+ attributes: {
175
+ shape: "map",
176
+ whenFalsy: 'hast-util-sanitize resolves an attributes entry with own.call(attributes, tagName) and reads a missing or falsy map as "no element carries any attribute", so this widens nothing and silently strips every attribute the pipeline allowed \u2014 className, href and the DOM-clobber prefix with them'
177
+ },
178
+ protocols: {
179
+ shape: "map",
180
+ whenFalsy: "hast-util-sanitize scheme-checks a URL property only while the protocols map restricts it, so a missing or falsy map applies no URL-scheme restriction at all and every scheme survives, javascript: included"
181
+ }
182
+ };
183
+ var EXAMPLE = {
184
+ list: '(s) => ({ ...s, <key>: [...s.<key>, "\u2026"] })',
185
+ map: "(s) => ({ ...s, <key>: { ...s.<key>, \u2026 } })"
186
+ };
187
+ function hasShape(value, shape) {
188
+ if (shape === "list") return Array.isArray(value);
189
+ return typeof value === "object" && value !== null && !Array.isArray(value);
190
+ }
191
+ function shapeError(capabilityId, key, rule) {
192
+ return new Error(
193
+ `Markdown capability "${capabilityId}" widened the sanitize schema so that "${key}" is not a ${rule.shape}. ${rule.whenFalsy}. Spread the ${rule.shape} you were given: ${EXAMPLE[rule.shape].replaceAll("<key>", key)}.`
194
+ );
195
+ }
196
+ function assertMutableKeyShape(capabilityId, key, value) {
197
+ const rule = MUTABLE_KEY_SHAPES[key];
198
+ if (hasShape(value, rule.shape)) return;
199
+ throw shapeError(capabilityId, key, rule);
200
+ }
201
+ function assertMutableKeysWellShaped(capabilityId, effective) {
202
+ for (const key of Object.keys(MUTABLE_KEY_SHAPES)) {
203
+ assertMutableKeyShape(capabilityId, key, ownSchemaKey(effective, key));
204
+ }
205
+ }
206
+
207
+ // src/markdown/guards/mutableKeys.ts
208
+ var MUTABLE_SCHEMA_KEYS = new Set(Object.keys(MUTABLE_KEY_SHAPES));
209
+ function sameValue(a, b) {
210
+ if (Object.is(a, b)) return true;
211
+ if (a instanceof RegExp || b instanceof RegExp) {
212
+ return a instanceof RegExp && b instanceof RegExp && a.source === b.source && a.flags === b.flags;
213
+ }
214
+ if (Array.isArray(a) || Array.isArray(b)) {
215
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((entry, index) => sameValue(entry, b[index]));
216
+ }
217
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
218
+ const left = a;
219
+ const right = b;
220
+ const keys = Object.keys(left);
221
+ return keys.length === Object.keys(right).length && keys.every(
222
+ (key) => Object.prototype.hasOwnProperty.call(right, key) && sameValue(left[key], right[key])
223
+ );
224
+ }
225
+ function assertOnlyMutableKeysChanged(capabilityId, schemas) {
226
+ const previous = schemas.previous;
227
+ const next = schemas.next;
228
+ const changed = Object.keys(next).filter(
229
+ (key) => !MUTABLE_SCHEMA_KEYS.has(key) && (!Object.prototype.hasOwnProperty.call(previous, key) || !sameValue(next[key], ownMapEntry(previous, key)))
230
+ );
231
+ if (changed.length === 0) return;
232
+ throw new Error(
233
+ `Markdown capability "${capabilityId}" changed the sanitize schema key ${changed.map((key) => `"${key}"`).join(", ")}, which a capability may not change. A capability may widen only ${[...MUTABLE_SCHEMA_KEYS].map((key) => `"${key}"`).join(", ")} \u2014 the keys a rendering feature needs. The rest are the sanitizer's own policy (clobber/clobberPrefix stop DOM clobbering; a \`required\` default is written out with no protocol check), so changing one is a change to the markdown package with a reviewer attached, not something a capability does. Spread what you were given and touch only the keys your own markup needs.`
234
+ );
235
+ }
236
+
237
+ // src/markdown/guards/returnedSchema.ts
238
+ function assertSchemaReturned(capabilityId, value) {
239
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) return;
240
+ throw new Error(
241
+ `Markdown capability "${capabilityId}" has an extendSchema that returned ${Array.isArray(value) ? "an array" : String(value)} rather than a schema. extendSchema must return the schema it was given, widened: (s) => ({ ...s, ... }). Returning nothing on some branch is the usual cause.`
242
+ );
243
+ }
244
+
245
+ // src/markdown/guards/schemaKeys.ts
246
+ function assertNoDroppedKeys(capabilityId, schemas) {
247
+ const dropped = Object.keys(schemas.previous).filter(
248
+ (key) => !Object.prototype.hasOwnProperty.call(schemas.next, key)
249
+ );
250
+ if (dropped.length === 0) return;
251
+ throw new Error(
252
+ `Markdown capability "${capabilityId}" returned a schema without ${dropped.map((key) => `"${key}"`).join(", ")} as an own property. hast-util-sanitize merges a schema over its own defaults one level deep and copies own properties only, so a dropped (or merely inherited) key falls back to the library default rather than to the pipeline's schema. Spread the schema you were given: (s) => ({ ...s, ... }).`
253
+ );
254
+ }
255
+
256
+ // src/markdown/guards/schemaValues.ts
257
+ var SCHEMA_VALUE_TYPES = /* @__PURE__ */ new Set([
258
+ "string",
259
+ "number",
260
+ "boolean",
261
+ "undefined"
262
+ ]);
263
+ function describe(value) {
264
+ if (typeof value === "function") return "a function";
265
+ return `a ${typeof value}`;
266
+ }
267
+ function assertSchemaIsData(capabilityId, schema) {
268
+ walk(capabilityId, schema, "");
269
+ }
270
+ function walk(capabilityId, value, path) {
271
+ if (value === null || value instanceof RegExp) return;
272
+ if (SCHEMA_VALUE_TYPES.has(typeof value)) return;
273
+ if (Array.isArray(value)) {
274
+ value.forEach((entry, index) => walk(capabilityId, entry, `${path}[${index}]`));
275
+ return;
276
+ }
277
+ if (typeof value === "object") {
278
+ for (const [key, entry] of Object.entries(value)) {
279
+ walk(capabilityId, entry, path ? `${path}.${key}` : key);
280
+ }
281
+ return;
282
+ }
283
+ throw new Error(
284
+ `Markdown capability "${capabilityId}" returned a sanitize schema whose ${path || "root"} is ${describe(value)}, not data. A schema is read by the guards here and then again by hast-util-sanitize at render time, and the sanitizer ASKS its values rather than iterating them \u2014 \`tagNames.includes(name)\` \u2014 so a value that can run can answer the two differently. Only strings, numbers, booleans, arrays, plain objects and RegExp belong in a schema.`
285
+ );
286
+ }
287
+
288
+ // src/markdown/guards/tagNames.ts
289
+ var FORBIDDEN_TAG_NAMES = [
290
+ "script",
291
+ "style",
292
+ "iframe",
293
+ "object",
294
+ "embed",
295
+ "base",
296
+ "meta",
297
+ "form",
298
+ // The third spelling of `style`. `<style>` is the inline one and `style=` the
299
+ // attribute (denied in `./attributes`); `<link rel="stylesheet" href>` is the same
300
+ // primitive fetched from off-origin, and it was the one missing. Under
301
+ // `trust="untrusted"` the `href` denial in `./attributes` happens to stop it, but
302
+ // under `internal` it rendered live: `<link rel="stylesheet" href="https://…">`.
303
+ // Attacker CSS is not cosmetic — it reads the page through selectors and exfiltrates
304
+ // through background URLs.
305
+ "link"
306
+ ];
307
+ var ANIMATION_TAG_NAMES = [
308
+ "animate",
309
+ "animatecolor",
310
+ "animatemotion",
311
+ "animatetransform",
312
+ "set"
313
+ ];
314
+ var IMAGE_TAG_NAMES = ["img", "picture", "source"];
315
+ var FORBIDDEN_TAG_SET = new Set(FORBIDDEN_TAG_NAMES);
316
+ var ANIMATION_TAG_SET = new Set(ANIMATION_TAG_NAMES);
317
+ var IMAGE_TAG_SET = new Set(IMAGE_TAG_NAMES);
318
+ function assertTagNamesAllowed(capabilityId, effective, trust) {
319
+ const tagNames = ownSchemaKey(effective, "tagNames");
320
+ assertMutableKeyShape(capabilityId, "tagNames", tagNames);
321
+ for (const tagName of tagNames) {
322
+ const normalised = String(tagName).toLowerCase();
323
+ if (FORBIDDEN_TAG_SET.has(normalised)) {
324
+ throw new Error(
325
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow <${tagName}>, which is never permitted.`
326
+ );
327
+ }
328
+ if (ANIMATION_TAG_SET.has(normalised)) {
329
+ throw new Error(
330
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow <${tagName}>, which rewrites another element's attributes after sanitize and so steps around every value rule here, the URL-scheme allowlist included. Never permitted, on any trust profile.`
331
+ );
332
+ }
333
+ if (trust === "untrusted" && IMAGE_TAG_SET.has(normalised)) {
334
+ throw new Error(
335
+ `Markdown capability "${capabilityId}" widened the sanitize schema to allow <${tagName}> under trust="untrusted", which is never permitted: untrusted content must fire no outbound request on view.`
336
+ );
337
+ }
338
+ }
339
+ }
340
+
341
+ // src/markdown/rehypeBlockRemoteImages.ts
342
+ var BLOCKED_IMAGE_TAG_NAME = "blockedimage";
343
+ var BLOCKED_IMAGE_ALT_PROP = "dataAlt";
344
+ function rehypeBlockRemoteImages() {
345
+ return (tree) => {
346
+ visit(tree);
347
+ };
348
+ }
349
+ function visit(node) {
350
+ const children = node.children;
351
+ if (!children) return;
352
+ for (let index = 0; index < children.length; index += 1) {
353
+ const child = children[index];
354
+ if (child.type !== "element") continue;
355
+ if (child.tagName === "img") {
356
+ children[index] = toPlaceholder(child);
357
+ continue;
358
+ }
359
+ visit(child);
360
+ }
361
+ }
362
+ function toPlaceholder(image) {
363
+ const alt = image.properties?.alt;
364
+ return {
365
+ type: "element",
366
+ tagName: BLOCKED_IMAGE_TAG_NAME,
367
+ properties: typeof alt === "string" && alt ? { [BLOCKED_IMAGE_ALT_PROP]: alt } : {},
368
+ children: []
369
+ };
370
+ }
371
+
372
+ // src/markdown/schemaClone.ts
373
+ function cloneSchema(value) {
374
+ if (Array.isArray(value)) {
375
+ return Array.from(value, (entry) => cloneSchema(entry));
376
+ }
377
+ if (value instanceof RegExp) {
378
+ return new RegExp(value.source, value.flags);
379
+ }
380
+ if (value !== null && typeof value === "object") {
381
+ const out = /* @__PURE__ */ Object.create(null);
382
+ for (const [key, entry] of Object.entries(value)) {
383
+ out[key] = cloneSchema(entry);
384
+ }
385
+ return out;
386
+ }
387
+ return value;
388
+ }
389
+ function deepFreeze(value) {
390
+ if (value === null || typeof value !== "object") return value;
391
+ if (value instanceof RegExp) return value;
392
+ Object.freeze(value);
393
+ for (const entry of Object.values(value)) deepFreeze(entry);
394
+ return value;
395
+ }
396
+
397
+ // src/markdown/schema.ts
398
+ var defaults = cloneSchema(defaultSchema);
399
+ var IMAGE_TAG_SET2 = new Set(IMAGE_TAG_NAMES);
400
+ var baseMarkdownSchema = deepFreeze({
401
+ ...defaults,
402
+ tagNames: [.../* @__PURE__ */ new Set([...defaults.tagNames ?? [], "input"])],
403
+ attributes: {
404
+ ...defaults.attributes ?? {},
405
+ input: ["type", "checked", "disabled", "className"]
406
+ }
407
+ });
408
+ var untrustedSchema = deepFreeze({
409
+ ...baseMarkdownSchema,
410
+ tagNames: [
411
+ ...(baseMarkdownSchema.tagNames ?? []).filter((tag) => !IMAGE_TAG_SET2.has(tag)),
412
+ BLOCKED_IMAGE_TAG_NAME
413
+ ],
414
+ attributes: {
415
+ ...baseMarkdownSchema.attributes ?? {},
416
+ [BLOCKED_IMAGE_TAG_NAME]: [BLOCKED_IMAGE_ALT_PROP]
417
+ }
418
+ });
419
+ function effectiveSchema(schema) {
420
+ return { ...defaultSchema, ...schema };
421
+ }
422
+ function assertCapabilitySchema(schema, capabilityId, context) {
423
+ assertSchemaIsData(capabilityId, schema);
424
+ const returned = { next: schema, previous: context.previous };
425
+ assertNoDroppedKeys(capabilityId, returned);
426
+ assertOnlyMutableKeysChanged(capabilityId, returned);
427
+ const effective = {
428
+ next: effectiveSchema(returned.next),
429
+ previous: effectiveSchema(returned.previous)
430
+ };
431
+ assertMutableKeysWellShaped(capabilityId, effective.next);
432
+ assertTagNamesAllowed(capabilityId, effective.next, context.trust);
433
+ assertAttributesNotWidened(capabilityId, effective, context.trust);
434
+ assertNoAliasedAttributeNames(capabilityId, effective);
435
+ assertProtocolsNotWidened(capabilityId, effective);
436
+ }
437
+
438
+ // src/markdown/trust.ts
439
+ var MARKDOWN_TRUST_VALUES = ["untrusted", "internal"];
440
+ var DEFAULT_MARKDOWN_TRUST = "untrusted";
441
+ var TRUST_VALUE_SET = new Set(MARKDOWN_TRUST_VALUES);
442
+ function normalizeTrust(value) {
443
+ return typeof value === "string" && TRUST_VALUE_SET.has(value) ? value : DEFAULT_MARKDOWN_TRUST;
444
+ }
445
+ function BlockedImage({ node }) {
446
+ const alt = node?.properties?.[BLOCKED_IMAGE_ALT_PROP];
447
+ const label = typeof alt === "string" && alt ? `[Image not shown: ${alt}]` : "[Image not shown]";
448
+ return /* @__PURE__ */ jsx("span", { className: "ethisys-md-blocked-image", "data-testid": "markdown-blocked-image", children: label });
449
+ }
450
+ var BLOCKED_IMAGE_COMPONENTS = {
451
+ [BLOCKED_IMAGE_TAG_NAME]: BlockedImage
452
+ };
453
+
454
+ // src/markdown/pipeline.ts
455
+ function buildMarkdownPipeline(options = {}) {
456
+ const { softBreaks = false, capabilities = [], components } = options;
457
+ const trust = normalizeTrust(options.trust);
458
+ const seen = /* @__PURE__ */ new Set();
459
+ for (const cap of capabilities) {
460
+ if (seen.has(cap.id)) throw new Error(`Duplicate markdown capability id: ${cap.id}`);
461
+ seen.add(cap.id);
462
+ }
463
+ const schema = capabilities.reduce(
464
+ (acc, cap) => {
465
+ if (!cap.extendSchema) return acc;
466
+ const returned = cap.extendSchema(cloneSchema(acc));
467
+ assertSchemaReturned(cap.id, returned);
468
+ const widened = cloneSchema(returned);
469
+ assertCapabilitySchema(widened, cap.id, { trust, previous: acc });
470
+ return deepFreeze(widened);
471
+ },
472
+ trust === "untrusted" ? untrustedSchema : baseMarkdownSchema
473
+ );
474
+ const remarkPlugins = Object.freeze([
475
+ remarkGfm,
476
+ ...softBreaks ? [remarkBreaks] : [],
477
+ ...capabilities.flatMap((c) => [...c.remarkPlugins ?? []])
478
+ ]);
479
+ const rehypePlugins = Object.freeze([
480
+ ...trust === "untrusted" ? [rehypeBlockRemoteImages] : [],
481
+ ...capabilities.flatMap((c) => [...c.rehypeBeforeSanitize ?? []]),
482
+ // The TUPLE is frozen too, not just the array around it. `Object.freeze` is one
483
+ // level deep: a frozen array still holds a writable tuple, and
484
+ // `rehypePlugins.at(-1)[1] = permissiveSchema` would then replace the schema this
485
+ // function spent its whole body validating — or `[0]` the sanitizer itself —
486
+ // without touching anything frozen. Unreachable today (nothing outside this module
487
+ // holds the array), but `MarkdownPipeline` documents the last entry as frozen, and
488
+ // a freeze that does not freeze what it appears to is the trap this package exists
489
+ // to stop shipping.
490
+ Object.freeze([rehypeSanitize, schema])
491
+ ]);
492
+ const mergedComponents = Object.assign(
493
+ {},
494
+ ...capabilities.map((c) => c.components ?? {}),
495
+ components ?? {},
496
+ // LAST: the built-in blocked-image renderer cannot be overridden by a caller.
497
+ trust === "untrusted" ? BLOCKED_IMAGE_COMPONENTS : {}
498
+ );
499
+ return Object.freeze({
500
+ remarkPlugins,
501
+ rehypePlugins,
502
+ components: mergedComponents,
503
+ skipHtml: true
504
+ });
505
+ }
506
+ function MarkdownView({
507
+ children,
508
+ // No default here on purpose: `buildMarkdownPipeline` normalises `trust`, and one
509
+ // definition of "the profile you get by forgetting" is the point — see `./trust`.
510
+ trust,
511
+ softBreaks = false,
512
+ capabilities,
513
+ components
514
+ }) {
515
+ const pipeline = useMemo(
516
+ () => buildMarkdownPipeline({ trust, softBreaks, capabilities, components }),
517
+ [trust, softBreaks, capabilities, components]
518
+ );
519
+ return /* @__PURE__ */ jsx(ReactMarkdown, { ...pipeline, children });
520
+ }
521
+
522
+ export { MarkdownView, baseMarkdownSchema };
523
+ //# sourceMappingURL=index.js.map
524
+ //# sourceMappingURL=index.js.map