@ai-gui/core 0.4.4 → 0.6.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/dist/index.cjs CHANGED
@@ -238,6 +238,18 @@ function sanitizeHtml(html, options = {}) {
238
238
  }
239
239
  return dompurify.default.sanitize(html);
240
240
  }
241
+ /**
242
+ * Sanitize one piece of rendered HTML, respecting a plugin's claim that it built the markup itself.
243
+ *
244
+ * Shared by every framework binding so the three of them cannot drift apart on what "sanitize"
245
+ * means — and so a diagram is not escaped into its own source text in one of them.
246
+ */
247
+ function sanitizeRenderedHtml(html, sanitize, trusted = false) {
248
+ if (sanitize === false) return html;
249
+ const options = typeof sanitize === "object" ? sanitize : void 0;
250
+ if (trusted && options?.trustPlugins !== false) return html;
251
+ return sanitizeHtml(html, options);
252
+ }
241
253
 
242
254
  //#endregion
243
255
  //#region src/debug-events.ts
@@ -1404,7 +1416,7 @@ function collectNodeRenderers(plugins = [], debugOptions = {}) {
1404
1416
  map[k] = render;
1405
1417
  continue;
1406
1418
  }
1407
- map[k] = (node) => {
1419
+ map[k] = (node, context) => {
1408
1420
  const emit = (type, data) => target?.emitDebug(type, data) ?? debug?.emit(type, data);
1409
1421
  emit("plugin-render-started", {
1410
1422
  plugin: p.name,
@@ -1412,7 +1424,7 @@ function collectNodeRenderers(plugins = [], debugOptions = {}) {
1412
1424
  nodeKey: node.key
1413
1425
  });
1414
1426
  try {
1415
- const output = render(node);
1427
+ const output = render(node, context);
1416
1428
  if (isPromise(output)) return output.then((resolved) => {
1417
1429
  emit("async-output-resolved", {
1418
1430
  plugin: p.name,
@@ -1672,6 +1684,85 @@ function normalizeLineEndings(src) {
1672
1684
  return src.replace(/\r\n|\r/g, "\n");
1673
1685
  }
1674
1686
 
1687
+ //#endregion
1688
+ //#region src/export-image.ts
1689
+ /** The intrinsic size of an SVG element, falling back to its attributes and then to a default. */
1690
+ function measure(svg, options) {
1691
+ const box = typeof svg.getBoundingClientRect === "function" ? svg.getBoundingClientRect() : void 0;
1692
+ const attribute = (name) => Number.parseFloat(svg.getAttribute(name) ?? "");
1693
+ const fromViewBox = (index) => {
1694
+ const parts = (svg.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
1695
+ return parts.length === 4 && Number.isFinite(parts[index]) ? parts[index] : Number.NaN;
1696
+ };
1697
+ const pick = (...candidates) => candidates.find((value) => Number.isFinite(value) && value > 0);
1698
+ return {
1699
+ width: Math.round(pick(options.width ?? Number.NaN, box?.width ?? Number.NaN, attribute("width"), fromViewBox(2), 720)),
1700
+ height: Math.round(pick(options.height ?? Number.NaN, box?.height ?? Number.NaN, attribute("height"), fromViewBox(3), 400))
1701
+ };
1702
+ }
1703
+ /**
1704
+ * Render an SVG element to a raster data URL.
1705
+ *
1706
+ * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
1707
+ * it in — is what gets exported.
1708
+ */
1709
+ async function exportSVGToImage(svg, options = {}) {
1710
+ const { width, height } = measure(svg, options);
1711
+ const scale = options.scale && options.scale > 0 ? options.scale : globalThis.devicePixelRatio || 1;
1712
+ const source = new XMLSerializer().serializeToString(svg);
1713
+ const namespaced = /\sxmlns=/.test(source) ? source : source.replace(/^<svg/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
1714
+ const url = URL.createObjectURL(new Blob([namespaced], { type: "image/svg+xml;charset=utf-8" }));
1715
+ try {
1716
+ const image = await loadImage(url);
1717
+ const canvas = document.createElement("canvas");
1718
+ canvas.width = Math.max(1, Math.round(width * scale));
1719
+ canvas.height = Math.max(1, Math.round(height * scale));
1720
+ const context = canvas.getContext("2d");
1721
+ if (!context) throw new Error("This browser cannot rasterize images");
1722
+ const background = options.background ?? "#ffffff";
1723
+ if (background !== "transparent") {
1724
+ context.fillStyle = background;
1725
+ context.fillRect(0, 0, canvas.width, canvas.height);
1726
+ }
1727
+ context.scale(scale, scale);
1728
+ context.drawImage(image, 0, 0, width, height);
1729
+ return {
1730
+ dataUrl: canvas.toDataURL(options.type ?? "image/png", options.quality),
1731
+ width: canvas.width,
1732
+ height: canvas.height
1733
+ };
1734
+ } finally {
1735
+ URL.revokeObjectURL(url);
1736
+ }
1737
+ }
1738
+ /**
1739
+ * Export every drawing inside a rendered answer.
1740
+ *
1741
+ * A host holds the element it handed to the renderer, not the individual charts, and the plugins
1742
+ * decide what lands in it.
1743
+ */
1744
+ async function exportRenderedImages(root, options = {}) {
1745
+ const drawings = Array.from(root.querySelectorAll("svg"));
1746
+ const exported = [];
1747
+ for (const drawing of drawings) exported.push(await exportSVGToImage(drawing, options));
1748
+ return exported;
1749
+ }
1750
+ /** Save an exported image under the given file name. */
1751
+ function downloadImage(image, filename) {
1752
+ const link = document.createElement("a");
1753
+ link.download = filename;
1754
+ link.href = image.dataUrl;
1755
+ link.click();
1756
+ }
1757
+ function loadImage(url) {
1758
+ return new Promise((resolve, reject) => {
1759
+ const image = new Image();
1760
+ image.onload = () => resolve(image);
1761
+ image.onerror = () => reject(new Error("The drawing could not be decoded"));
1762
+ image.src = url;
1763
+ });
1764
+ }
1765
+
1675
1766
  //#endregion
1676
1767
  //#region src/diff.ts
1677
1768
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
@@ -2550,6 +2641,9 @@ exports.createActionRuntime = createActionRuntime
2550
2641
  exports.createParser = createParser
2551
2642
  exports.createParserWithMetadata = createParserWithMetadata
2552
2643
  exports.diffAst = diffAst
2644
+ exports.downloadImage = downloadImage
2645
+ exports.exportRenderedImages = exportRenderedImages
2646
+ exports.exportSVGToImage = exportSVGToImage
2553
2647
  exports.getActionKey = getActionKey
2554
2648
  exports.getIdleActionState = getIdleActionState
2555
2649
  exports.isCardPatchResult = isCardPatchResult
@@ -2563,5 +2657,6 @@ exports.readableBytes = readableBytes
2563
2657
  exports.repairMarkdown = repairMarkdown
2564
2658
  exports.safeDebugValue = safeDebugValue
2565
2659
  exports.sanitizeHtml = sanitizeHtml
2660
+ exports.sanitizeRenderedHtml = sanitizeRenderedHtml
2566
2661
  exports.textLines = textLines
2567
2662
  exports.validateJSONSchema = validateJSONSchema
package/dist/index.d.cts CHANGED
@@ -36,6 +36,12 @@ interface SanitizeHtmlOptions {
36
36
  sanitizer?: (html: string) => string;
37
37
  /** Bare SSR either escapes all markup (default) or fails explicitly. */
38
38
  ssr?: "escape" | "throw";
39
+ /**
40
+ * Whether a plugin may declare its own markup safe, which it does by returning `trusted: true`.
41
+ * On by default: a plugin is code the host installed, and sanitizing the SVG it drew would
42
+ * escape it into text. Set to `false` to sanitize every output whatever its origin.
43
+ */
44
+ trustPlugins?: boolean;
39
45
  }
40
46
  /**
41
47
  * Sanitize an HTML string, stripping scripts and unsafe attributes.
@@ -46,6 +52,15 @@ interface SanitizeHtmlOptions {
46
52
  * markup and never throws.
47
53
  */
48
54
  declare function sanitizeHtml(html: string, options?: SanitizeHtmlOptions): string;
55
+ /** What a renderer was told to do about sanitizing: nothing, the default, or these options. */
56
+ type SanitizeSetting = boolean | SanitizeHtmlOptions | undefined;
57
+ /**
58
+ * Sanitize one piece of rendered HTML, respecting a plugin's claim that it built the markup itself.
59
+ *
60
+ * Shared by every framework binding so the three of them cannot drift apart on what "sanitize"
61
+ * means — and so a diagram is not escaped into its own source text in one of them.
62
+ */
63
+ declare function sanitizeRenderedHtml(html: string, sanitize: SanitizeSetting, trusted?: boolean): string;
49
64
 
50
65
  //#endregion
51
66
  //#region src/debug-events.d.ts
@@ -139,9 +154,21 @@ type Patch = {
139
154
  key: string;
140
155
  };
141
156
  /** Framework-neutral render descriptor returned by plugin node renderers. */
142
- type RenderOutput = {
157
+ type RenderOutput =
158
+ /**
159
+ * `trusted` marks markup the plugin built itself rather than markup taken from the model.
160
+ *
161
+ * A plugin that renders a diagram returns SVG, and sanitizing SVG escapes it — the reader gets
162
+ * the source text instead of the picture. Hosts worked around that by matching the plugin's
163
+ * internal id prefix with a regular expression, which breaks the moment the plugin renames its
164
+ * ids and lets any model output wearing that prefix through unsanitized. A plugin is code the
165
+ * host chose to install, so it can say so itself; a host that disagrees sets
166
+ * `sanitize: { trustPlugins: false }`.
167
+ */
168
+ {
143
169
  kind: "html";
144
170
  html: string;
171
+ trusted?: boolean;
145
172
  } | {
146
173
  kind: "element";
147
174
  tag: string;
@@ -166,7 +193,18 @@ interface MountedCardSlot {
166
193
  interface RenderMountContext {
167
194
  mountCard?: (host: HTMLElement, request: MountCardSlotRequest) => MountedCardSlot | undefined;
168
195
  }
169
- type NodeRenderer = (node: ASTNode) => RenderOutput | Promise<RenderOutput>;
196
+ /** What the host can tell a plugin about the surroundings it is rendering into. */
197
+ interface NodeRenderContext {
198
+ /**
199
+ * The host's colour scheme, "light" or "dark" by convention.
200
+ *
201
+ * A diagram or a chart picks its own palette, and a plugin has no way to read the palette of
202
+ * the page it is embedded in, so without this an answer rendered on a dark page comes back
203
+ * with white plot areas.
204
+ */
205
+ readonly theme?: string;
206
+ }
207
+ type NodeRenderer = (node: ASTNode, context?: NodeRenderContext) => RenderOutput | Promise<RenderOutput>;
170
208
  interface PluginCommitContext {
171
209
  readonly generation: number;
172
210
  emitDebug(type: string, data?: Record<string, unknown>): void;
@@ -550,6 +588,57 @@ declare function collectNodeRenderers(plugins?: AIGuiPlugin[], debugOptions?: Co
550
588
  /** The set of node types claimed by the given plugins. */
551
589
  declare function pluginNodeTypes(plugins?: AIGuiPlugin[]): Set<string>;
552
590
 
591
+ //#endregion
592
+ //#region src/export-image.d.ts
593
+ /**
594
+ * Turn what a plugin drew into a PNG the reader can keep.
595
+ *
596
+ * Charts and diagrams are the point of rendering a model's answer, and the first thing anyone
597
+ * wants to do with one is save it. There is no browser API for "download this SVG as an image":
598
+ * it has to be serialized, loaded through an `Image`, and painted onto a canvas. Every host that
599
+ * needs it rewrites those twenty lines, and gets the same details wrong — a transparent
600
+ * background that turns black in a viewer, a blurry export on a retina screen, or a light
601
+ * background hardcoded into a page that has since gone dark.
602
+ */
603
+ interface ExportImageOptions {
604
+ /**
605
+ * Device pixels per CSS pixel. Defaults to the screen's own ratio, so an export looks as sharp
606
+ * as what it was copied from rather than half the resolution on a retina display.
607
+ */
608
+ scale?: number;
609
+ /**
610
+ * What to paint behind the drawing. An SVG is usually transparent, which reads as black in most
611
+ * image viewers. Pass the page's own background — or "transparent" to keep the alpha channel.
612
+ */
613
+ background?: string;
614
+ /** Overrides for the drawing's own size, in CSS pixels. */
615
+ width?: number;
616
+ height?: number;
617
+ type?: "image/png" | "image/jpeg" | "image/webp";
618
+ quality?: number;
619
+ }
620
+ interface ExportedImage {
621
+ dataUrl: string;
622
+ width: number;
623
+ height: number;
624
+ }
625
+ /**
626
+ * Render an SVG element to a raster data URL.
627
+ *
628
+ * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
629
+ * it in — is what gets exported.
630
+ */
631
+ declare function exportSVGToImage(svg: SVGElement, options?: ExportImageOptions): Promise<ExportedImage>;
632
+ /**
633
+ * Export every drawing inside a rendered answer.
634
+ *
635
+ * A host holds the element it handed to the renderer, not the individual charts, and the plugins
636
+ * decide what lands in it.
637
+ */
638
+ declare function exportRenderedImages(root: ParentNode, options?: ExportImageOptions): Promise<ExportedImage[]>;
639
+ /** Save an exported image under the given file name. */
640
+ declare function downloadImage(image: ExportedImage, filename: string): void;
641
+
553
642
  //#endregion
554
643
  //#region src/diff.d.ts
555
644
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
@@ -694,4 +783,4 @@ declare function mockModelStream(events: Iterable<ModelStreamEvent> | AsyncItera
694
783
  declare function readableBytes(chunks: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>): ReadableStream<Uint8Array>;
695
784
 
696
785
  //#endregion
697
- export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderer, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SourceBlock, StreamParseOptions, StreamRouter, Usage, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
786
+ export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SanitizeSetting, SourceBlock, StreamParseOptions, StreamRouter, Usage, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, validateJSONSchema };
package/dist/index.d.ts CHANGED
@@ -36,6 +36,12 @@ interface SanitizeHtmlOptions {
36
36
  sanitizer?: (html: string) => string;
37
37
  /** Bare SSR either escapes all markup (default) or fails explicitly. */
38
38
  ssr?: "escape" | "throw";
39
+ /**
40
+ * Whether a plugin may declare its own markup safe, which it does by returning `trusted: true`.
41
+ * On by default: a plugin is code the host installed, and sanitizing the SVG it drew would
42
+ * escape it into text. Set to `false` to sanitize every output whatever its origin.
43
+ */
44
+ trustPlugins?: boolean;
39
45
  }
40
46
  /**
41
47
  * Sanitize an HTML string, stripping scripts and unsafe attributes.
@@ -46,6 +52,15 @@ interface SanitizeHtmlOptions {
46
52
  * markup and never throws.
47
53
  */
48
54
  declare function sanitizeHtml(html: string, options?: SanitizeHtmlOptions): string;
55
+ /** What a renderer was told to do about sanitizing: nothing, the default, or these options. */
56
+ type SanitizeSetting = boolean | SanitizeHtmlOptions | undefined;
57
+ /**
58
+ * Sanitize one piece of rendered HTML, respecting a plugin's claim that it built the markup itself.
59
+ *
60
+ * Shared by every framework binding so the three of them cannot drift apart on what "sanitize"
61
+ * means — and so a diagram is not escaped into its own source text in one of them.
62
+ */
63
+ declare function sanitizeRenderedHtml(html: string, sanitize: SanitizeSetting, trusted?: boolean): string;
49
64
 
50
65
  //#endregion
51
66
  //#region src/debug-events.d.ts
@@ -139,9 +154,21 @@ type Patch = {
139
154
  key: string;
140
155
  };
141
156
  /** Framework-neutral render descriptor returned by plugin node renderers. */
142
- type RenderOutput = {
157
+ type RenderOutput =
158
+ /**
159
+ * `trusted` marks markup the plugin built itself rather than markup taken from the model.
160
+ *
161
+ * A plugin that renders a diagram returns SVG, and sanitizing SVG escapes it — the reader gets
162
+ * the source text instead of the picture. Hosts worked around that by matching the plugin's
163
+ * internal id prefix with a regular expression, which breaks the moment the plugin renames its
164
+ * ids and lets any model output wearing that prefix through unsanitized. A plugin is code the
165
+ * host chose to install, so it can say so itself; a host that disagrees sets
166
+ * `sanitize: { trustPlugins: false }`.
167
+ */
168
+ {
143
169
  kind: "html";
144
170
  html: string;
171
+ trusted?: boolean;
145
172
  } | {
146
173
  kind: "element";
147
174
  tag: string;
@@ -166,7 +193,18 @@ interface MountedCardSlot {
166
193
  interface RenderMountContext {
167
194
  mountCard?: (host: HTMLElement, request: MountCardSlotRequest) => MountedCardSlot | undefined;
168
195
  }
169
- type NodeRenderer = (node: ASTNode) => RenderOutput | Promise<RenderOutput>;
196
+ /** What the host can tell a plugin about the surroundings it is rendering into. */
197
+ interface NodeRenderContext {
198
+ /**
199
+ * The host's colour scheme, "light" or "dark" by convention.
200
+ *
201
+ * A diagram or a chart picks its own palette, and a plugin has no way to read the palette of
202
+ * the page it is embedded in, so without this an answer rendered on a dark page comes back
203
+ * with white plot areas.
204
+ */
205
+ readonly theme?: string;
206
+ }
207
+ type NodeRenderer = (node: ASTNode, context?: NodeRenderContext) => RenderOutput | Promise<RenderOutput>;
170
208
  interface PluginCommitContext {
171
209
  readonly generation: number;
172
210
  emitDebug(type: string, data?: Record<string, unknown>): void;
@@ -550,6 +588,57 @@ declare function collectNodeRenderers(plugins?: AIGuiPlugin[], debugOptions?: Co
550
588
  /** The set of node types claimed by the given plugins. */
551
589
  declare function pluginNodeTypes(plugins?: AIGuiPlugin[]): Set<string>;
552
590
 
591
+ //#endregion
592
+ //#region src/export-image.d.ts
593
+ /**
594
+ * Turn what a plugin drew into a PNG the reader can keep.
595
+ *
596
+ * Charts and diagrams are the point of rendering a model's answer, and the first thing anyone
597
+ * wants to do with one is save it. There is no browser API for "download this SVG as an image":
598
+ * it has to be serialized, loaded through an `Image`, and painted onto a canvas. Every host that
599
+ * needs it rewrites those twenty lines, and gets the same details wrong — a transparent
600
+ * background that turns black in a viewer, a blurry export on a retina screen, or a light
601
+ * background hardcoded into a page that has since gone dark.
602
+ */
603
+ interface ExportImageOptions {
604
+ /**
605
+ * Device pixels per CSS pixel. Defaults to the screen's own ratio, so an export looks as sharp
606
+ * as what it was copied from rather than half the resolution on a retina display.
607
+ */
608
+ scale?: number;
609
+ /**
610
+ * What to paint behind the drawing. An SVG is usually transparent, which reads as black in most
611
+ * image viewers. Pass the page's own background — or "transparent" to keep the alpha channel.
612
+ */
613
+ background?: string;
614
+ /** Overrides for the drawing's own size, in CSS pixels. */
615
+ width?: number;
616
+ height?: number;
617
+ type?: "image/png" | "image/jpeg" | "image/webp";
618
+ quality?: number;
619
+ }
620
+ interface ExportedImage {
621
+ dataUrl: string;
622
+ width: number;
623
+ height: number;
624
+ }
625
+ /**
626
+ * Render an SVG element to a raster data URL.
627
+ *
628
+ * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
629
+ * it in — is what gets exported.
630
+ */
631
+ declare function exportSVGToImage(svg: SVGElement, options?: ExportImageOptions): Promise<ExportedImage>;
632
+ /**
633
+ * Export every drawing inside a rendered answer.
634
+ *
635
+ * A host holds the element it handed to the renderer, not the individual charts, and the plugins
636
+ * decide what lands in it.
637
+ */
638
+ declare function exportRenderedImages(root: ParentNode, options?: ExportImageOptions): Promise<ExportedImage[]>;
639
+ /** Save an exported image under the given file name. */
640
+ declare function downloadImage(image: ExportedImage, filename: string): void;
641
+
553
642
  //#endregion
554
643
  //#region src/diff.d.ts
555
644
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
@@ -694,4 +783,4 @@ declare function mockModelStream(events: Iterable<ModelStreamEvent> | AsyncItera
694
783
  declare function readableBytes(chunks: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>): ReadableStream<Uint8Array>;
695
784
 
696
785
  //#endregion
697
- export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderer, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SourceBlock, StreamParseOptions, StreamRouter, Usage, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
786
+ export { AIGuiPlugin, ASTNode, ActionAbortedError, ActionAlreadyRegisteredError, ActionContext, ActionDefinition, ActionDestroyedError, ActionDispatchOptions, ActionErrorEvent, ActionEventBase, ActionExecutionError, ActionNotFoundError, ActionRegisterOptions, ActionRegistry, ActionRequest, ActionRuntime, ActionRuntimeError, ActionRuntimeOptions, ActionStartEvent, ActionState, ActionStateListener, ActionStatus, ActionSuccessEvent, ActionTimeoutError, ActionValidationError, BuildSystemPromptOptions, ByteStreamSource, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardAction, CardActionError, CardDef, CardJSONError, CardLimitError, CardListener, CardNotFoundError, CardParseResult, CardPatch, CardPatchBatch, CardPatchResult, CardRecord, CardRegistry, CardRevisionConflictError, CardSnapshot, CardSnapshotError, CardStore, CardStoreError, CardStoreListener, CardStoreOptions, CardTypeConflictError, CardValidationError, ChannelSink, Citation, CollectNodeRendererOptions, DebugEmitter, DebugEvent, DebugEventListener, DebugEventTarget, DebugInstrumentationTarget, DebugOptions, DebugRedactContext, DebugSource, ExportImageOptions, ExportedImage, FeedChunk, FeedOptions, FeedSource, JSONSchema, JSONSchemaValidationResult, ModelStreamEvent, MountCardSlotRequest, MountedCardSlot, NodeRenderContext, NodeRenderer, ParseResult, ParserOptions, PartialJSONResult, Patch, PluginCommitContext, RenderMountContext, RenderOutput, Renderer, RendererOptions, SSEEvent, SSEOptions, SafeDebugValueOptions, SanitizeHtmlOptions, SanitizeSetting, SourceBlock, StreamParseOptions, StreamRouter, Usage, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, validateJSONSchema };
package/dist/index.js CHANGED
@@ -214,6 +214,18 @@ function sanitizeHtml(html, options = {}) {
214
214
  }
215
215
  return DOMPurify.sanitize(html);
216
216
  }
217
+ /**
218
+ * Sanitize one piece of rendered HTML, respecting a plugin's claim that it built the markup itself.
219
+ *
220
+ * Shared by every framework binding so the three of them cannot drift apart on what "sanitize"
221
+ * means — and so a diagram is not escaped into its own source text in one of them.
222
+ */
223
+ function sanitizeRenderedHtml(html, sanitize, trusted = false) {
224
+ if (sanitize === false) return html;
225
+ const options = typeof sanitize === "object" ? sanitize : void 0;
226
+ if (trusted && options?.trustPlugins !== false) return html;
227
+ return sanitizeHtml(html, options);
228
+ }
217
229
 
218
230
  //#endregion
219
231
  //#region src/debug-events.ts
@@ -1380,7 +1392,7 @@ function collectNodeRenderers(plugins = [], debugOptions = {}) {
1380
1392
  map[k] = render;
1381
1393
  continue;
1382
1394
  }
1383
- map[k] = (node) => {
1395
+ map[k] = (node, context) => {
1384
1396
  const emit = (type, data) => target?.emitDebug(type, data) ?? debug?.emit(type, data);
1385
1397
  emit("plugin-render-started", {
1386
1398
  plugin: p.name,
@@ -1388,7 +1400,7 @@ function collectNodeRenderers(plugins = [], debugOptions = {}) {
1388
1400
  nodeKey: node.key
1389
1401
  });
1390
1402
  try {
1391
- const output = render(node);
1403
+ const output = render(node, context);
1392
1404
  if (isPromise(output)) return output.then((resolved) => {
1393
1405
  emit("async-output-resolved", {
1394
1406
  plugin: p.name,
@@ -1648,6 +1660,85 @@ function normalizeLineEndings(src) {
1648
1660
  return src.replace(/\r\n|\r/g, "\n");
1649
1661
  }
1650
1662
 
1663
+ //#endregion
1664
+ //#region src/export-image.ts
1665
+ /** The intrinsic size of an SVG element, falling back to its attributes and then to a default. */
1666
+ function measure(svg, options) {
1667
+ const box = typeof svg.getBoundingClientRect === "function" ? svg.getBoundingClientRect() : void 0;
1668
+ const attribute = (name) => Number.parseFloat(svg.getAttribute(name) ?? "");
1669
+ const fromViewBox = (index) => {
1670
+ const parts = (svg.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
1671
+ return parts.length === 4 && Number.isFinite(parts[index]) ? parts[index] : Number.NaN;
1672
+ };
1673
+ const pick = (...candidates) => candidates.find((value) => Number.isFinite(value) && value > 0);
1674
+ return {
1675
+ width: Math.round(pick(options.width ?? Number.NaN, box?.width ?? Number.NaN, attribute("width"), fromViewBox(2), 720)),
1676
+ height: Math.round(pick(options.height ?? Number.NaN, box?.height ?? Number.NaN, attribute("height"), fromViewBox(3), 400))
1677
+ };
1678
+ }
1679
+ /**
1680
+ * Render an SVG element to a raster data URL.
1681
+ *
1682
+ * The element is serialized as it stands, so whatever a plugin drew — including the theme it drew
1683
+ * it in — is what gets exported.
1684
+ */
1685
+ async function exportSVGToImage(svg, options = {}) {
1686
+ const { width, height } = measure(svg, options);
1687
+ const scale = options.scale && options.scale > 0 ? options.scale : globalThis.devicePixelRatio || 1;
1688
+ const source = new XMLSerializer().serializeToString(svg);
1689
+ const namespaced = /\sxmlns=/.test(source) ? source : source.replace(/^<svg/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
1690
+ const url = URL.createObjectURL(new Blob([namespaced], { type: "image/svg+xml;charset=utf-8" }));
1691
+ try {
1692
+ const image = await loadImage(url);
1693
+ const canvas = document.createElement("canvas");
1694
+ canvas.width = Math.max(1, Math.round(width * scale));
1695
+ canvas.height = Math.max(1, Math.round(height * scale));
1696
+ const context = canvas.getContext("2d");
1697
+ if (!context) throw new Error("This browser cannot rasterize images");
1698
+ const background = options.background ?? "#ffffff";
1699
+ if (background !== "transparent") {
1700
+ context.fillStyle = background;
1701
+ context.fillRect(0, 0, canvas.width, canvas.height);
1702
+ }
1703
+ context.scale(scale, scale);
1704
+ context.drawImage(image, 0, 0, width, height);
1705
+ return {
1706
+ dataUrl: canvas.toDataURL(options.type ?? "image/png", options.quality),
1707
+ width: canvas.width,
1708
+ height: canvas.height
1709
+ };
1710
+ } finally {
1711
+ URL.revokeObjectURL(url);
1712
+ }
1713
+ }
1714
+ /**
1715
+ * Export every drawing inside a rendered answer.
1716
+ *
1717
+ * A host holds the element it handed to the renderer, not the individual charts, and the plugins
1718
+ * decide what lands in it.
1719
+ */
1720
+ async function exportRenderedImages(root, options = {}) {
1721
+ const drawings = Array.from(root.querySelectorAll("svg"));
1722
+ const exported = [];
1723
+ for (const drawing of drawings) exported.push(await exportSVGToImage(drawing, options));
1724
+ return exported;
1725
+ }
1726
+ /** Save an exported image under the given file name. */
1727
+ function downloadImage(image, filename) {
1728
+ const link = document.createElement("a");
1729
+ link.download = filename;
1730
+ link.href = image.dataUrl;
1731
+ link.click();
1732
+ }
1733
+ function loadImage(url) {
1734
+ return new Promise((resolve, reject) => {
1735
+ const image = new Image();
1736
+ image.onload = () => resolve(image);
1737
+ image.onerror = () => reject(new Error("The drawing could not be decoded"));
1738
+ image.src = url;
1739
+ });
1740
+ }
1741
+
1651
1742
  //#endregion
1652
1743
  //#region src/diff.ts
1653
1744
  /** Produce a minimal set of patches turning `prev` into `next`, keyed by node key. */
@@ -2491,4 +2582,4 @@ function delay(ms, signal) {
2491
2582
  }
2492
2583
 
2493
2584
  //#endregion
2494
- export { ActionAbortedError, ActionAlreadyRegisteredError, ActionDestroyedError, ActionExecutionError, ActionNotFoundError, ActionRegistry, ActionRuntime, ActionRuntimeError, ActionTimeoutError, ActionValidationError, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardJSONError, CardLimitError, CardNotFoundError, CardRegistry, CardRevisionConflictError, CardSnapshotError, CardStore, CardStoreError, CardTypeConflictError, CardValidationError, DebugEmitter, Renderer, StreamRouter, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, textLines, validateJSONSchema };
2585
+ export { ActionAbortedError, ActionAlreadyRegisteredError, ActionDestroyedError, ActionExecutionError, ActionNotFoundError, ActionRegistry, ActionRuntime, ActionRuntimeError, ActionTimeoutError, ActionValidationError, CARD_ID_MAX_LENGTH, CARD_JSON_MAX_DEPTH, CARD_JSON_MAX_NODES, CARD_PATCH_BATCH_MAX_SIZE, CardJSONError, CardLimitError, CardNotFoundError, CardRegistry, CardRevisionConflictError, CardSnapshotError, CardStore, CardStoreError, CardTypeConflictError, CardValidationError, DebugEmitter, Renderer, StreamRouter, applyPatches, buildSystemPrompt, collectNodeRenderers, contentDeltas, createActionRuntime, createParser, createParserWithMetadata, diffAst, downloadImage, exportRenderedImages, exportSVGToImage, getActionKey, getIdleActionState, isCardPatchResult, jsonLines, mockModelStream, ndjson, parsePartialJSON, parseSSE, pluginNodeTypes, readableBytes, repairMarkdown, safeDebugValue, sanitizeHtml, sanitizeRenderedHtml, textLines, validateJSONSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/core",
3
- "version": "0.4.4",
3
+ "version": "0.6.0",
4
4
  "description": "Headless streaming renderer core for LLM-generated UI — markdown/JSON repair, card registry, AST diff, plugin engine, sanitizer.",
5
5
  "keywords": [
6
6
  "llm",