@zumer/snapdom 2.24.16 → 3.0.0-beta.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.
@@ -1,11 +1,11 @@
1
1
  /**
2
- * snapDOM – ultra-fast DOM-to-image capture
3
- * TypeScript definitions (v2.16)
2
+ * SnapDOM — browser capture engine for web interfaces
3
+ * TypeScript definitions (v3)
4
4
  *
5
5
  * Notes:
6
6
  * - Style compression is internal (no public option).
7
- * - Icon fonts are always embedded; `embedFonts` controls non-icon fonts only.
8
- * - This file preserves backward compatibility with earlier defs.
7
+ * - Recognized icon fonts are rendered as images; `embedFonts` controls text fonts.
8
+ * - These are the v3 public declarations; see the migration guide for removed options.
9
9
  */
10
10
 
11
11
  /* =========================
@@ -16,14 +16,43 @@ export type RasterMime = "png" | "jpg" | "jpeg" | "webp";
16
16
  export type BlobType = "svg" | RasterMime;
17
17
 
18
18
  export type IconFontMatcher = string | RegExp;
19
- export type CachePolicy = "disabled" | "full" | "auto" | "soft";
20
19
 
21
- /** Geometry of the serialized capture, expressed in SVG viewBox CSS pixels. */
20
+ /**
21
+ * How far a capture runs: the frozen clone, or the rendered image. Plugins declare it as
22
+ * `needs`; the result reports it as `needs`. A shallower "dom" stage existed and was
23
+ * removed: a capture that takes no clone does no capturing.
24
+ */
25
+ export type CaptureStage = "clone" | "render";
26
+ /**
27
+ * Persistent resource/style cache policy. 'soft' is the v3 default; 'disabled' (or
28
+ * `cache: false`) empties and bypasses those caches for debugging. v2's 'auto' and 'full'
29
+ * are no longer part of the type: at runtime both map to 'soft' (see the README migration
30
+ * table), so a stored v2 value keeps working while new code gets the v3 vocabulary.
31
+ */
32
+ export type CachePolicy = "soft" | "disabled";
33
+
34
+ /**
35
+ * Stable codes of the degradations a capture records in `result.warnings`. Every literal
36
+ * here is pushed by the runtime (grep `sessionWarn(` in src); the trailing `string & {}`
37
+ * keeps the union open without widening the literals away.
38
+ */
39
+ export type WarningCode =
40
+ | "image-fallback"
41
+ | "raster-clamp"
42
+ | "canvas-clamp"
43
+ | "safari-png-fallback"
44
+ | "reconcile-risk"
45
+ | "backdrop-filter-failed"
46
+ | "selection-compose-failed"
47
+ | "shrink-failed"
48
+ | (string & {});
49
+
50
+ /** Geometry of the rendered capture, expressed in viewBox-style CSS pixels. */
22
51
  export interface CaptureMeta {
23
52
  /** Logical capture-box size (the clip-window size when clip is active). */
24
53
  readonly w0: number;
25
54
  readonly h0: number;
26
- /** Serialized SVG viewBox size, including bleed/padding. */
55
+ /** Render viewBox size, including bleed/padding. */
27
56
  readonly vbW: number;
28
57
  readonly vbH: number;
29
58
  /** Requested output basis before scale/dpr rasterization. */
@@ -36,7 +65,7 @@ export interface CaptureMeta {
36
65
  readonly clip: Readonly<{ x: number; y: number; width: number; height: number }> | null;
37
66
  }
38
67
 
39
- /** A window in serialized SVG viewBox coordinates. */
68
+ /** A window in capture viewBox coordinates (see `CaptureMeta`). */
40
69
  export interface CanvasCrop {
41
70
  x: number;
42
71
  y: number;
@@ -46,18 +75,6 @@ export interface CanvasCrop {
46
75
 
47
76
  export type CanvasExportOptions = Partial<SnapdomOptions> & { crop?: CanvasCrop };
48
77
 
49
- /** Silent core exporters exposed only while a plugin defines custom exports. */
50
- export interface PluginExportFacade {
51
- img(options?: Partial<SnapdomOptions>): Promise<HTMLImageElement>;
52
- svg(options?: Partial<SnapdomOptions>): Promise<HTMLImageElement>;
53
- canvas(options?: CanvasExportOptions): Promise<HTMLCanvasElement>;
54
- blob(options?: BlobOptions & Partial<SnapdomOptions>): Promise<Blob>;
55
- png(options?: Partial<SnapdomOptions>): Promise<HTMLImageElement>;
56
- jpeg(options?: Partial<SnapdomOptions>): Promise<HTMLImageElement>;
57
- jpg(options?: Partial<SnapdomOptions>): Promise<HTMLImageElement>;
58
- webp(options?: Partial<SnapdomOptions>): Promise<HTMLImageElement>;
59
- }
60
-
61
78
  /* =========================
62
79
  * Font & proxy declarations
63
80
  * ========================= */
@@ -85,9 +102,8 @@ export interface ExcludeFonts {
85
102
  * ========================= */
86
103
 
87
104
  export interface SnapdomOptions {
88
- /** Fast path: skip small idle delays where safe. */
89
- fast?: boolean;
90
- /** Output scale multiplier. Takes precedence over width/height. */
105
+ /** Output scale multiplier. Applies only when neither width nor height is set —
106
+ * width/height are the absolute output size and win (one rule across all exporters). */
91
107
  scale?: number;
92
108
  /** Device pixel ratio to use for rasterization (defaults to `devicePixelRatio`). */
93
109
  dpr?: number;
@@ -97,29 +113,36 @@ export interface SnapdomOptions {
97
113
  height?: number;
98
114
 
99
115
  /** Background fallback color. Default: `"#ffffff"` for JPEG/WebP, `null` (transparent) otherwise. */
100
- backgroundColor?: string;
116
+ backgroundColor?: string | null;
101
117
  /** Quality for JPEG/WebP (0..1). Default 0.92. */
102
118
  quality?: number;
103
- /** Output format for capture/export helpers. Default "png". */
119
+ /** Default for format-selecting exports; named helpers choose their own codec. Default "png". */
104
120
  format?: BlobType;
105
121
 
106
122
  /** Cross-origin proxy prefix (used as a fallback when CORS blocks). */
107
123
  useProxy?: string;
108
124
 
109
- /** Default Blob type for toBlob() when unspecified. */
125
+ /** @deprecated Legacy alias kept synchronized with canonical `format`; either key is honored. */
110
126
  type?: BlobType;
111
127
 
112
- /** CSS selector list to filter nodes. */
113
- exclude?: string[];
114
- /** How to apply `exclude` ("hide" keeps layout via visibility:hidden; "remove" drops nodes). Default "hide". */
128
+ /**
129
+ * Nodes to leave out of the capture: a CSS selector, a predicate returning true to
130
+ * EXCLUDE the element, or any mix of both in an array.
131
+ * Predicates are synchronous and run on each new capture; result memoization is bypassed
132
+ * when a predicate is present so it can read current application state.
133
+ */
134
+ exclude?: string | ((el: Element) => boolean) | Array<string | ((el: Element) => boolean)>;
135
+ /** How excluded nodes leave ("hide" keeps layout via an invisible spacer; "remove" drops them). Default "hide". */
115
136
  excludeMode?: "hide" | "remove";
116
137
 
117
138
  /**
118
- * Custom predicate: return true to keep node, false to exclude.
119
- * Runs in document order; pairs with `filterMode`.
139
+ * Independent keep predicate, evaluated after exclude. Truthy keeps the node; falsy
140
+ * omits it using filterMode. An exclusion takes precedence and skips this predicate
141
+ * for that node. Synchronous callbacks run again for each capture so application-state
142
+ * changes do not reuse an earlier filtering decision.
120
143
  */
121
144
  filter?: (el: Element) => boolean;
122
- /** How to apply `filter` ("hide" or "remove"). Default "hide". */
145
+ /** Mode for nodes omitted only by filter; independent of excludeMode. Default "hide". */
123
146
  filterMode?: "hide" | "remove";
124
147
 
125
148
  /**
@@ -130,30 +153,30 @@ export interface SnapdomOptions {
130
153
  reconcile?: boolean;
131
154
 
132
155
  /**
133
- * Memoizes repeated captures of this element: a scoped MutationObserver marks it dirty on
134
- * external change, and an unchanged repeat capture returns the cached result without
135
- * re-running the pipeline. Opt-in — costs a persistent observer per element. Without it,
136
- * snapdom warns once (console.warn) if the same element is captured 3+ times within 2s.
137
- * Default false.
138
- */
139
- burst?: boolean;
140
-
141
- /**
142
- * With `burst: true`, forces a fresh capture on this call for changes the automatic
143
- * MutationObserver tracking can't see: canvas pixel draws, programmatic CSSOM edits
156
+ * Forces one fresh capture that is not served from the existing memo, and clears style
157
+ * snapshots after application changes for which the browser exposes no signal, notably
158
+ * programmatic CSSOM edits
144
159
  * (stylesheet.insertRule/deleteRule, cssRule.style.* on a rule rather than an element).
145
- * Ignored without `burst: true`. Default false.
160
+ * Frame-driven canvas/video/iframe trees already capture fresh automatically.
161
+ * The fresh stable result may become the new memo for later calls. Works with the engine's
162
+ * automatic memoization — no other option required. Default false.
146
163
  */
147
164
  invalidate?: boolean;
148
165
 
149
- /** outerTransforms the root: remove translate/rotate, keep scale/skew. Default true. */
166
+ /** Root translation is normalized in both modes. true (default) keeps rotation;
167
+ * false removes rotation, keeps scale/skew and recomputes the bounding box. */
150
168
  outerTransforms?: boolean;
151
169
  /**
152
- * Expand root bbox for shadows/blur/outline instead of stripping them from the
153
- * cloned root. Default false (root shadows/outline are stripped, blur bleed is
154
- * still included).
170
+ * Control root shadow/outline bleed. Default false strips root box/text shadows,
171
+ * outline and drop-shadow(); blur remains visible and its bleed is included unless an
172
+ * explicit `clip` fixes the capture edges.
173
+ * true keeps and bounds all root effects.
174
+ *
175
+ * `'subtree'` additionally widens the capture for the outer-shadow ink DESCENDANTS
176
+ * paint past the root's box — a card whose own ring is a box-shadow, captured from a
177
+ * wrapper around it — measured per side and bounded by any ancestor that clips.
155
178
  */
156
- outerShadows?: boolean;
179
+ outerShadows?: boolean | 'subtree';
157
180
 
158
181
  /**
159
182
  * Capture only a region instead of the full element: `'viewport'` (what the user
@@ -163,14 +186,12 @@ export interface SnapdomOptions {
163
186
  clip?: "viewport" | { x: number; y: number; width: number; height: number } | null;
164
187
 
165
188
  /**
166
- * Downsample inlined raster images to their visible resolution (display box × scale × dpr),
167
- * preserving the source codec (lossless PNG stays lossless). On by default; pass `false`
168
- * to embed images verbatim.
189
+ * Inline non-icon fonts actually used within the subtree. Default 'auto': embeds only
190
+ * when the element uses families the document declares as webfonts (system-font pages
191
+ * skip the phase entirely). true forces the embed pass; false disables it (webfont text
192
+ * will rasterize with fallback metrics).
169
193
  */
170
- compress?: boolean;
171
-
172
- /** Inline non-icon fonts actually used within the subtree. */
173
- embedFonts?: boolean;
194
+ embedFonts?: boolean | 'auto';
174
195
  /** Provide fonts explicitly to avoid remote discovery. */
175
196
  localFonts?: LocalFont[];
176
197
  /** Additional matchers for icon font families (strings or regex). */
@@ -183,44 +204,71 @@ export interface SnapdomOptions {
183
204
  /**
184
205
  * Skip style properties when snapshotting computed styles (e.g. `/^--/` to exclude
185
206
  * CSS variables on pages with thousands of custom props).
207
+ * A function is reevaluated on each new capture, without reusing a prior style snapshot.
186
208
  */
187
209
  excludeStyleProps?: RegExp | ((prop: string) => boolean);
188
210
 
211
+ /**
212
+ * Renderer: 'svg' by default. The second engine, 'html-in-canvas', is EXPERIMENTAL and
213
+ * requires a build with SNAPDOM_CANVAS_ENGINE=1. It renders through the WICG html-in-canvas
214
+ * API (ctx.drawElementImage — Chrome 148+ origin trial / chrome://flags/#canvas-draw-element)
215
+ * when available, using the browser's own painter (native form controls, no svg-as-image
216
+ * quirks). Falls back silently to the svg pipeline whenever unsupported/not applicable,
217
+ * including captures with beforeRender/afterRender plugin hooks. Successful native-engine
218
+ * results expose raster exports; reading `.url` or calling `toRaw()` synchronously
219
+ * materializes and memoizes a PNG data URL, while `toSvg()`/`toImg()` return an image
220
+ * backed by a PNG encoding of the bitmap. There is no serialized SVG.
221
+ */
222
+ engine?: 'svg' | 'html-in-canvas';
223
+
189
224
  /** Verbose diagnostics via console.warn. Default false. */
190
225
  debug?: boolean;
191
226
 
192
- /** Default filename (without extension) for download(). Default "snapDOM". */
227
+ /**
228
+ * Filename for download(). The extension is appended from the export format when the name
229
+ * does not already carry one, so `'card'` saves as `card.png`. Default `"snapDOM"`.
230
+ */
193
231
  filename?: string;
194
232
 
195
233
  /**
196
- * Fallback image when <img> fails to load.
197
- * Can be a fixed URL or a callback that receives measured dimensions.
234
+ * Fallback image when an <img> fails to inline. A fixed URL, or a callback that receives
235
+ * the measured box, the failing `src` and the source `<img>` and returns the URL to fetch
236
+ * instead (a Promise is awaited). A falsy return, a throw or a failed fallback fetch falls
237
+ * through to the placeholder. With a callback, each new capture runs the pipeline again
238
+ * and calls it when a fallback is needed; an earlier capture result keeps its image.
198
239
  */
199
240
  fallbackURL?:
200
241
  | string
201
- | ((dims: { width?: number; height?: number }) => string);
242
+ | ((info: { width?: number; height?: number; src?: string; element?: HTMLImageElement }) => string | Promise<string>);
202
243
 
203
- /** Cache policy for resources and style maps. Default "soft". */
204
- cache?: CachePolicy;
244
+ /**
245
+ * Persistent resource/style cache behavior. 'disabled' (or false) empties and bypasses
246
+ * those persistent caches whenever the capture pipeline runs, a debug/testing escape
247
+ * hatch. 'soft' is the default. v2's 'auto'/'full' map to 'soft' at runtime and are gone
248
+ * from the type. Automatic repeat-capture memoization and differential recapture are separate.
249
+ */
250
+ cache?: CachePolicy | false;
205
251
 
206
252
  /** Show placeholders when resources are missing. Default true. */
207
253
  placeholders?: boolean;
208
254
 
209
255
  /**
210
- * Resolve lazy `<picture>` placeholders / `data-src` patterns before clone (default true).
211
- * Set false to skip; register the `picture-resolver` plugin explicitly if you need overrides while core is off.
256
+ * Render the user's live text selection into the capture. A selection is paint without DOM
257
+ * — the browser draws the highlight and restyles the selected glyphs at paint time — so a
258
+ * structural clone loses it. Selected runs are wrapped in the styles the browser painted
259
+ * them with: the authored `::selection` background, color, text-shadow and text-decoration
260
+ * where a rule matches, the UA highlight colour where none does, and nothing at all over
261
+ * `user-select: none`. A focused field's selection is painted as background layers behind
262
+ * its value. Default false.
212
263
  */
213
- resolvePicturePlaceholders?: boolean;
214
- /** Fine-tune the built-in picture/lazy resolver (timeout, concurrency, etc.). */
215
- pictureResolver?: {
216
- timeout?: number;
217
- concurrency?: number;
218
- resolveLazySrc?: boolean;
219
- silent?: boolean;
220
- };
264
+ captureSelection?: boolean;
221
265
 
222
266
  /** Arbitrary plugin configuration at call-site (see PluginUse). */
223
267
  plugins?: PluginUse[];
268
+
269
+ /** Reuse an existing canvas as the render target instead of allocating one
270
+ * (`toCanvas` and everything built on it). */
271
+ canvas?: HTMLCanvasElement;
224
272
  }
225
273
 
226
274
  /* =========================
@@ -228,59 +276,95 @@ export interface SnapdomOptions {
228
276
  * ========================= */
229
277
 
230
278
  /**
231
- * Union of the two runtime context shapes. Which fields actually exist depends on the hook:
232
- *
233
- * - Clone-phase hooks (beforeSnap, beforeClone, afterClone, beforeRender, afterRender) get the
234
- * capture state: `element`, `options`, `plugins`, `clone`, `classCSS`, `styleCache`, `nodeMap`,
235
- * `fontsCSS`, `baseCSS`, `scrollbarCSS`, `svgString`, `dataURL`. Capture options are NOT
236
- * flattened onto it there: read them from `ctx.options`.
237
- * - Export-phase hooks (beforeExport, afterExport, defineExports) get a spread of the normalized
238
- * options plus `element`, `meta`, `export` and (in defineExports) `exports`. There is no
239
- * `clone` or `nodeMap` at that point: the clone is already serialized.
240
- *
241
- * Consequence: data stashed as `ctx.__x` in afterClone is invisible in defineExports. Mirror it
242
- * on `ctx.options.__x`, which is the object the export context is spread from.
279
+ * The single object every hook receives: the normalized capture options at the TOP level
280
+ * (`ctx.scale`, `ctx.backgroundColor`, …) plus the per-stage fields as they are produced.
281
+ * It is the very bag the pipeline reads, so changing an option in `beforeSnap` takes effect
282
+ * — except for the options resolved before the first hook (`plugins`, `needs`,
283
+ * `invalidate`, `cache`). `format` is canonical; deprecated `type` stays synchronized with
284
+ * it, and changing either supported name in `beforeSnap` is honored.
243
285
  */
244
- export interface CaptureContext extends SnapdomOptions {
245
- /** Input element being captured. Present in every hook. */
286
+ export interface CaptureContext extends Omit<SnapdomOptions, "exclude" | "filter"> {
287
+ /** Input element being captured. */
246
288
  element: Element;
247
289
 
248
- /** Normalized capture options. Present in clone-phase hooks only (export hooks are spread from it). */
249
- options?: SnapdomOptions & Record<string, any>;
290
+ /** Exclusion selectors only, always an array here: createContext splits the option's
291
+ * predicates off into `excludePredicates`. A plugin deciding whether a node stays should
292
+ * call `shouldExclude(el)` rather than re-run either list. */
293
+ exclude: readonly string[];
294
+
295
+ /** The predicates passed through `exclude`, or null when there were none. */
296
+ readonly excludePredicates: ReadonlyArray<(el: Element) => boolean> | null;
297
+
298
+ /** The keep predicate, or null when the option was not set. */
299
+ filter: ((el: Element) => boolean) | null;
250
300
 
251
- /** Cloned root (detached). Clone-phase hooks only, from `afterClone` onward. */
301
+ /** Canonical normalized capture format. */
302
+ format: BlobType;
303
+
304
+ /** @deprecated Synchronized legacy alias for `format`. */
305
+ type: BlobType;
306
+
307
+ /** Self-reference to this same context, for plugins written against `ctx.options`. */
308
+ readonly options: CaptureContext;
309
+
310
+ /** Combined omission policy: true for data-capture="exclude", exclusion selectors or
311
+ * predicates, and nodes rejected by filter. Available at every hook stage. */
312
+ readonly shouldExclude: (el: Element) => boolean;
313
+
314
+ /** How far this capture runs (the deepest `needs` of its plugins). A plugin that
315
+ * defines exports can read it to know whether there will be an image at all. */
316
+ needs: CaptureStage;
317
+
318
+ /** Cloned root (detached), from `afterClone` through the render hooks. Released (null)
319
+ * once `afterRender` has run, so a live result never retains the whole tree. */
252
320
  clone?: HTMLElement | SVGElement | null;
253
321
 
254
- /** Internal style/class caches (opaque to user). Clone-phase hooks only. */
322
+ /** Internal style/class caches (opaque to user). */
255
323
  classCSS?: string;
256
324
  styleCache?: unknown;
325
+ nodeMap?: unknown;
257
326
  fontsCSS?: string;
258
327
  baseCSS?: string;
259
- scrollbarCSS?: string;
260
- /** Clone node → source node map for this capture. Clone-phase hooks only. */
261
- nodeMap?: Map<Node, Node>;
262
328
 
263
- /** Serialized artifacts, available after render. Clone-phase hooks only. */
264
- svgString?: string;
329
+ /** Serialized artifacts, available in `afterRender`. `svgString` is released as soon as
330
+ * that hook returns (read it lazily from `export.svgString` in an export hook). */
331
+ svgString?: string | null;
265
332
  dataURL?: string;
266
333
 
267
- /** Authoritative geometry produced by the render pass. */
334
+ /** Authoritative render geometry, frozen and pinned once the render stage runs.
335
+ * Absent on a capture that stopped at 'clone' (no viewBox exists). */
268
336
  readonly meta?: Readonly<CaptureMeta>;
269
337
 
270
- /** Silent core-export facade, available to `defineExports` only. */
271
- exports?: PluginExportFacade;
338
+ /** Render artifacts handed to `defineExports` so exporters never reverse-parse the data
339
+ * URL for CSS the pipeline already holds. `svgString` stays out on purpose (it would
340
+ * double retained memory) — read it lazily from `export.svgString`. */
341
+ artifacts?: {
342
+ classCSS: string;
343
+ fontsCSS: string;
344
+ baseCSS: string;
345
+ scrollbarCSS: string;
346
+ } | null;
272
347
 
273
348
  /** Current export info during beforeExport/afterExport. */
274
349
  export?: {
275
- /** Export key (e.g., "png", "jpeg", "svg", or any custom key). */
350
+ /** Export key (e.g., "png", "jpeg", "svg", or any custom key). Absent inside `defineExports`. */
276
351
  type?: string;
277
352
  /** Capture defaults merged with the options passed to the exporter. */
278
353
  options?: any;
279
- /** Exact own options supplied to this export call; omitted keys stay omitted. */
354
+ /** Exact own options supplied to this export call, frozen when `toXxx()` was called;
355
+ * omitted keys stay omitted, so a plugin can tell an explicit value from a default. */
280
356
  requestedOptions?: Readonly<Record<string, unknown>>;
281
- /** Canonical SVG data URL of this capture. */
357
+ /** Capture data URL: SVG for the default engine; lazily materialized PNG for a
358
+ * successful `html-in-canvas` capture. */
282
359
  url: string;
360
+ /** Lazily decodes the default engine's SVG source from `url` (kept a thunk so a live
361
+ * result doesn't retain a second copy). Throws for a raster `html-in-canvas` capture. */
362
+ svgString?: () => string;
283
363
  };
364
+
365
+ /** Core export facade for `defineExports`: the built-in exporters without their hooks,
366
+ * so a plugin format can build on `png`/`canvas`/… without re-entering the pipeline. */
367
+ exports?: Record<string, (opts?: any) => Promise<any>>;
284
368
  }
285
369
 
286
370
  /* =========================
@@ -289,9 +373,22 @@ export interface CaptureContext extends SnapdomOptions {
289
373
 
290
374
  export type Exporter = (ctx: CaptureContext, opts?: any) => Promise<any>;
291
375
 
292
- /** Map returned by `defineExports`: keys are exposed on the result as a `to<Name>()` helper and through `to(name)` (e.g. `pdf` → `result.toPdf()` and `result.to('pdf')`). */
376
+ /** Map returned by `defineExports`: each key is exposed on the result as a `to<Key>()`
377
+ * helper (`pdf` → `result.toPdf()`) and by name through `result.to('pdf')`. */
293
378
  export type ExportMap = Record<string, Exporter>;
294
379
 
380
+ /** Second argument of the export hooks. Their return values are observational: every plugin
381
+ * gets this same payload and hook returns are ignored, while `options` is the SAME object
382
+ * the exporter receives, so mutating it is how a plugin steers the export. For a
383
+ * format-selecting export, `options.format` is canonical and deprecated `options.type`
384
+ * stays synchronized with it; changing either supported name is honored. */
385
+ export interface ExportHookPayload {
386
+ /** Export name: "png", "blob", "download", or any plugin-declared key. */
387
+ format: string;
388
+ /** Capture defaults merged with the options passed to this export call. */
389
+ options: any;
390
+ }
391
+
295
392
  /* =========================
296
393
  * Plugin system
297
394
  * ========================= */
@@ -300,6 +397,26 @@ export interface SnapdomPlugin {
300
397
  /** Unique name for de-dupe/overrides. */
301
398
  name: string;
302
399
 
400
+ /**
401
+ * How far the capture has to run for this plugin: 'clone' (frozen tree, no pixels)
402
+ * or 'render' (the whole pipeline). Default 'render'.
403
+ * The capture runs to the deepest stage any attached plugin declares, so one plugin can
404
+ * only lower it when every other agrees. Below 'render' there is no image: `url`,
405
+ * `meta` and core image exports throw. Custom plugin exports still work, and
406
+ * `result.needs` reports what ran.
407
+ */
408
+ needs?: CaptureStage;
409
+
410
+ /**
411
+ * Declares capture-affecting hooks deterministic and idempotent, opting the plugin back
412
+ * into unchanged-repeat memoization. Pure beforeRender/afterRender hooks may also use
413
+ * differential recapture; clone-construction hooks still force a conservative full
414
+ * recapture after a change because the splice path cannot skip them. Set this only if
415
+ * re-running your hooks on the same input always produces the same output. Default false.
416
+ * Captures stopped at `needs: 'clone'` are never memoized.
417
+ */
418
+ pure?: boolean;
419
+
303
420
  /** Hook order follows registration order. All hooks may be async. */
304
421
  beforeSnap?(context: CaptureContext): void | Promise<void>;
305
422
  beforeClone?(context: CaptureContext): void | Promise<void>;
@@ -307,32 +424,25 @@ export interface SnapdomPlugin {
307
424
  beforeRender?(context: CaptureContext): void | Promise<void>;
308
425
  afterRender?(context: CaptureContext): void | Promise<void>;
309
426
 
310
- /** Runs before EACH export. */
311
- beforeExport?(
312
- context: CaptureContext,
313
- payload: { format: string; options: any }
314
- ): void | Promise<void>;
427
+ /** Runs before EACH export. Mutate `payload.options` to change what the exporter does. */
428
+ beforeExport?(context: CaptureContext, payload: ExportHookPayload): void | Promise<void>;
315
429
  /**
316
- * Runs after EACH export. Observation only: the value returned here becomes the payload
317
- * passed to the next plugin's afterExport, but what the caller receives from
318
- * toPng()/toBlob()/... is always what the exporter produced. To change an output,
319
- * register your own export with `defineExports`.
430
+ * Runs after EACH export. Observational: the return value is ignored and the export's own
431
+ * result is what the caller gets. To produce a different result, declare that format in
432
+ * `defineExports` (it can build on `ctx.exports.png()` and friends).
320
433
  */
321
- afterExport?(
322
- context: CaptureContext,
323
- payload: { format: string; options: any; result: any }
324
- ): any | Promise<any>;
434
+ afterExport?(context: CaptureContext, payload: ExportHookPayload & { result: any }): void | Promise<void>;
325
435
 
326
436
  /**
327
437
  * Provide custom exporters (e.g., { pdf: async (ctx, opts) => Blob }).
328
- * Keys are exposed on the capture result as a `to<Name>()` helper (toPdf()) and through
329
- * `to(name)` (result.to('pdf')). Bare index access (result.pdf()) is NOT generated.
438
+ * Keys are exposed on the capture result as helpers (`toPdf()`) and by name (`to('pdf')`).
330
439
  */
331
440
  defineExports?(context: CaptureContext): ExportMap | Promise<ExportMap>;
332
441
 
333
442
  /**
334
- * Per-node hook, called for every element while the clone is built (after exclude/filter,
335
- * before built-in iframe/canvas/video/audio handling). First plugin returning a value wins:
443
+ * Per-node hook, called for every element while the clone is built (after the compiled
444
+ * exclusion policy, before built-in iframe/canvas/video/audio handling). First plugin
445
+ * returning a value wins:
336
446
  * - Node → used as the finished clone for that node (mapped to source, box styles applied)
337
447
  * - null → skip the node entirely
338
448
  * - undefined → continue with the normal pipeline
@@ -356,11 +466,13 @@ export type PluginUse =
356
466
  * Capture result API
357
467
  * ========================= */
358
468
 
469
+ /** Options of `download()`. A `dpr` passed alongside is ignored: the file is always written
470
+ * at dpr 1, the capture's CSS size in pixels, independent of the monitor it was taken on. */
359
471
  export interface DownloadOptions {
360
472
  filename?: string;
361
473
  /** Output format for the downloaded file. Default "png". */
362
474
  format?: BlobType;
363
- /** Override default blob type for this download. */
475
+ /** @deprecated Legacy alias kept synchronized with canonical `format`; either key is honored. */
364
476
  type?: BlobType;
365
477
  /** Quality hint for raster formats. */
366
478
  quality?: number;
@@ -370,6 +482,9 @@ export interface DownloadOptions {
370
482
  }
371
483
 
372
484
  export interface BlobOptions {
485
+ /** Blob codec. Defaults to SVG for the default engine and PNG for a successful native capture. */
486
+ format?: BlobType;
487
+ /** @deprecated Legacy alias kept synchronized with canonical `format`; either key is honored. */
373
488
  type?: BlobType;
374
489
  quality?: number;
375
490
  width?: number;
@@ -377,31 +492,63 @@ export interface BlobOptions {
377
492
  }
378
493
 
379
494
  export interface CaptureResult {
380
- /** Canonical data URL of the SVG snapshot (when available). */
495
+ /**
496
+ * Canonical data URL of the capture. On the default engine, the serialized SVG. On a
497
+ * successful experimental engine:'html-in-canvas' capture the artifact is a raster bitmap:
498
+ * pixel exports (toCanvas/toPng/toJpg/toWebp and raster toBlob) consume it directly, and reading `url`
499
+ * (or toRaw()) mints a PNG data URL once, lazily — the encode costs 15-22x the direct
500
+ * draw, so it is paid only when a string is actually asked for.
501
+ *
502
+ * THROWS when `needs` is not 'render': that capture produced no image, and it is not
503
+ * re-captured on demand (it would be a different instant).
504
+ */
381
505
  url: string;
382
506
 
383
- /** Authoritative serialized viewBox/content geometry. */
507
+ /**
508
+ * How far this capture ran — the deepest stage its plugins declared. 'render' unless a
509
+ * plugin lowered it, in which case core image exports throw. Custom plugin exports
510
+ * can still return their own data.
511
+ */
512
+ needs: CaptureStage;
513
+
514
+ /**
515
+ * Degradation log for this capture — empty in the common case. Entries record the
516
+ * capture's silent fallbacks, keyed by `WarningCode`. Export-time entries (clamps, PNG
517
+ * fallback) append after the corresponding export resolves.
518
+ */
519
+ warnings: Array<{ code: WarningCode; message: string; detail?: unknown }>;
520
+
521
+ /**
522
+ * Authoritative render viewBox/content geometry — the same frozen record the exporters
523
+ * read, so raster placement cannot diverge from the captured artifact.
524
+ *
525
+ * THROWS when `needs` is not 'render', for the same reason `url` does.
526
+ */
384
527
  readonly meta: Readonly<CaptureMeta>;
385
528
 
386
- /** Returns the raw SVG data URL (same as `url`). */
529
+ /** Returns the capture data URL (SVG by default; PNG for a successful html-in-canvas capture). */
387
530
  toRaw(): string;
388
531
 
389
532
  /** Run any registered export by name (core or plugin), e.g. `to("png")`. */
390
533
  to(type: string, options?: any): Promise<any>;
391
534
 
392
535
  /**
393
- * @deprecated Use `toSvg()` for an <img> that renders the SVG snapshot.
536
+ * @deprecated Use `toSvg()` for an <img> representing the capture.
394
537
  * Historical alias kept for compatibility.
395
538
  */
396
- toImg(): Promise<HTMLImageElement>;
539
+ toImg(options?: Partial<SnapdomOptions>): Promise<HTMLImageElement>;
397
540
 
398
- /** Returns an HTMLImageElement that renders the SVG snapshot. */
541
+ /** Returns an HTMLImageElement representing the capture (SVG-backed by default,
542
+ * PNG-backed after a successful native html-in-canvas capture, SVG-backed on fallback). */
399
543
  toSvg(options?: Partial<SnapdomOptions>): Promise<HTMLImageElement>;
400
544
 
401
- /** Returns a Canvas with the rasterized snapshot. */
545
+ /** Returns a Canvas with the rasterized snapshot. `crop` windows the capture in
546
+ * `meta` viewBox coordinates, so a long capture can be rasterized page by page. */
402
547
  toCanvas(options?: CanvasExportOptions): Promise<HTMLCanvasElement>;
403
548
 
404
- /** Returns a Blob of the chosen type (svg/png/jpeg/webp). */
549
+ /** Returns a Blob of the chosen type (svg/png/jpeg/webp). Defaults to SVG on the default
550
+ * engine and PNG after a successful html-in-canvas capture. An explicit SVG request on
551
+ * that raster path rejects because there is no SVG source. */
405
552
  toBlob(options?: BlobOptions & Partial<SnapdomOptions>): Promise<Blob>;
406
553
 
407
554
  /** Convenience raster exports returning an HTMLImageElement. */
@@ -415,12 +562,9 @@ export interface CaptureResult {
415
562
  download(options?: DownloadOptions & Partial<SnapdomOptions>): Promise<void>;
416
563
 
417
564
  /**
418
- * Custom exporters exposed by plugins:
419
- * - As helpers: a plugin returning { pdf: (...) => ... } enables result.toPdf(...)
420
- * - By name: result.to('pdf', options)
421
- *
422
- * The index signature exists because helper names are not known ahead of time; it does
423
- * not mean result['pdf']() is defined (it is not: only the toX() helper is generated).
565
+ * Custom exporters exposed by plugins: a plugin returning { pdf: (...) => ... } enables
566
+ * `result.toPdf(...)` and `result.to('pdf', ...)`. Those helper names are not known ahead
567
+ * of time, hence the index signature.
424
568
  */
425
569
  [key: string]: any;
426
570
  }
@@ -444,15 +588,43 @@ export declare function snapdom(
444
588
  export declare namespace snapdom {
445
589
  function plugins(...defs: PluginUse[]): typeof snapdom;
446
590
 
591
+ /** The package version this bundle was built from (`'src'` when imported from source). */
592
+ const version: string;
593
+
594
+ /**
595
+ * Capture an HTML string (SSR markup, templates) without wiring a mount: it mounts
596
+ * offscreen in the live document (page CSS/fonts apply), captures, and cleans up.
597
+ *
598
+ * ⚠️ TRUSTED HTML ONLY. The string is assigned to `innerHTML` and attached to the real
599
+ * document, so it is parsed and activated exactly like markup you wrote yourself:
600
+ * `<img onerror>` and friends RUN, in your origin, with your cookies. This is inherent
601
+ * to rendering arbitrary markup — there is no capture without a live layout — so the
602
+ * boundary is the caller's to hold. Never pass user-supplied HTML that has not been
603
+ * sanitized first (DOMPurify or equivalent).
604
+ */
605
+ function fromString(html: string, options?: SnapdomOptions): Promise<CaptureResult>;
606
+
607
+ /**
608
+ * The capture that is ready before the click: link prefetch, translated. Arm it once at
609
+ * setup. A memo-eligible capture started in the same event task as a control's press or
610
+ * click event is learned for that control. Later pointer-enter or focus intent repeats it
611
+ * with a shallow copy of the original call's top-level options, so an unchanged click can
612
+ * be served from memory; the first unknown intent event warms the visible viewport once.
613
+ * It reacts to intent events and does no background polling. A programmatic capture needs
614
+ * none of this, because memoization engages on every eligible element's first capture.
615
+ */
616
+ function preCapture(): void;
617
+
447
618
  /** Shortcut helpers that run a one-off capture+export. */
448
619
 
449
- /** Returns the raw SVG data URL of a one-off capture. */
620
+ /** Returns the capture data URL (SVG by default; PNG after a successful native
621
+ * html-in-canvas capture, SVG on fallback). */
450
622
  function toRaw(
451
623
  element: Element,
452
624
  options?: SnapdomOptions
453
625
  ): Promise<string>;
454
626
 
455
- /** @deprecated Returns an SVG <img>; prefer `toSvg`. */
627
+ /** @deprecated Returns an <img> representing the capture; prefer `toSvg`. */
456
628
  function toImg(
457
629
  element: Element,
458
630
  options?: SnapdomOptions
@@ -495,37 +667,39 @@ export declare namespace snapdom {
495
667
  }
496
668
 
497
669
  /* =========================
498
- * preCache helper
499
- * ========================= */
500
-
501
- export interface PreCacheOptions {
502
- /** Root to scan (defaults to `document`). */
503
- root?: Element | Document;
504
- /** Try to embed non-icon fonts used under root (see also localFonts). */
505
- embedFonts?: boolean;
506
- /** Provide fonts explicitly to avoid remote discovery. */
507
- localFonts?: LocalFont[];
508
- /** Additional matchers for icon fonts (strings or regex). */
509
- iconFonts?: IconFontMatcher | IconFontMatcher[];
510
- /** Cross-origin proxy prefix (as in SnapdomOptions.useProxy). */
511
- useProxy?: string;
512
- /** Cache policy for this preload operation. */
513
- cache?: CachePolicy;
514
-
515
- /** Back-compat fields (no-ops if present) */
516
- /**
517
- * @deprecated Use `cache` instead.
518
- */
519
- cacheOpt?: CachePolicy;
520
- }
670
+ * Plugin registry
671
+ * =========================
672
+ * One runtime owns the registry: the `@zumer/snapdom/plugins` subpath resolves to the root
673
+ * module itself (package.json maps it there), so registering through either reaches the
674
+ * same list.
675
+ */
521
676
 
677
+ /** Register plugins globally, deduped by name. A global plugin must run to 'render'. */
678
+ export declare function registerPlugins(...defs: PluginUse[]): void;
679
+ /** Drop every globally registered plugin. */
680
+ export declare function clearPlugins(): void;
681
+ /** The globally registered plugin instances, in registration order. */
682
+ export declare function getGlobalPlugins(): SnapdomPlugin[];
683
+ /** Resolve a factory/tuple/instance into a plugin instance. */
684
+ export declare function normalizePlugin(spec: PluginUse): SnapdomPlugin | null;
685
+ /** Ordered capture stages, cheapest first. */
686
+ export declare const STAGES: readonly CaptureStage[];
687
+ /** The stage a plugin gets when it declares nothing. */
688
+ export declare const DEFAULT_STAGE: CaptureStage;
522
689
  /**
523
- * Preload external resources for a subtree to avoid first-capture stalls.
524
- * Uses the same discovery heuristics as the main capture path.
690
+ * Validate a declared stage, naming `pluginName` in the error. Returns the deepest supported
691
+ * stage when `value` is undefined/null, so a plugin that declares nothing runs the full pipeline.
525
692
  */
526
- export declare function preCache(
527
- root?: Element | Document,
528
- options?: PreCacheOptions
529
- ): Promise<void>;
693
+ export declare function assertNeeds(
694
+ pluginName: string,
695
+ value?: unknown,
696
+ supported?: readonly CaptureStage[]
697
+ ): CaptureStage;
698
+
699
+ /* The `/plugins` subpath points at THIS file through the `types` condition
700
+ * in package.json's exports map. They used to be declared here as
701
+ * `declare module "@zumer/snapdom/plugins" { … }`. In a file that is already a module those
702
+ * are AUGMENTATIONS of a specifier TypeScript has to resolve first, and it cannot resolve a
703
+ * package into itself, so every consumer without `skipLibCheck: true` got TS2665. */
530
704
 
531
705
  export {};