scenic-prism-standalone 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ import { shaderConstant } from "./embed.js";
2
+ import { minify } from "./minify.js";
3
+ import { configureCanvas } from "./options.js";
4
+ /** The file's own header: what it is, and what it is not. */
5
+ function header(config) {
6
+ return `/**
7
+ * <${config.name} /> — one scenic-prism scene, compiled, with nothing to install.
8
+ *
9
+ * Generated by scenic-prism-standalone. The scene is already WGSL by the time
10
+ * this file exists: every shape, material and light of it is baked into the
11
+ * TRACE source below as numeric literals, so there is no library to add to
12
+ * the project this is pasted into and nothing is fetched at runtime. The only
13
+ * import is React; the only requirement is a browser with WebGPU, and a
14
+ * TypeScript whose DOM library declares it (6.0 and later do).
15
+ *
16
+ * <${config.name} size={720} className="rounded-lg" />
17
+ *
18
+ * The component renders a bare <canvas> and nothing else — no wrapper, no
19
+ * styling, no fallback element — so its size on the page, and what it sits in,
20
+ * stay yours. Everything a <canvas> takes is passed straight through.
21
+ *
22
+ * Nothing regenerates this file. Edit it as your own.
23
+ */`;
24
+ }
25
+ /** The scene, the framing and the budget, as the constants the component reads. */
26
+ function constants(config) {
27
+ return [
28
+ '/** Backing resolution the component falls back to, in pixels. */',
29
+ `const WIDTH = ${config.width};`,
30
+ `const HEIGHT = ${config.height};`,
31
+ '',
32
+ '/** Samples per pixel accumulated before the loop stops. */',
33
+ `const MAX_FRAMES = ${config.maxFrames};`,
34
+ '',
35
+ '/** A fixed base seed, or null for a fresh sample sequence on every mount. */',
36
+ `const SEED: number | null = ${config.seed === null ? 'null' : config.seed};`,
37
+ '',
38
+ '/** The WebGPU flags this file uses, by value. */',
39
+ 'const BUFFER_STORAGE = 0x80;',
40
+ 'const BUFFER_UNIFORM = 0x40;',
41
+ 'const BUFFER_COPY_DST = 0x08;',
42
+ '',
43
+ '// The scene itself: every shape, material and light of it, compiled to a',
44
+ '// WGSL compute shader with all of its parameters baked in as literals. One',
45
+ "// invocation per pixel adds one sample to that pixel's running sum.",
46
+ shaderConstant('TRACE', config.trace),
47
+ '',
48
+ '// Tonemap and gamma, from the accumulation buffer to the canvas.',
49
+ shaderConstant('DISPLAY', config.display),
50
+ ].join('\n');
51
+ }
52
+ /** The props, which are a canvas's plus the render's own. */
53
+ function props(name) {
54
+ return `/**
55
+ * Everything a <canvas> takes, minus the five this component owns. \`width\` and
56
+ * \`height\` are the backing store rather than the CSS box, and \`onProgress\` and
57
+ * \`onError\` are DOM media events on every element — here they mean the render's
58
+ * progress and the render's failure.
59
+ */
60
+ type CanvasProps = Omit<
61
+ ComponentPropsWithoutRef<'canvas'>,
62
+ 'width' | 'height' | 'onProgress' | 'onError' | 'children'
63
+ >;
64
+
65
+ export interface ${name}Props extends CanvasProps {
66
+ /** Square backing resolution in pixels; \`width\`/\`height\` override it. */
67
+ size?: number;
68
+ /** Backing-store width in pixels. Not a CSS width. */
69
+ width?: number;
70
+ /** Backing-store height in pixels. Not a CSS height. */
71
+ height?: number;
72
+ /** Stop accumulating after this many samples per pixel. */
73
+ maxFrames?: number;
74
+ /** Fix the sample sequence, for a reproducible accumulation. */
75
+ seed?: number | null;
76
+ /** Called after every accumulated frame, with the running sample count. */
77
+ onProgress?: (frames: number, total: number) => void;
78
+ /** Called once the accumulation finishes, with the count. */
79
+ onDone?: (frames: number) => void;
80
+ /** Called when the scene cannot be traced at all — no WebGPU, say. */
81
+ onError?: (error: Error) => void;
82
+ }`;
83
+ }
84
+ /**
85
+ * The fixed half of the file: the same progressive path tracer the library
86
+ * runs, inside the effect that owns it.
87
+ */
88
+ function body(config) {
89
+ const { name, workgroup } = config;
90
+ return `export function ${name}({
91
+ size,
92
+ width,
93
+ height,
94
+ maxFrames = MAX_FRAMES,
95
+ seed = SEED,
96
+ onProgress,
97
+ onDone,
98
+ onError,
99
+ ...canvasProps
100
+ }: ${name}Props) {
101
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
102
+
103
+ /**
104
+ * The callbacks are read through a ref, so passing them inline — which is how
105
+ * anyone writes them — does not count as a change of scene. A fresh
106
+ * onProgress on every parent render would otherwise tear the render down and
107
+ * start the accumulation again from noise.
108
+ */
109
+ const callbacks = useRef({ onProgress, onDone, onError });
110
+ useEffect(() => {
111
+ callbacks.current = { onProgress, onDone, onError };
112
+ });
113
+
114
+ // The backing store, which is what the trace is sized for. How large the
115
+ // canvas is drawn is CSS, and so the caller's.
116
+ const w = Math.max(1, Math.round(width ?? size ?? WIDTH));
117
+ const h = Math.max(1, Math.round(height ?? size ?? HEIGHT));
118
+
119
+ useEffect(() => {
120
+ const canvas = canvasRef.current;
121
+ if (!canvas) return;
122
+
123
+ let running = true;
124
+ let raf = 0;
125
+ let device: GPUDevice | undefined;
126
+
127
+ /**
128
+ * A callback is the caller's code, so a throw inside one must not take the
129
+ * loop down with it. Report it the way the browser reports a throwing event
130
+ * listener: asynchronously, leaving everything here untouched.
131
+ */
132
+ const notify = (report: () => void) => {
133
+ try {
134
+ report();
135
+ } catch (error) {
136
+ setTimeout(() => {
137
+ throw error;
138
+ });
139
+ }
140
+ };
141
+
142
+ const start = async () => {
143
+ const gpu = navigator.gpu as GPU | undefined;
144
+ const adapter = await gpu?.requestAdapter();
145
+ if (!gpu || !adapter) throw new Error('This scene needs a browser with WebGPU.');
146
+ // The accumulation buffer is 16 bytes a pixel, so the storage limits are
147
+ // raised to whatever the adapter allows: a large canvas outgrows them.
148
+ device = await adapter.requestDevice({
149
+ requiredLimits: {
150
+ maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
151
+ maxBufferSize: adapter.limits.maxBufferSize,
152
+ },
153
+ });
154
+ // Unmounted while the device was on its way: give it straight back.
155
+ if (!running) {
156
+ device.destroy();
157
+ return;
158
+ }
159
+
160
+ const context = canvas.getContext('webgpu') as GPUCanvasContext | null;
161
+ if (!context) throw new Error('This canvas cannot provide a WebGPU context.');
162
+ const format = gpu.getPreferredCanvasFormat();
163
+ ${configureCanvas(config.colorSpace, config.alpha, ' ')}
164
+ // A shader that will not build is reported through onError, in the
165
+ // compiler's own words, rather than printed to a console.
166
+ device.pushErrorScope('validation');
167
+ const traceModule = device.createShaderModule({ code: TRACE });
168
+ const displayModule = device.createShaderModule({ code: DISPLAY });
169
+ const invalid = await device.popErrorScope();
170
+ if (invalid) throw new Error(invalid.message);
171
+
172
+ const trace = await device.createComputePipelineAsync({
173
+ layout: 'auto',
174
+ compute: { module: traceModule, entryPoint: 'main' },
175
+ });
176
+ const display = await device.createRenderPipelineAsync({
177
+ layout: 'auto',
178
+ vertex: { module: displayModule, entryPoint: 'vertexMain' },
179
+ fragment: { module: displayModule, entryPoint: 'fragmentMain', targets: [{ format }] },
180
+ });
181
+ if (!running) return;
182
+
183
+ // One RGBA32F running sum per pixel, zeroed at creation: read and written
184
+ // in place by the trace, read by the display.
185
+ const accum = device.createBuffer({ size: w * h * 16, usage: BUFFER_STORAGE });
186
+ // Per sample: the resolution, which sample this is, and its seed.
187
+ const frameBuffer = device.createBuffer({
188
+ size: 16,
189
+ usage: BUFFER_UNIFORM | BUFFER_COPY_DST,
190
+ });
191
+ // For the display: the resolution, and how many samples have gone in.
192
+ const displayBuffer = device.createBuffer({
193
+ size: 16,
194
+ usage: BUFFER_UNIFORM | BUFFER_COPY_DST,
195
+ });
196
+
197
+ const frameData = new ArrayBuffer(16);
198
+ new Float32Array(frameData, 0, 2).set([w, h]);
199
+ const frameWords = new Uint32Array(frameData, 8, 2);
200
+ const displayData = new Float32Array([w, h, 0, 0]);
201
+
202
+ const gpuDevice = device;
203
+ const bind = (pipeline: GPUComputePipeline | GPURenderPipeline, uniforms: GPUBuffer) =>
204
+ gpuDevice.createBindGroup({
205
+ layout: pipeline.getBindGroupLayout(0),
206
+ entries: [
207
+ { binding: 0, resource: { buffer: uniforms } },
208
+ { binding: 1, resource: { buffer: accum } },
209
+ ],
210
+ });
211
+ const traceGroup = bind(trace, frameBuffer);
212
+ const displayGroup = bind(display, displayBuffer);
213
+
214
+ let frame = 0;
215
+
216
+ const draw = () => {
217
+ if (!running) return;
218
+ frameWords[0] = frame;
219
+ frameWords[1] =
220
+ seed === null
221
+ ? (Math.random() * 4294967296) >>> 0
222
+ : (Math.imul(seed + frame, 2654435761) ^ frame) >>> 0;
223
+ gpuDevice.queue.writeBuffer(frameBuffer, 0, frameData);
224
+ displayData[2] = frame + 1;
225
+ gpuDevice.queue.writeBuffer(displayBuffer, 0, displayData);
226
+
227
+ const encoder = gpuDevice.createCommandEncoder();
228
+
229
+ // Trace: one invocation per pixel adds one new sample to its running sum.
230
+ const pass = encoder.beginComputePass();
231
+ pass.setPipeline(trace);
232
+ pass.setBindGroup(0, traceGroup);
233
+ pass.dispatchWorkgroups(Math.ceil(w / ${workgroup[0]}), Math.ceil(h / ${workgroup[1]}));
234
+ pass.end();
235
+
236
+ // Display: tonemap what has accumulated so far onto the canvas.
237
+ const out = encoder.beginRenderPass({
238
+ colorAttachments: [
239
+ {
240
+ view: context.getCurrentTexture().createView(),
241
+ loadOp: 'clear',
242
+ storeOp: 'store',
243
+ clearValue: [0, 0, 0, 0],
244
+ },
245
+ ],
246
+ });
247
+ out.setPipeline(display);
248
+ out.setBindGroup(0, displayGroup);
249
+ out.draw(3);
250
+ out.end();
251
+
252
+ gpuDevice.queue.submit([encoder.finish()]);
253
+ frame++;
254
+ notify(() => callbacks.current.onProgress?.(frame, maxFrames));
255
+
256
+ if (frame >= maxFrames) {
257
+ notify(() => callbacks.current.onDone?.(frame));
258
+ return;
259
+ }
260
+ // The next sample waits for this one, so a scene slower than the
261
+ // screen never queues work faster than the GPU can drain it.
262
+ void gpuDevice.queue.onSubmittedWorkDone().then(() => {
263
+ if (running) raf = requestAnimationFrame(draw);
264
+ });
265
+ };
266
+
267
+ draw();
268
+ };
269
+
270
+ start().catch((cause: unknown) => {
271
+ // A scene that cannot be traced is the caller's to present: there is no
272
+ // fallback element here, because anything drawn in place of the canvas
273
+ // would be this component's styling rather than theirs.
274
+ if (!running) return;
275
+ notify(() =>
276
+ callbacks.current.onError?.(cause instanceof Error ? cause : new Error(String(cause))),
277
+ );
278
+ });
279
+
280
+ // Unmounting, or a change of size, gives the GPU back: the device goes, and
281
+ // every buffer and pipeline on it with it.
282
+ return () => {
283
+ running = false;
284
+ cancelAnimationFrame(raf);
285
+ device?.destroy();
286
+ };
287
+ }, [h, maxFrames, seed, w]);
288
+
289
+ return <canvas ref={canvasRef} width={w} height={h} {...canvasProps} />;
290
+ }`;
291
+ }
292
+ /**
293
+ * The whole `.tsx` file for one scene: the baked constants, the props, then the
294
+ * component that reads them. `min` squeezes the two shaders — which are most of
295
+ * the file — and leaves the component alone, since that half is meant to be
296
+ * read, and whatever bundles it will minify it anyway.
297
+ */
298
+ export function componentSource(config, min = false) {
299
+ const shaders = min
300
+ ? { ...config, trace: minify(config.trace), display: minify(config.display) }
301
+ : config;
302
+ const imports = [
303
+ "import { useEffect, useRef } from 'react';",
304
+ "import type { ComponentPropsWithoutRef } from 'react';",
305
+ ].join('\n');
306
+ const parts = [header(config), imports, constants(shaders), props(config.name), body(config)];
307
+ // A directive has to be the very first thing in the file, above the header.
308
+ if (config.useClient)
309
+ parts.unshift("'use client';");
310
+ if (config.exportDefault)
311
+ parts.push(`export default ${config.name};`);
312
+ return `${parts.join('\n\n')}\n`;
313
+ }
@@ -0,0 +1,24 @@
1
+ import type { StandaloneOptions } from './html.js';
2
+ import type { FluentSceneSpec } from 'scenic-prism-fluent';
3
+ /** What a downloaded file is called when the caller does not name it. */
4
+ export declare const DEFAULT_FILENAME = "scene.html";
5
+ export interface DownloadOptions extends StandaloneOptions {
6
+ /** The name to save under. `.html` is appended if it is missing. */
7
+ filename?: string;
8
+ }
9
+ /**
10
+ * Build the page for `spec` and save it, as though the visitor had clicked a
11
+ * link to it. Returns the same HTML {@link buildStandaloneHtml} would, so a
12
+ * caller that also wants to show or measure what it just saved need not build
13
+ * it twice.
14
+ *
15
+ * ```ts
16
+ * <button onClick={() => downloadStandaloneHtml(spec, { size: 900, min: true })}>
17
+ * Download
18
+ * </button>
19
+ * ```
20
+ *
21
+ * Browser only, and deliberately not guarded: called where there is no
22
+ * `document` it throws, rather than quietly doing nothing on a server render.
23
+ */
24
+ export declare function downloadStandaloneHtml(spec: FluentSceneSpec, options?: DownloadOptions): string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Handing the file to the person looking at the page.
3
+ *
4
+ * The rest of this package is a pure function of a scene and runs anywhere —
5
+ * Node, a build script, a worker. This one is the browser half: it is what
6
+ * turns "generate a page" into "download a page", which is the thing an app
7
+ * with an export button actually wants and the only part that needs a DOM.
8
+ */
9
+ import { buildStandaloneHtml } from "./html.js";
10
+ /** What a downloaded file is called when the caller does not name it. */
11
+ export const DEFAULT_FILENAME = 'scene.html';
12
+ /** `name.html` for anything that does not already end in `.html` or `.htm`. */
13
+ function withExtension(filename) {
14
+ return /\.html?$/i.test(filename) ? filename : `${filename}.html`;
15
+ }
16
+ /**
17
+ * Build the page for `spec` and save it, as though the visitor had clicked a
18
+ * link to it. Returns the same HTML {@link buildStandaloneHtml} would, so a
19
+ * caller that also wants to show or measure what it just saved need not build
20
+ * it twice.
21
+ *
22
+ * ```ts
23
+ * <button onClick={() => downloadStandaloneHtml(spec, { size: 900, min: true })}>
24
+ * Download
25
+ * </button>
26
+ * ```
27
+ *
28
+ * Browser only, and deliberately not guarded: called where there is no
29
+ * `document` it throws, rather than quietly doing nothing on a server render.
30
+ */
31
+ export function downloadStandaloneHtml(spec, options = {}) {
32
+ const html = buildStandaloneHtml(spec, options);
33
+ const url = URL.createObjectURL(new Blob([html], { type: 'text/html;charset=utf-8' }));
34
+ const link = document.createElement('a');
35
+ link.href = url;
36
+ link.download = withExtension(options.filename ?? DEFAULT_FILENAME);
37
+ // Firefox only follows a click on an element that is in the document.
38
+ link.style.display = 'none';
39
+ document.body.append(link);
40
+ link.click();
41
+ link.remove();
42
+ // The download reads the blob asynchronously, so the URL has to outlive the
43
+ // click — a tick is enough, and holding it longer would leak the whole file.
44
+ setTimeout(() => URL.revokeObjectURL(url));
45
+ return html;
46
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The one check every generated file shares.
3
+ *
4
+ * Both of the files this package writes — a page and a component — carry the
5
+ * two shaders as template literals, so both need the same promise kept: that
6
+ * nothing in a shader ends the literal, or the element around it, early.
7
+ *
8
+ * It is a guard on a promise rather than a case to handle. `compile.ts` emits
9
+ * WGSL out of numbers and fixed text, and the one part of a shader it does not
10
+ * write — a `custom` node's own source — is checked for exactly this list by
11
+ * the builder that takes it. So this fails loudly rather than writing a broken
12
+ * file.
13
+ */
14
+ /** Throw unless `source` can be embedded verbatim as a template literal. */
15
+ export declare function checkEmbeddable(name: string, source: string): void;
16
+ /**
17
+ * `const NAME = ` + the shader, as one line and its contents. The opening
18
+ * backtick is followed by the shader and nothing else, which WGSL does not
19
+ * insist on the way GLSL's first-line `#version` did, but which keeps a
20
+ * shader's line numbers in an error message the line numbers of the string.
21
+ */
22
+ export declare function shaderConstant(name: string, source: string): string;
package/dist/embed.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The one check every generated file shares.
3
+ *
4
+ * Both of the files this package writes — a page and a component — carry the
5
+ * two shaders as template literals, so both need the same promise kept: that
6
+ * nothing in a shader ends the literal, or the element around it, early.
7
+ *
8
+ * It is a guard on a promise rather than a case to handle. `compile.ts` emits
9
+ * WGSL out of numbers and fixed text, and the one part of a shader it does not
10
+ * write — a `custom` node's own source — is checked for exactly this list by
11
+ * the builder that takes it. So this fails loudly rather than writing a broken
12
+ * file.
13
+ */
14
+ /**
15
+ * What a shader may never contain: something a template literal reads as an
16
+ * escape or as an interpolation, or that closes the `<script>` a page puts one
17
+ * in. The last two matter to the HTML file alone, but a shader that could not
18
+ * go in a page is not one worth putting in a component either — the two files
19
+ * are the same scene, and an author should not find out which is which.
20
+ */
21
+ const FORBIDDEN = ['`', '${', '\\', '</script', '<!--'];
22
+ /** Throw unless `source` can be embedded verbatim as a template literal. */
23
+ export function checkEmbeddable(name, source) {
24
+ for (const forbidden of FORBIDDEN) {
25
+ if (source.includes(forbidden)) {
26
+ throw new Error(`scenic-prism-standalone: the ${name} shader contains '${forbidden}', ` +
27
+ 'which cannot be embedded in a generated file as a template literal.');
28
+ }
29
+ }
30
+ }
31
+ /**
32
+ * `const NAME = ` + the shader, as one line and its contents. The opening
33
+ * backtick is followed by the shader and nothing else, which WGSL does not
34
+ * insist on the way GLSL's first-line `#version` did, but which keeps a
35
+ * shader's line numbers in an error message the line numbers of the string.
36
+ */
37
+ export function shaderConstant(name, source) {
38
+ checkEmbeddable(name, source);
39
+ return `const ${name} = \`${source}\`;`;
40
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * A page's source, coloured — for showing or handing out the markup
3
+ * {@link buildStandaloneHtml} wrote rather than the scene it draws.
4
+ *
5
+ * This package may not depend on the site (see `CLAUDE.md`), so it cannot
6
+ * reach for the site's highlighters. What follows is its own tokenizing: a
7
+ * small WGSL grammar for the shaders baked into the script, and a tokenizer over
8
+ * that script, the stylesheet and the markup around them. Like its site
9
+ * counterparts it is narrow on purpose — it assumes the input is a page this
10
+ * package generated (every template literal in the script is a shader), not
11
+ * arbitrary hand-written HTML.
12
+ */
13
+ export interface FragmentOptions {
14
+ /**
15
+ * Bake in the site's dark palette instead of its light one. Default false —
16
+ * the colours are fixed values rather than custom properties, so this is
17
+ * the one choice about them the caller gets.
18
+ */
19
+ dark?: boolean;
20
+ /**
21
+ * Put this in front of every class name, both in the markup and in the
22
+ * stylesheet. Default `''`, which is the single letters
23
+ * {@link highlightStandaloneHtml} uses; give a prefix (`'sd-'`, say) where
24
+ * the host page might already have styled `.b` or `.l` itself.
25
+ */
26
+ prefix?: string;
27
+ }
28
+ export interface HighlightOptions extends FragmentOptions {
29
+ /** The document's title. Default `scenic-prism source`. */
30
+ title?: string;
31
+ /**
32
+ * Let the long lines wrap instead of scrolling sideways. Default false: the
33
+ * source sits in a `<pre>` and a line as long as a minified page's is
34
+ * reached by scrolling. True drops the `<pre>` for a `<div>` set to
35
+ * `white-space: pre-wrap`, so the same source folds to the window's width —
36
+ * the indentation is still the file's, but a line break on screen may not
37
+ * be one in the file.
38
+ */
39
+ wrap?: boolean;
40
+ }
41
+ /** The two halves of a highlighted page, for a caller assembling their own. */
42
+ export interface HighlightedFragment {
43
+ /**
44
+ * The coloured source as markup — nothing but text and `<span>`s, with no
45
+ * `<pre>`, `<code>` or wrapper of any kind around it. It carries the
46
+ * original's own newlines and indentation, so whatever encloses it has to
47
+ * preserve them (a `<pre>`, or `white-space: pre` — `pre-wrap` where the
48
+ * long lines should fold rather than scroll).
49
+ */
50
+ code: string;
51
+ /**
52
+ * The rules those spans need, as stylesheet text: one rule per token class
53
+ * and nothing else — no `:root`, no `body`, no rule for the container.
54
+ */
55
+ css: string;
56
+ }
57
+ /**
58
+ * The same colouring as {@link highlightStandaloneHtml}, handed back in two
59
+ * pieces instead of as a document: the marked-up source, and the stylesheet
60
+ * that gives it its colours. Neither carries a `<pre>`, a `<code>`, a `<style>`
61
+ * or anything else around it, so both can be dropped into a page of the
62
+ * caller's own — a docs template, a React component, an email — beside
63
+ * whatever else it shows.
64
+ *
65
+ * ```ts
66
+ * import { buildStandaloneHtml, highlightStandaloneHtmlFragment } from 'scenic-prism-standalone';
67
+ *
68
+ * const html = buildStandaloneHtml(spec);
69
+ * const { code, css } = highlightStandaloneHtmlFragment(html, { prefix: 'sd-' });
70
+ *
71
+ * page = `<style>${css}</style><pre class="listing"><code>${code}</code></pre>`;
72
+ * ```
73
+ *
74
+ * The markup is the original's text with the characters that would not survive
75
+ * it escaped, so the container must preserve whitespace: `code` holds the
76
+ * newlines and the indentation, and the stylesheet says nothing about them.
77
+ */
78
+ export declare function highlightStandaloneHtmlFragment(html: string, options?: FragmentOptions): HighlightedFragment;
79
+ /**
80
+ * Render a page {@link buildStandaloneHtml} wrote as coloured source, rather
81
+ * than as the scene it draws — a self-contained document with no dependency
82
+ * of its own, ready to be written to disk, pasted somewhere that keeps
83
+ * formatting, or shown alongside the render it came from.
84
+ *
85
+ * ```ts
86
+ * import { buildStandaloneHtml, highlightStandaloneHtml } from 'scenic-prism-standalone';
87
+ *
88
+ * const html = buildStandaloneHtml(spec);
89
+ * const source = highlightStandaloneHtml(html, { dark: true });
90
+ * ```
91
+ *
92
+ * `wrap` is the one choice about the shape of it: off, the source sits in a
93
+ * `<pre>` and long lines scroll; on, there is no `<pre>` at all and they fold
94
+ * to the window instead.
95
+ *
96
+ * For the same colouring inside a page of your own, see
97
+ * {@link highlightStandaloneHtmlFragment}, which returns the markup and the
98
+ * stylesheet separately and wraps neither.
99
+ */
100
+ export declare function highlightStandaloneHtml(html: string, options?: HighlightOptions): string;