@slexkit/streamdown 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SlexKit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # @slexkit/streamdown
2
+
3
+ Streamdown custom renderer for Slex fenced UI blocks.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install slexkit @slexkit/theme-shadcn @slexkit/streamdown streamdown react react-dom
9
+ ```
10
+
11
+ Import both style sheets in your app:
12
+
13
+ ```ts
14
+ import "@slexkit/theme-shadcn/style.css";
15
+ import "@slexkit/streamdown/style.css";
16
+ ```
17
+
18
+ ## Minimal Usage
19
+
20
+ ```tsx
21
+ import { Streamdown } from "streamdown";
22
+ import { slexkitRenderer } from "@slexkit/streamdown";
23
+
24
+ export function Message({ markdown }: { markdown: string }) {
25
+ return (
26
+ <Streamdown plugins={{ renderers: [slexkitRenderer] }}>
27
+ {markdown}
28
+ </Streamdown>
29
+ );
30
+ }
31
+ ```
32
+
33
+ ## Vercel AI SDK
34
+
35
+ ```tsx
36
+ import { useChat } from "@ai-sdk/react";
37
+ import { Streamdown } from "streamdown";
38
+ import { slexkitRenderer } from "@slexkit/streamdown";
39
+
40
+ export function Chat() {
41
+ const { messages, status } = useChat();
42
+
43
+ return messages.map((message) =>
44
+ message.parts.map((part, index) =>
45
+ part.type === "text" ? (
46
+ <Streamdown
47
+ key={index}
48
+ isAnimating={status === "streaming"}
49
+ plugins={{ renderers: [slexkitRenderer] }}
50
+ >
51
+ {part.text}
52
+ </Streamdown>
53
+ ) : null,
54
+ ),
55
+ );
56
+ }
57
+ ```
58
+
59
+ ## Model Output
60
+
61
+ Ask the model to emit Slex only in explicit `slex` fenced blocks. The body is JavaScript-compatible Slex source: a plain object literal with native JS state and expression strings.
62
+
63
+ ````md
64
+ ```slex
65
+ {
66
+ namespace: "status_demo",
67
+ g: {},
68
+ layout: {
69
+ "text:status": { text: "3/4 checks complete" }
70
+ }
71
+ }
72
+ ```
73
+
74
+ **Status:** 3/4 checks complete
75
+ ````
76
+
77
+ The renderer intentionally handles only `slex` fences by default. It does not sniff generic JavaScript code blocks.
78
+
79
+ To show the same Slex source inside the embedded playground UI, keep the `slex` fence and switch the renderer mode with fence meta:
80
+
81
+ ````md
82
+ ```slex render="playground" title="Editable demo"
83
+ {
84
+ namespace: "editable_demo",
85
+ layout: {
86
+ "text:status": { text: "Rendered inside the playground" }
87
+ }
88
+ }
89
+ ```
90
+ ````
91
+
92
+ You can also make playground display the default for a Markdown surface:
93
+
94
+ ```tsx
95
+ const renderer = createSlexKitRenderer({ renderMode: "playground" });
96
+ ```
97
+
98
+ ### Fence meta resolution
99
+
100
+ Fence meta values resolve to the render mode via a flexible parser:
101
+
102
+ | Meta value | Resolved mode |
103
+ |------------|--------------|
104
+ | `render="playground"`, `as="playground"`, `mode="playground"` | `playground` |
105
+ | `render="editor"`, `render="workbench"` | `playground` |
106
+ | `render="component"`, `as="component"`, `as="preview"` | `component` |
107
+ | No meta | Fallback to `renderMode` option |
108
+
109
+ Playground meta also supports `title`, `mode`, `height`/`previewMinHeight`, `webUrl`/`playgroundUrl`, and `pluginVersion`.
110
+
111
+ ## Streaming behavior
112
+
113
+ During streaming (`isIncomplete: true`), the renderer shows a placeholder instead of attempting to parse partial Slex source. The placeholder defaults to "Rendering SlexKit..." and can be customized:
114
+
115
+ ```tsx
116
+ <SlexKitRenderer
117
+ code={code}
118
+ isIncomplete={true}
119
+ placeholder={<Spinner />}
120
+ />
121
+ ```
122
+
123
+ State-only blocks (source with `namespace` and `g` but no renderable `layout`) are processed via `ingest()` and return `null` from the component — they update state silently without rendering DOM.
124
+
125
+ ## Trust Boundary
126
+
127
+ Slex source is JavaScript-compatible object literal syntax with runtime expressions. This package is intended for trusted or controlled model output. Do not treat it as a sandbox for arbitrary third-party Markdown.
128
+
129
+ For untrusted model output, opt into the secure runtime. The renderer will pass the fence source text into a sandbox frame and route sensitive capabilities through host policy enforced `api.*` methods:
130
+
131
+ ```tsx
132
+ import { createSlexKitRenderer } from "@slexkit/streamdown";
133
+ import "@slexkit/theme-shadcn/style.css";
134
+ import "@slexkit/streamdown/style.css";
135
+
136
+ const renderer = createSlexKitRenderer({
137
+ runtime: "secure",
138
+ securePolicy: {
139
+ network: {
140
+ enabled: true,
141
+ methods: ["GET", "POST"],
142
+ allowOrigins: ["https://api.example.com"],
143
+ credentials: "omit",
144
+ timeoutMs: 15000,
145
+ maxBodyBytes: 4096,
146
+ },
147
+ timer: { enabled: true, maxTimers: 8, minIntervalMs: 16 },
148
+ animation: { enabled: true },
149
+ canvas: { enabled: true, maxCanvases: 4, maxPixels: 1048576, allowedContexts: ["2d"] },
150
+ execution: { heartbeatIntervalMs: 1000, maxUnresponsiveMs: 5000 },
151
+ },
152
+ secureFrame: {
153
+ runtimeUrl: "/slexkit.runtime.js",
154
+ loadTimeoutMs: 8000,
155
+ },
156
+ });
157
+ ```
158
+
159
+ The secure renderer uses the main `slexkit.runtime.js` module as its iframe runner. You do not need to ship a second runner file.
160
+
161
+ Copy the runtime file into your public static directory:
162
+
163
+ ```sh
164
+ npx -y slexkit copy-runtime public/slexkit.runtime.js
165
+ ```
166
+
167
+ The file must be served by your app or CDN with these response headers:
168
+
169
+ ```http
170
+ Access-Control-Allow-Origin: *
171
+ Content-Type: text/javascript
172
+ ```
173
+
174
+ This header is configured in your deployment layer, not inside `createSlexKitRenderer()`. Secure iframes intentionally run with an opaque origin, so the browser treats `runtimeUrl` as a cross-origin ES module import even when the URL is on your own site. Do not add `allow-same-origin` to the iframe sandbox to bypass this; that weakens isolation.
175
+
176
+ If the iframe cannot load or mount the runtime, SlexKit renders a `role="alert"` diagnostic next to the iframe and logs the same message to `console.error`. Tune the diagnostic delay with `secureFrame.loadTimeoutMs`.
177
+
178
+ For the full trust boundary and capability contract, see the root package security runtime reference.
179
+
180
+ Examples:
181
+
182
+ ```js
183
+ // next.config.js
184
+ export default {
185
+ async headers() {
186
+ return [
187
+ {
188
+ source: "/slexkit.runtime.js",
189
+ headers: [
190
+ { key: "Access-Control-Allow-Origin", value: "*" },
191
+ { key: "Content-Type", value: "text/javascript" },
192
+ ],
193
+ },
194
+ ];
195
+ },
196
+ };
197
+ ```
198
+
199
+ ```text
200
+ # Netlify / Cloudflare Pages _headers
201
+ /slexkit.runtime.js
202
+ Access-Control-Allow-Origin: *
203
+ Content-Type: text/javascript
204
+ ```
205
+
206
+ If your Markdown surface already owns a page-level SlexKit runtime, pass it into the renderer so block mount/unmount events are delegated to that host:
207
+
208
+ ```tsx
209
+ import {
210
+ createSlexKitMarkdownRuntimeHost,
211
+ } from "slexkit";
212
+ import { createSlexKitRenderer } from "@slexkit/streamdown";
213
+
214
+ const runtimeHost = createSlexKitMarkdownRuntimeHost({
215
+ mode: "secure",
216
+ policy,
217
+ secureFrame: {
218
+ runtimeUrl: "/slexkit.runtime.js",
219
+ },
220
+ });
221
+
222
+ const renderer = createSlexKitRenderer({
223
+ runtimeHost,
224
+ });
225
+ ```
226
+
227
+ ## Custom Renderer Options
228
+
229
+ ```tsx
230
+ import { createSlexKitRenderer } from "@slexkit/streamdown";
231
+
232
+ export const renderer = createSlexKitRenderer({
233
+ languages: ["slex"], // Fence language tags to process (default: ["slex"])
234
+ renderMode: "component", // "component" | "playground"
235
+ runtime: "trusted", // "trusted" | "secure"
236
+ domain: "my-app", // Namespace prefix for blocks
237
+ showChrome: true, // Show CodeBlockContainer + toolbar (default: true)
238
+ showSource: false, // Show source code in a details element
239
+ className: "my-block", // Extra CSS class on wrapper
240
+ useGlobalRuntimeHost: false, // Use global markdown runtime host
241
+ playgroundUrl: "/playground.html", // Custom playground iframe URL
242
+ placeholder: null, // ReactNode shown during streaming
243
+ onError(error, code) {
244
+ console.error("Failed to render SlexKit", error, code);
245
+ },
246
+ });
247
+ ```
248
+
249
+ ### Standalone component usage
250
+
251
+ `SlexKitRenderer` is also exported for direct use without Streamdown:
252
+
253
+ ```tsx
254
+ import { SlexKitRenderer } from "@slexkit/streamdown";
255
+
256
+ function MyBlock() {
257
+ return (
258
+ <SlexKitRenderer
259
+ code={`{
260
+ namespace: "inline",
261
+ g: {},
262
+ layout: { "text:hello": { text: "Hello" } }
263
+ }`}
264
+ language="slex"
265
+ renderMode="component"
266
+ />
267
+ );
268
+ }
269
+ ```
270
+
271
+ ## Error handling
272
+
273
+ Parse errors display a diagnostic panel with line, column, excerpt, and detail. Runtime mount errors render a similar alert. The `onError` callback fires for both cases, receiving the error object and the source code string.
274
+
275
+ ## Documentation
276
+
277
+ - [SlexKit Security runtime](https://github.com/slexkit/slexkit/blob/main/site/content/reference/security/en-US.md)
278
+ - [SlexKit Host integration](https://github.com/slexkit/slexkit/blob/main/site/content/reference/integration/en-US.md)
279
+ - [SlexKit Usage guide](https://github.com/slexkit/slexkit/blob/main/site/content/reference/usage/en-US.md)
280
+
281
+ ## Release validation
282
+
283
+ The package is considered release-ready when the repository checks pass:
284
+
285
+ ```sh
286
+ bun run build
287
+ bun run test
288
+ bun run smoke:release
289
+ ```
290
+
291
+ Those checks verify the public renderer exports, `style.css` subpath export, trusted rendering, secure frame rendering, runtime-host delegation, state-only fences, domain isolation, embedded playground mode, and package version sync.
@@ -0,0 +1,29 @@
1
+ import { type ReactNode } from "react";
2
+ import { type CustomRenderer, type CustomRendererProps } from "streamdown";
3
+ import { type SlexKitMarkdownRuntimeHost, type HostRuntimeAdapter, type HostRuntimePolicy, type SecureFrameOptions } from "slexkit";
4
+ export type SlexKitRendererOptions = {
5
+ languages?: string | string[];
6
+ domain?: string;
7
+ renderMode?: "component" | "playground";
8
+ runtime?: "trusted" | "secure";
9
+ runtimeHost?: SlexKitMarkdownRuntimeHost;
10
+ useGlobalRuntimeHost?: boolean;
11
+ securePolicy?: HostRuntimePolicy;
12
+ hostAdapter?: HostRuntimeAdapter;
13
+ secureFrame?: boolean | SecureFrameOptions;
14
+ playgroundUrl?: string;
15
+ showChrome?: boolean;
16
+ showSource?: boolean;
17
+ placeholder?: ReactNode;
18
+ className?: string;
19
+ onError?: (error: unknown, code: string) => void;
20
+ };
21
+ export type SlexKitRendererProps = CustomRendererProps & Omit<SlexKitRendererOptions, "languages">;
22
+ export declare function SlexKitRenderer({ code, language, isIncomplete, meta, domain, renderMode, runtime, runtimeHost, useGlobalRuntimeHost, securePolicy, hostAdapter, secureFrame, playgroundUrl, showChrome, showSource, placeholder, className, onError, }: SlexKitRendererProps): import("react").DetailedReactHTMLElement<{
23
+ className: string;
24
+ }, HTMLElement> | import("react").FunctionComponentElement<import("react").ClassAttributes<HTMLDivElement> & import("react").HTMLAttributes<HTMLDivElement> & {
25
+ language: string;
26
+ isIncomplete?: boolean;
27
+ }> | null;
28
+ export declare function createSlexKitRenderer(options?: SlexKitRendererOptions): CustomRenderer;
29
+ export declare const slexkitRenderer: CustomRenderer;
package/dist/index.js ADDED
@@ -0,0 +1,297 @@
1
+ import { createElement, Fragment, useEffect, useMemo, useRef, useState, } from "react";
2
+ import { CodeBlockContainer, CodeBlockHeader, } from "streamdown";
3
+ import { ingest, getSlexKitMarkdownRuntimeHost, mount, mountSecureArtifact, parseSlexSource, } from "slexkit";
4
+ const DEFAULT_LANGUAGES = ["slex"];
5
+ const STREAMDOWN_RENDERER_VERSION = "0.2.0";
6
+ const DEFAULT_SECURE_POLICY = {};
7
+ function languageList(languages) {
8
+ return languages ?? [...DEFAULT_LANGUAGES];
9
+ }
10
+ async function copySource(code) {
11
+ if (navigator.clipboard?.writeText) {
12
+ await navigator.clipboard.writeText(code);
13
+ return;
14
+ }
15
+ const textarea = document.createElement("textarea");
16
+ textarea.value = code;
17
+ textarea.setAttribute("readonly", "");
18
+ textarea.style.position = "fixed";
19
+ textarea.style.opacity = "0";
20
+ document.body.appendChild(textarea);
21
+ textarea.select();
22
+ document.execCommand("copy");
23
+ textarea.remove();
24
+ }
25
+ function defaultPlaceholder() {
26
+ return createElement("div", { className: "slex-streamdown-placeholder" }, "Rendering SlexKit...");
27
+ }
28
+ function parseFenceOptions(meta = "") {
29
+ const options = {};
30
+ for (const match of String(meta).matchAll(/([A-Za-z0-9_-]+)=("([^"]*)"|'([^']*)'|([^\s]+))/g)) {
31
+ options[match[1]] = match[3] ?? match[4] ?? match[5] ?? "";
32
+ }
33
+ return options;
34
+ }
35
+ function resolveRenderMode(meta, fallback) {
36
+ const options = parseFenceOptions(meta);
37
+ const value = String(options.render ?? options.as ?? options.mode ?? "").toLowerCase();
38
+ if (value === "playground" || value === "editor" || value === "workbench")
39
+ return "playground";
40
+ if (value === "component" || value === "render" || value === "preview")
41
+ return "component";
42
+ return fallback;
43
+ }
44
+ function errorMessage(error) {
45
+ return error instanceof Error ? error.message : String(error);
46
+ }
47
+ function errorDiagnostic(error) {
48
+ if (!isRecord(error))
49
+ return null;
50
+ const diagnostic = error.diagnostic;
51
+ if (!isRecord(diagnostic))
52
+ return null;
53
+ if (typeof diagnostic.message !== "string" ||
54
+ typeof diagnostic.line !== "number" ||
55
+ typeof diagnostic.column !== "number" ||
56
+ typeof diagnostic.excerpt !== "string") {
57
+ return null;
58
+ }
59
+ return {
60
+ message: diagnostic.message,
61
+ line: diagnostic.line,
62
+ column: diagnostic.column,
63
+ detail: typeof diagnostic.detail === "string" ? diagnostic.detail : undefined,
64
+ excerpt: diagnostic.excerpt,
65
+ };
66
+ }
67
+ function renderError(error) {
68
+ const diagnostic = errorDiagnostic(error);
69
+ if (!diagnostic) {
70
+ return createElement("div", { className: "slex-streamdown-error", role: "alert" }, createElement("div", { className: "slex-streamdown-error-title" }, "Failed to render SlexKit"), createElement("div", { className: "slex-streamdown-error-message" }, errorMessage(error)));
71
+ }
72
+ return createElement("div", { className: "slex-streamdown-error", role: "alert" }, createElement("div", { className: "slex-streamdown-error-title" }, "SlexKit syntax error"), createElement("div", { className: "slex-streamdown-error-message" }, diagnostic.message), createElement("div", { className: "slex-streamdown-error-location" }, `Line ${diagnostic.line}, column ${diagnostic.column}`), diagnostic.detail
73
+ ? createElement("div", { className: "slex-streamdown-error-detail" }, diagnostic.detail)
74
+ : null, createElement("pre", { className: "slex-streamdown-error-excerpt" }, diagnostic.excerpt));
75
+ }
76
+ function isRecord(value) {
77
+ return !!value && typeof value === "object" && !Array.isArray(value);
78
+ }
79
+ function isRenderableSource(value) {
80
+ if (!isRecord(value))
81
+ return false;
82
+ if ("layout" in value)
83
+ return isRecord(value.layout) && Object.keys(value.layout).length > 0;
84
+ if ("namespace" in value || "g" in value)
85
+ return false;
86
+ return Object.keys(value).some((key) => key.includes(":"));
87
+ }
88
+ function isStateOnlySource(value) {
89
+ return isRecord(value)
90
+ && !isRenderableSource(value)
91
+ && ("slex" in value || "namespace" in value || "g" in value);
92
+ }
93
+ function scopedSlexKitInput(code, source, domain) {
94
+ if (!domain || !isRecord(source) || !("slex" in source || "namespace" in source || "g" in source || "layout" in source))
95
+ return code;
96
+ const namespace = String(source.namespace || "default");
97
+ if (!("layout" in source) && isRenderableSource(source)) {
98
+ const { slex, namespace: _namespace, g, layout: _layout, ...bareLayout } = source;
99
+ return {
100
+ ...(typeof slex === "string" ? { slex } : {}),
101
+ namespace: `${domain}::${namespace}`,
102
+ ...(isRecord(g) ? { g } : {}),
103
+ layout: bareLayout,
104
+ };
105
+ }
106
+ return {
107
+ ...source,
108
+ namespace: `${domain}::${namespace}`,
109
+ };
110
+ }
111
+ function playgroundOption(meta, key, fallback = "") {
112
+ return parseFenceOptions(meta)[key] || fallback;
113
+ }
114
+ function playgroundSlexKitInput(code, meta, domain, playgroundUrl) {
115
+ const options = parseFenceOptions(meta);
116
+ const namespace = domain || "streamdown";
117
+ return {
118
+ slex: "0.1",
119
+ namespace: `${namespace}::playground`,
120
+ g: {},
121
+ layout: {
122
+ "playground:inline": {
123
+ domain: `${namespace}::playground`,
124
+ mode: options.mode || options.webMode || "render",
125
+ playgroundUrl: options.webUrl || options.playgroundUrl || playgroundUrl || "/playground.html",
126
+ pluginVersion: options.pluginVersion || STREAMDOWN_RENDERER_VERSION,
127
+ previewMinHeight: playgroundOption(meta, "previewMinHeight", playgroundOption(meta, "height", "360px")),
128
+ source: code,
129
+ sourceType: "slex",
130
+ title: playgroundOption(meta, "title", "SlexKit playground"),
131
+ },
132
+ },
133
+ };
134
+ }
135
+ function PlaygroundShell({ code, domain, meta, playgroundUrl, }) {
136
+ const hostRef = useRef(null);
137
+ const [loadError, setLoadError] = useState(null);
138
+ useEffect(() => {
139
+ const host = hostRef.current;
140
+ if (!host)
141
+ return;
142
+ host.replaceChildren();
143
+ setLoadError(null);
144
+ let active = true;
145
+ let cleanup;
146
+ void import("slexkit/tooling")
147
+ .then(() => {
148
+ if (!active)
149
+ return;
150
+ cleanup = mount(playgroundSlexKitInput(code, meta, domain, playgroundUrl), host);
151
+ })
152
+ .catch((err) => {
153
+ if (active)
154
+ setLoadError(err);
155
+ });
156
+ return () => {
157
+ active = false;
158
+ cleanup?.();
159
+ host.replaceChildren();
160
+ };
161
+ }, [code, domain, meta, playgroundUrl]);
162
+ return createElement(Fragment, null, loadError ? renderError(loadError) : null, createElement("div", { ref: hostRef }));
163
+ }
164
+ export function SlexKitRenderer({ code, language, isIncomplete, meta, domain, renderMode = "component", runtime = "trusted", runtimeHost, useGlobalRuntimeHost = false, securePolicy = DEFAULT_SECURE_POLICY, hostAdapter, secureFrame = true, playgroundUrl, showChrome = true, showSource = false, placeholder, className, onError, }) {
165
+ const hostRef = useRef(null);
166
+ const [error, setError] = useState(null);
167
+ const isSecureRuntime = runtime === "secure";
168
+ const activeRuntimeHost = runtimeHost ?? (useGlobalRuntimeHost ? getSlexKitMarkdownRuntimeHost() : undefined);
169
+ const activeRuntimeMode = activeRuntimeHost?.getMode();
170
+ const delegatesToSecureHost = activeRuntimeMode === "secure";
171
+ const parsedSource = useMemo(() => (isIncomplete || isSecureRuntime || delegatesToSecureHost ? undefined : parseSlexSource(code)), [code, delegatesToSecureHost, isIncomplete, isSecureRuntime]);
172
+ const sourceKind = useMemo(() => {
173
+ if (isIncomplete)
174
+ return "renderable";
175
+ if (isSecureRuntime || delegatesToSecureHost)
176
+ return "renderable";
177
+ return parsedSource?.ok && isStateOnlySource(parsedSource.value) ? "state-only" : "renderable";
178
+ }, [delegatesToSecureHost, isIncomplete, isSecureRuntime, parsedSource]);
179
+ const isStateOnly = sourceKind === "state-only";
180
+ const runtimeInput = useMemo(() => isSecureRuntime || delegatesToSecureHost ? String(code) : scopedSlexKitInput(code, parsedSource?.ok ? parsedSource.value : undefined, domain), [code, delegatesToSecureHost, domain, isSecureRuntime, parsedSource]);
181
+ const effectiveRenderMode = useMemo(() => resolveRenderMode(meta, renderMode), [meta, renderMode]);
182
+ const parseError = parsedSource && !parsedSource.ok ? parsedSource.error : null;
183
+ const displayError = parseError ?? error;
184
+ useEffect(() => {
185
+ setError(null);
186
+ if (isIncomplete)
187
+ return;
188
+ if (parseError) {
189
+ onError?.(parseError, String(code));
190
+ return;
191
+ }
192
+ if (isStateOnly) {
193
+ if (!ingest(runtimeInput)) {
194
+ const err = new Error("Failed to parse Slex state block.");
195
+ setError(err);
196
+ onError?.(err, String(code));
197
+ }
198
+ return;
199
+ }
200
+ if (effectiveRenderMode === "playground")
201
+ return;
202
+ const host = hostRef.current;
203
+ if (!host)
204
+ return;
205
+ host.replaceChildren();
206
+ let cleanup;
207
+ try {
208
+ cleanup = activeRuntimeHost
209
+ ? activeRuntimeHost.mountBlock({
210
+ artifactId: domain,
211
+ source: runtimeInput,
212
+ container: host,
213
+ theme: undefined,
214
+ })
215
+ : isSecureRuntime
216
+ ? mountSecureArtifact(runtimeInput, host, {
217
+ policy: securePolicy,
218
+ hostAdapter,
219
+ frame: secureFrame,
220
+ })
221
+ : mount(runtimeInput, host);
222
+ if (!host.querySelector(".slexkit-root")) {
223
+ if (delegatesToSecureHost)
224
+ return;
225
+ if (isSecureRuntime && host.querySelector("iframe[data-slexkit-secure-frame='true']"))
226
+ return;
227
+ throw new Error("SlexKit did not render a root. Check the source syntax.");
228
+ }
229
+ }
230
+ catch (err) {
231
+ try {
232
+ cleanup?.();
233
+ }
234
+ finally {
235
+ host.replaceChildren();
236
+ }
237
+ setError(err);
238
+ onError?.(err, String(code));
239
+ }
240
+ return () => {
241
+ cleanup?.();
242
+ host.replaceChildren();
243
+ };
244
+ }, [
245
+ code,
246
+ activeRuntimeHost,
247
+ delegatesToSecureHost,
248
+ effectiveRenderMode,
249
+ hostAdapter,
250
+ isIncomplete,
251
+ isSecureRuntime,
252
+ isStateOnly,
253
+ onError,
254
+ parseError,
255
+ runtimeInput,
256
+ secureFrame,
257
+ securePolicy,
258
+ ]);
259
+ if (isStateOnly)
260
+ return null;
261
+ const body = createElement("div", { className: "slex-streamdown-body" }, isIncomplete ? (placeholder ?? defaultPlaceholder()) : null, !isIncomplete && effectiveRenderMode === "playground"
262
+ ? createElement(PlaygroundShell, {
263
+ code: String(code),
264
+ domain,
265
+ meta,
266
+ playgroundUrl,
267
+ })
268
+ : createElement(Fragment, null, displayError ? renderError(displayError) : null, createElement("div", { ref: hostRef })));
269
+ if (!showChrome) {
270
+ return createElement("div", {
271
+ className: ["slex-streamdown-block", className].filter(Boolean).join(" "),
272
+ }, body, showSource
273
+ ? createElement("details", { className: "slex-streamdown-source" }, createElement("summary", null, "Source"), createElement("pre", null, createElement("code", null, code)))
274
+ : null);
275
+ }
276
+ return createElement(CodeBlockContainer, {
277
+ className: ["slex-streamdown-block", className].filter(Boolean).join(" "),
278
+ isIncomplete,
279
+ language,
280
+ }, createElement(CodeBlockHeader, { language }), createElement("div", { className: "slex-streamdown-toolbar" }, createElement("button", {
281
+ className: "slex-streamdown-copy",
282
+ onClick: () => {
283
+ void copySource(code);
284
+ },
285
+ type: "button",
286
+ }, "Copy source")), body, showSource
287
+ ? createElement("details", { className: "slex-streamdown-source" }, createElement("summary", null, "Source"), createElement("pre", null, createElement("code", null, code)))
288
+ : null);
289
+ }
290
+ export function createSlexKitRenderer(options = {}) {
291
+ const { languages, ...rendererOptions } = options;
292
+ return {
293
+ language: languageList(languages),
294
+ component: (props) => createElement(SlexKitRenderer, { ...props, ...rendererOptions }),
295
+ };
296
+ }
297
+ export const slexkitRenderer = createSlexKitRenderer();
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@slexkit/streamdown",
3
+ "version": "0.2.0",
4
+ "description": "Streamdown custom renderer for SlexKit fenced UI blocks.",
5
+ "author": "SlexKit contributors",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/slexkit/slexkit.git",
10
+ "directory": "packages/streamdown"
11
+ },
12
+ "homepage": "https://github.com/slexkit/slexkit/tree/main/packages/streamdown#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/slexkit/slexkit/issues"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./style.css": "./style.css"
25
+ },
26
+ "files": [
27
+ "dist/",
28
+ "style.css",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "sideEffects": [
33
+ "./style.css"
34
+ ],
35
+ "scripts": {
36
+ "build": "bun run scripts/build.ts"
37
+ },
38
+ "keywords": [
39
+ "slexkit",
40
+ "streamdown",
41
+ "react",
42
+ "markdown",
43
+ "renderer"
44
+ ],
45
+ "license": "MIT",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "peerDependencies": {
50
+ "slexkit": "^0.2.0",
51
+ "react": "^18.0.0 || ^19.0.0",
52
+ "react-dom": "^18.0.0 || ^19.0.0",
53
+ "streamdown": "^2.5.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "slexkit": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@types/react": "^19.2.7",
62
+ "@types/react-dom": "^19.2.3",
63
+ "react": "^19.2.6",
64
+ "react-dom": "^19.2.6",
65
+ "slexkit": "file:../..",
66
+ "streamdown": "^2.5.0"
67
+ }
68
+ }
package/style.css ADDED
@@ -0,0 +1,95 @@
1
+ .slex-streamdown-block {
2
+ margin-block: 0.75rem;
3
+ }
4
+
5
+ .slex-streamdown-toolbar {
6
+ display: flex;
7
+ justify-content: flex-end;
8
+ gap: 0.5rem;
9
+ padding: 0.5rem 0.75rem 0;
10
+ }
11
+
12
+ .slex-streamdown-copy {
13
+ border: 1px solid var(--border);
14
+ border-radius: calc(var(--radius) - 2px);
15
+ background: var(--secondary);
16
+ color: var(--secondary-foreground);
17
+ cursor: pointer;
18
+ font: inherit;
19
+ font-size: 0.75rem;
20
+ line-height: 1;
21
+ padding: 0.4rem 0.6rem;
22
+ }
23
+
24
+ .slex-streamdown-copy:hover {
25
+ background: var(--accent);
26
+ color: var(--accent-foreground);
27
+ }
28
+
29
+ .slex-streamdown-body {
30
+ padding: 0.75rem;
31
+ }
32
+
33
+ .slex-streamdown-placeholder {
34
+ border: 1px dashed var(--border);
35
+ border-radius: calc(var(--radius) - 2px);
36
+ color: var(--muted-foreground);
37
+ padding: 1.5rem;
38
+ text-align: center;
39
+ }
40
+
41
+ .slex-streamdown-error {
42
+ border: 1px dashed var(--destructive);
43
+ border-radius: calc(var(--radius) - 2px);
44
+ color: var(--destructive);
45
+ padding: 1rem;
46
+ text-align: left;
47
+ }
48
+
49
+ .slex-streamdown-error-title {
50
+ font-weight: 700;
51
+ }
52
+
53
+ .slex-streamdown-error-message {
54
+ margin-top: 0.5rem;
55
+ color: var(--foreground);
56
+ font-family: var(--font-mono, "Geist Mono", "Noto Sans Mono", "Noto Sans Mono CJK SC", "SFMono-Regular", "Cascadia Code", Consolas, monospace);
57
+ font-size: 0.8125rem;
58
+ line-height: 1.5;
59
+ }
60
+
61
+ .slex-streamdown-error-location,
62
+ .slex-streamdown-error-detail {
63
+ margin-top: 0.5rem;
64
+ color: var(--muted-foreground);
65
+ font-size: 0.8125rem;
66
+ }
67
+
68
+ .slex-streamdown-error-excerpt {
69
+ overflow: auto;
70
+ margin: 0.75rem 0 0;
71
+ border-radius: calc(var(--radius) - 2px);
72
+ background: var(--background);
73
+ color: var(--foreground);
74
+ padding: 0.75rem;
75
+ font-family: var(--font-mono, "Geist Mono", "Noto Sans Mono", "Noto Sans Mono CJK SC", "SFMono-Regular", "Cascadia Code", Consolas, monospace);
76
+ font-size: 0.8125rem;
77
+ line-height: 1.55;
78
+ white-space: pre;
79
+ }
80
+
81
+ .slex-streamdown-source {
82
+ border-top: 1px solid var(--border);
83
+ padding: 0.5rem 0.75rem 0.75rem;
84
+ }
85
+
86
+ .slex-streamdown-source summary {
87
+ color: var(--muted-foreground);
88
+ cursor: pointer;
89
+ font-size: 0.75rem;
90
+ }
91
+
92
+ .slex-streamdown-source pre {
93
+ margin: 0.5rem 0 0;
94
+ overflow: auto;
95
+ }