@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 +97 -2
- package/dist/index.d.cts +92 -3
- package/dist/index.d.ts +92 -3
- package/dist/index.js +94 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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