@zumer/snapdom-plugins 3.0.0 → 4.0.1

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +50 -16
  3. package/agent-map.d.ts +32 -0
  4. package/agent-map.js +5 -3
  5. package/ascii-export.d.ts +10 -0
  6. package/ascii-export.js +21 -6
  7. package/capture-frames.js +5 -5
  8. package/color-tint.js +4 -0
  9. package/context-export.d.ts +23 -0
  10. package/context-export.js +4 -1
  11. package/filter.js +1 -1
  12. package/gif-export.d.ts +6 -0
  13. package/gif-export.js +3 -3
  14. package/html-export.d.ts +8 -0
  15. package/html-export.js +6 -4
  16. package/index.d.ts +2 -1
  17. package/index.js +2 -1
  18. package/package.json +22 -6
  19. package/pdf/LICENSE +21 -0
  20. package/pdf/README.md +22 -0
  21. package/pdf/REFERENCE.md +520 -0
  22. package/pdf/src/frontmatter.js +200 -0
  23. package/pdf/src/furniture.js +398 -0
  24. package/pdf/src/index.js +2415 -0
  25. package/pdf/src/mega/README.md +47 -0
  26. package/pdf/src/mega/diag.js +148 -0
  27. package/pdf/src/mega/flow.js +56 -0
  28. package/pdf/src/mega/fonts.js +287 -0
  29. package/pdf/src/mega/forms.js +479 -0
  30. package/pdf/src/mega/labels.js +43 -0
  31. package/pdf/src/mega/meta.js +145 -0
  32. package/pdf/src/nav.js +357 -0
  33. package/pdf/src/paginate.js +583 -0
  34. package/pdf/src/tagged.js +355 -0
  35. package/pdf/src/text-layer.js +1771 -0
  36. package/pdf/src/writer/encrypt.js +218 -0
  37. package/pdf/src/writer/font.js +507 -0
  38. package/pdf/src/writer/index.js +1135 -0
  39. package/pdf/src/writer/subset.js +387 -0
  40. package/pdf.d.ts +294 -0
  41. package/pdf.js +1 -0
  42. package/redact-inputs.js +6 -6
  43. package/vector/CONTRACT.md +906 -0
  44. package/vector/LICENSE +21 -0
  45. package/vector/README.md +35 -0
  46. package/vector/src/adapters/snapdom.js +795 -0
  47. package/vector/src/collect/box.js +1676 -0
  48. package/vector/src/collect/font.js +487 -0
  49. package/vector/src/collect/image.js +1458 -0
  50. package/vector/src/collect/mathml.js +272 -0
  51. package/vector/src/collect/text.js +3027 -0
  52. package/vector/src/css/color.js +823 -0
  53. package/vector/src/css/gradient.js +1126 -0
  54. package/vector/src/css/matrix.js +398 -0
  55. package/vector/src/css/radius.js +124 -0
  56. package/vector/src/css/shadow.js +201 -0
  57. package/vector/src/emit/figma-json.js +1995 -0
  58. package/vector/src/emit/h2d-vector.js +215 -0
  59. package/vector/src/emit/h2d.js +1147 -0
  60. package/vector/src/emit/pdf.js +2571 -0
  61. package/vector/src/emit/svg-flat.js +4842 -0
  62. package/vector/src/exclusion.js +13 -0
  63. package/vector/src/index.js +2910 -0
  64. package/vector/src/paint.js +2908 -0
  65. package/vector/src/public.js +57 -0
  66. package/vector/src/writer/encrypt.js +227 -0
  67. package/vector/src/writer/font.js +507 -0
  68. package/vector/src/writer/index.js +1148 -0
  69. package/vector/src/writer/subset.js +387 -0
  70. package/vector/svd/index.js +72 -0
  71. package/vector/svd/schema.js +985 -0
  72. package/vector.d.ts +28 -0
  73. package/vector.js +1 -0
  74. package/video-export.d.ts +6 -0
  75. package/video-export.js +7 -7
  76. package/pdf-image.d.ts +0 -18
  77. package/pdf-image.js +0 -114
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Plugin changes
2
+
3
+ ## 4.0.1
4
+
5
+ - Add GitHub Sponsors funding metadata.
6
+ - Raise the SnapDOM peer dependency to ^3.3.0 and document the minimum supported core version.
7
+ - Explicitly include the PDF README in the package and update packaging checks.
8
+
9
+ Requires SnapDOM 3.3.0 or newer within 3.x.
10
+
11
+ ## 4.0.0
12
+
13
+ - Replace pdf-image with the full PDF exporter previously distributed as PDF Pro. Import pdf from @zumer/snapdom-plugins/pdf; toPdf returns a Blob, with optional download, selectable text, links and pagination.
14
+ - Add the MIT-licensed Vector plugin with internal SVD, editable SVG and Figma clipboard exports.
15
+ - Add typed HTML, context, map, ASCII and recording result methods.
16
+ - Add escaped title and language metadata to HTML snapshots.
17
+ - Add optional colored HTML ASCII previews and contrast control while preserving plain-text defaults.
18
+ - Handle Unicode code points in ASCII ramps and reject empty ramps.
19
+ - Reject invalid semantic budgets, map widths and tint opacity with explicit errors.
20
+
21
+ Requires SnapDOM 3.x. This version is published on npm.
package/README.md CHANGED
@@ -2,20 +2,20 @@
2
2
 
3
3
  Official plugins for [SnapDOM](https://github.com/zumerlab/snapdom), a capture engine for
4
4
  web apps. Core exports images and canvases; this package adds HTML, structured context,
5
- element maps, PDF, ASCII, visual transforms and live GIF/video recording.
5
+ element maps, PDF, editable vectors, ASCII, visual transforms and live GIF/video recording.
6
6
 
7
7
  Image, HTML and structured exports use captured state; GIF and video start a sequence of
8
8
  live captures when their export method is called.
9
9
 
10
10
  ## Install
11
11
 
12
- Install the plugins next to a matching core:
12
+ Install the plugins next to a core supported by their peer dependency:
13
13
 
14
14
  ```bash
15
15
  npm install @zumer/snapdom@latest @zumer/snapdom-plugins@latest
16
16
  ```
17
17
 
18
- Plugins 3.x require a 3.x core. See the [migration guide](https://github.com/zumerlab/snapdom#migrating-from-v2) when upgrading from v2.
18
+ Plugins 4.x in this checkout require SnapDOM 3.3.0 or newer within 3.x. The 4.x release replaces pdf-image with the full PDF exporter and adds Vector. Both are MIT-licensed and available on npm in `@zumer/snapdom-plugins` 4.x. The `@latest` command above installs the published release. See the [migration guide](https://github.com/zumerlab/snapdom#migrating-from-v2) when upgrading from v2.
19
19
 
20
20
  ## Usage
21
21
 
@@ -169,7 +169,7 @@ Use `blocks` to hide the subtree that paints visible content. A `value` attribut
169
169
  an input or textarea clears its displayed value and omits its structured `state.value`.
170
170
  Existing default field masks still apply when you add block or attribute rules.
171
171
 
172
- HTML exports use the sanitized clone. `agent-map` and `context-export` apply the same
172
+ HTML exports use the prepared clone with the selected redaction rules. `agent-map` and `context-export` apply the same
173
173
  block, attribute and field rules to their source-derived data, regardless of official
174
174
  plugin order. The rules do not search arbitrary text or images for sensitive content.
175
175
 
@@ -206,7 +206,7 @@ snapdom(el, { plugins: [colorTint({ color: 'royalblue', opacity: 0.4 })] });
206
206
 
207
207
  ### `ascii-export`
208
208
 
209
- Adds a `toAscii()` export method that converts captures to ASCII art.
209
+ Adds a `toAscii()` export method that converts captures to ASCII art. Plain text remains the default. Use `toAscii({ format: 'html', contrast: 0.8 })` for a colored, compact HTML preview; contrast defaults to zero and does not change existing text exports.
210
210
 
211
211
  ```js
212
212
  import { asciiExport } from '@zumer/snapdom-plugins/ascii-export';
@@ -226,23 +226,33 @@ console.log(art);
226
226
 
227
227
  ---
228
228
 
229
- ### `pdf-image`
229
+ ### `pdf`
230
230
 
231
- Adds `toPdfImage()`, which downloads a single-page A4 PDF containing a JPEG of the capture.
232
- Text is part of the image. The method returns a temporary object URL, revoked after five seconds.
231
+ Adds `toPdf()`, returning a PDF Blob with the capture's appearance, selectable text,
232
+ links and pagination. This is the MIT-licensed full exporter formerly distributed as PDF Pro.
233
+ It replaces `pdf-image`; there is no second image-only PDF implementation.
233
234
 
234
235
  ```js
235
- import { pdfImage } from '@zumer/snapdom-plugins/pdf-image';
236
+ import { pdf } from '@zumer/snapdom-plugins/pdf';
236
237
 
237
- const result = await snapdom(el, { plugins: [pdfImage({ orientation: 'landscape' })] });
238
- await result.toPdfImage(); // triggers download
238
+ const result = await snapdom(el, { plugins: [pdf()] });
239
+ const blob = await result.toPdf({ page: 'a4', margin: 36 });
240
+ await result.toPdf({ page: 'letter', download: 'report.pdf' });
239
241
  ```
240
242
 
241
- | Option | Type | Default | Description |
242
- |--------|------|---------|-------------|
243
- | `orientation` | `string` | `'portrait'` | `'portrait'` \| `'landscape'` |
244
- | `quality` | `number` | `0.92` | JPEG quality (0–1) |
245
- | `filename` | `string` | `'capture.pdf'` | Download filename |
243
+ Configure capture-time measurement on `pdf({ forms, outline, breakAvoid, shadow,
244
+ formValues })`. Configure paper size, orientation, margins, tagging, metadata,
245
+ headers, footers, compression or encryption on `toPdf()`. Downloads are opt-in;
246
+ ordinary exports return a Blob. See [the PDF overview](pdf/README.md) and [full reference](pdf/REFERENCE.md).
247
+
248
+ **Migration:** import `pdf` from `/pdf`, replace `pdfImage()` with `pdf()`, and
249
+ replace `toPdfImage()` with `toPdf({ download: 'capture.pdf' })`. Put orientation
250
+ on the export. The old temporary object-URL return becomes a persistent Blob.
251
+ The default page sizing and pagination differ: choose `page: 'a4'` explicitly.
252
+
253
+ Tagged output alone is not a PDF/UA conformance guarantee. The exporter reports
254
+ eligibility and blockers. Redact source text structurally; covering pixels does
255
+ not remove text from a searchable document.
246
256
 
247
257
  ---
248
258
 
@@ -385,6 +395,8 @@ await result.toHtml({ download: 'snapshot.html' });
385
395
  |--------|------|---------|-------------|
386
396
  | `fullDocument` | `boolean` | `true` | Wrap output in `<!DOCTYPE html>…`; if `false`, return just `<style>` + the fragment |
387
397
  | `filename` | `string` | `'capture.html'` | Download filename when `opts.download` is `true` |
398
+ | `title` | `string` | `'Captured interface'` | Escaped document title |
399
+ | `lang` | `string` | `'en'` | Escaped document language |
388
400
 
389
401
  Per-call `opts` accepts `fullDocument`, `filename` and `download`. Set `download: true` to
390
402
  download, or pass a filename string. The method returns the HTML string either way.
@@ -393,6 +405,24 @@ The document retains source markup, including event-handler attributes; it is no
393
405
 
394
406
  ---
395
407
 
408
+ ### `vector`
409
+
410
+ Adds `toVector()` for standalone SVG with editable text and supported shapes, and
411
+ `toFigma()` for Figma clipboard export. Its source and internal SVD format live in
412
+ `vector/` under the MIT license. Both exporters are available on npm in `@zumer/snapdom-plugins` 4.x.
413
+
414
+ ```js
415
+ import { vector } from '@zumer/snapdom-plugins/vector';
416
+ const result = await snapdom(el, { plugins: [vector()] });
417
+ const svg = await result.toVector({ exclude: '.private' });
418
+ await result.toFigma(); // HTTPS or localhost, from a user action
419
+ ```
420
+
421
+ Register per capture. Unsupported paint is diagnosed; the connected source and
422
+ its styles must remain available during export. See [Vector](./vector/README.md).
423
+
424
+ ---
425
+
396
426
  ### `gif-export`
397
427
 
398
428
  Adds `toGif()`, which records the live element and returns an `image/gif` Blob. Its built-in
@@ -500,3 +530,7 @@ See [PLUGIN_SPEC.md](../../PLUGIN_SPEC.md) for the full hook specification and [
500
530
  ## License
501
531
 
502
532
  MIT
533
+
534
+ ## Live demos
535
+
536
+ Run `npm run compile` and `npm run site` from the monorepo root, then open `/playground/`, `/pro/pdf/` or `/pro/vector/`. The demos generate actual files from editable DOM samples; the `/pro/` paths are retained for existing links and do not denote paid plugins.
package/agent-map.d.ts CHANGED
@@ -29,3 +29,35 @@ export interface AgentMapOptions {
29
29
  * of the interactive elements and their bounding boxes. Published as `prompt-export` until 2.12.0 renamed it.
30
30
  */
31
31
  export declare function agentMap(options?: AgentMapOptions): SnapdomPlugin;
32
+
33
+ export interface AgentMapEntry {
34
+ i: number;
35
+ n: string;
36
+ r: string;
37
+ b: [number, number, number, number];
38
+ s?: Record<string, string | number | boolean>;
39
+ t?: string;
40
+ a?: Record<string, string>;
41
+ isSemanticOnly?: true;
42
+ }
43
+ export interface AgentMapResult {
44
+ image?: string;
45
+ map: AgentMapEntry[];
46
+ dimensions: { width: number; height: number };
47
+ }
48
+ export interface ToAgentMapOptions {
49
+ image?: 'annotated' | 'raw' | false;
50
+ imageFormat?: 'png' | 'jpg' | 'webp';
51
+ imageQuality?: number;
52
+ maxImageWidth?: number;
53
+ width?: number;
54
+ height?: number;
55
+ scale?: number;
56
+ dpr?: number;
57
+ backgroundColor?: string;
58
+ }
59
+ declare module '@zumer/snapdom' {
60
+ interface CaptureResult {
61
+ toAgentMap(options?: ToAgentMapOptions): Promise<AgentMapResult>;
62
+ }
63
+ }
package/agent-map.js CHANGED
@@ -64,12 +64,12 @@ export function agentMap(options = {}) {
64
64
  // The map is read in afterClone: with no clone there is no map, only a plausible-looking
65
65
  // empty one.
66
66
  if (needs !== 'clone' && needs !== 'render') {
67
- throw new Error(`[snapdom] agent-map cannot run at stage '${needs}': it supports clone, render`);
67
+ throw new Error(`[SnapDOM] agent-map cannot run at stage '${needs}': it supports clone, render`);
68
68
  }
69
69
  // A capture with no render can never carry an image, and asking for both is a
70
70
  // contradiction the caller should hear now, not at export time.
71
71
  if (needs === 'clone' && image !== false) {
72
- throw new Error("[snapdom] agent-map: needs: 'clone' produces no image — pass image: false, or needs: 'render'");
72
+ throw new Error("[SnapDOM] agent-map: needs: 'clone' makes no image; pass image: false, or use needs: 'render'.");
73
73
  }
74
74
 
75
75
  return {
@@ -97,11 +97,13 @@ export function agentMap(options = {}) {
97
97
  agentMap: async (ctx, opts = {}) => {
98
98
  const meta = ctx.__agentMapMeta;
99
99
  const wantImage = opts.image !== undefined ? opts.image : image;
100
- if (!meta) throw new Error('[snapdom] agent-map: this capture carries no frozen map.');
100
+ if (!meta) throw new Error('[SnapDOM] agent-map: this capture has no map to export.');
101
101
 
102
102
  const format = opts.imageFormat || imageFormat;
103
103
  const quality = opts.imageQuality ?? imageQuality;
104
104
  const maxWidth = opts.maxImageWidth ?? maxImageWidth;
105
+ if (!Number.isFinite(maxWidth) || maxWidth < 1) throw new RangeError('[SnapDOM] agent-map: maxImageWidth must be positive');
106
+ if (wantImage !== false && wantImage !== 'raw' && wantImage !== 'annotated') throw new TypeError('[SnapDOM] agent-map: image must be annotated, raw or false');
105
107
  const geometry = ctx.meta;
106
108
  const sourceW = geometry?.vbW || meta.dimensions.width || 1;
107
109
  const sourceH = geometry?.vbH || meta.dimensions.height || 1;
package/ascii-export.d.ts CHANGED
@@ -14,7 +14,17 @@ export interface AsciiExportOptions {
14
14
  charset?: string;
15
15
  /** Invert the luminance mapping. */
16
16
  invert?: boolean;
17
+ /** Plain text (default) or colored HTML with preserved character spacing. */
18
+ format?: 'text' | 'html';
19
+ /** Non-negative luminance contrast boost. */
20
+ contrast?: number;
17
21
  }
18
22
 
19
23
  /** Adds `result.toAscii()`. */
20
24
  export declare function asciiExport(options?: AsciiExportOptions): SnapdomPlugin;
25
+
26
+ declare module '@zumer/snapdom' {
27
+ interface CaptureResult {
28
+ toAscii(options?: AsciiExportOptions): Promise<string>;
29
+ }
30
+ }
package/ascii-export.js CHANGED
@@ -8,6 +8,8 @@
8
8
  * @param {number} [options.width=80] - Character width of output
9
9
  * @param {string} [options.charset=' .:-=+*#%@'] - Characters from lightest to darkest
10
10
  * @param {boolean} [options.invert=false] - Invert luminance mapping
11
+ * @param {'text'|'html'} [options.format='text'] - Plain text or colored HTML preview
12
+ * @param {number} [options.contrast=0] - Non-negative luminance contrast boost
11
13
  * @returns {Object} SnapDOM plugin
12
14
  */
13
15
  export function asciiExport(options = {}) {
@@ -15,6 +17,8 @@ export function asciiExport(options = {}) {
15
17
  width: charWidth = 80,
16
18
  charset = ' .:-=+*#%@',
17
19
  invert = false,
20
+ format = 'text',
21
+ contrast = 0,
18
22
  } = options;
19
23
 
20
24
  return {
@@ -26,12 +30,17 @@ export function asciiExport(options = {}) {
26
30
  // Capture width is a pixel size, not a character count. v3 merges it into opts.
27
31
  const width = Object.hasOwn(ctx.export.requestedOptions, 'width') || opts.width !== ctx.width
28
32
  ? opts.width : charWidth;
29
- if (!Number.isFinite(width) || width < 1) throw new RangeError('[snapdom] ascii-export: width must be a positive number');
33
+ if (!Number.isFinite(width) || width < 1) throw new RangeError('[SnapDOM] ascii-export: width must be a positive number');
30
34
  const cols = Math.floor(width);
31
- const chars = opts.charset || charset;
35
+ const value = opts.charset ?? charset;
36
+ if (typeof value !== 'string' || !value.length) throw new RangeError('[SnapDOM] ascii-export: charset must be a nonempty string');
37
+ const chars = Array.from(value);
32
38
  const inv = opts.invert ?? invert;
39
+ const outputFormat = ctx.export.requestedOptions.format ?? format;
40
+ if (outputFormat !== 'text' && outputFormat !== 'html') throw new TypeError('[SnapDOM] ascii-export: format must be text or html');
41
+ const gain = opts.contrast ?? contrast;
42
+ if (!Number.isFinite(gain) || gain < 0) throw new RangeError('[SnapDOM] ascii-export: contrast must be non-negative');
33
43
 
34
- if (!chars.length) throw new RangeError('[snapdom] ascii-export: charset must not be empty');
35
44
  // Keep core's decoded-image/Safari path and captured originals without firing hooks again.
36
45
  const source = await exports.canvas({
37
46
  ...opts, width: ctx.width, height: ctx.height, scale: 1, dpr: 1, canvas: null,
@@ -46,18 +55,24 @@ export function asciiExport(options = {}) {
46
55
  c.drawImage(source, 0, 0, cols, rows);
47
56
  const data = c.getImageData(0, 0, cols, rows).data;
48
57
 
49
- let ascii = '';
58
+ const colored = outputFormat === 'html';
59
+ let ascii = colored ? '<pre style="margin:0;font:7px/1 monospace;white-space:pre;overflow-wrap:normal;max-width:100%;overflow:auto;background:white">' : '';
50
60
  for (let y = 0; y < rows; y++) {
51
61
  for (let x = 0; x < cols; x++) {
52
62
  const i = (y * cols + x) * 4;
53
63
  let lum = 1 - (data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114) / 255;
64
+ lum = Math.max(0, Math.min(1, (1 + gain) * (lum - 0.5) + 0.5));
54
65
  if (inv) lum = 1 - lum;
55
66
  const ci = Math.min(Math.floor(lum * chars.length), chars.length - 1);
56
- ascii += chars[ci];
67
+ const char = chars[ci];
68
+ if (colored) {
69
+ const safe = char.replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
70
+ ascii += `<span style="color:rgb(${data[i]},${data[i + 1]},${data[i + 2]})">${safe}</span>`;
71
+ } else ascii += char;
57
72
  }
58
73
  ascii += '\n';
59
74
  }
60
- return ascii;
75
+ return colored ? ascii + '</pre>' : ascii;
61
76
  }
62
77
  };
63
78
  }
package/capture-frames.js CHANGED
@@ -50,12 +50,12 @@ export function frameOptions(ctx, opts, defaults, name) {
50
50
  const duration = exportOption(ctx, opts, 'duration', defaults.duration);
51
51
  const frames = exportOption(ctx, opts, 'frames', defaults.frames);
52
52
  const scale = exportOption(ctx, opts, 'scale', defaults.scale ?? ctx.scale ?? 1);
53
- if (!Number.isFinite(fps) || fps <= 0) throw new RangeError(`[snapdom] ${name}: fps must be positive and finite`);
54
- if (!Number.isFinite(scale) || scale <= 0) throw new RangeError(`[snapdom] ${name}: scale must be positive and finite`);
55
- if (frames != null && (!Number.isInteger(frames) || frames < 1)) throw new RangeError(`[snapdom] ${name}: frames must be a positive integer`);
56
- if (frames == null && (!Number.isFinite(duration) || duration <= 0)) throw new RangeError(`[snapdom] ${name}: duration must be positive and finite`);
53
+ if (!Number.isFinite(fps) || fps <= 0) throw new RangeError(`[SnapDOM] ${name}: fps must be positive and finite`);
54
+ if (!Number.isFinite(scale) || scale <= 0) throw new RangeError(`[SnapDOM] ${name}: scale must be positive and finite`);
55
+ if (frames != null && (!Number.isInteger(frames) || frames < 1)) throw new RangeError(`[SnapDOM] ${name}: frames must be a positive integer`);
56
+ if (frames == null && (!Number.isFinite(duration) || duration <= 0)) throw new RangeError(`[SnapDOM] ${name}: duration must be positive and finite`);
57
57
  const count = frames ?? Math.max(1, Math.round(duration / 1000 * fps));
58
- if (!Number.isFinite(count)) throw new RangeError(`[snapdom] ${name}: frame count must be finite`);
58
+ if (!Number.isFinite(count)) throw new RangeError(`[SnapDOM] ${name}: frame count must be finite`);
59
59
 
60
60
  // Copy capture inputs, not released stage artifacts or export-session internals. In
61
61
  // particular createContext splits exclusion predicates out of exclude: put them back.
package/color-tint.js CHANGED
@@ -16,6 +16,10 @@ export function colorTint(options = {}) {
16
16
  opacity = 1,
17
17
  } = options;
18
18
 
19
+ if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
20
+ throw new RangeError('[SnapDOM] color-tint: opacity must be between 0 and 1');
21
+ }
22
+
19
23
  return {
20
24
  name: 'color-tint',
21
25
  pure: true,
@@ -23,3 +23,26 @@ export interface ContextExportOptions {
23
23
  export declare function contextExport(options?: ContextExportOptions): SnapdomPlugin;
24
24
 
25
25
  export default contextExport;
26
+
27
+ export interface ContextNode {
28
+ tag: string;
29
+ id?: string;
30
+ class?: string;
31
+ box?: [number, number, number, number];
32
+ text?: string;
33
+ state?: Record<string, string | number | boolean>;
34
+ children?: ContextNode[];
35
+ }
36
+ export interface ContextResult {
37
+ root: ContextNode | null;
38
+ truncated: boolean;
39
+ nodes: number;
40
+ }
41
+ export type ToContextOptions = Pick<ContextExportOptions, 'format' | 'maxTextLength' | 'geometry'>;
42
+ declare module '@zumer/snapdom' {
43
+ interface CaptureResult {
44
+ toContext(options: ToContextOptions & { format: 'json' }): Promise<ContextResult>;
45
+ toContext(options: ToContextOptions & { format: 'outline' }): Promise<string>;
46
+ toContext(options?: ToContextOptions): Promise<string | ContextResult>;
47
+ }
48
+ }
package/context-export.js CHANGED
@@ -73,6 +73,8 @@ export function contextExport(options = {}) {
73
73
  maxNodes = 800,
74
74
  geometry = true,
75
75
  } = options
76
+ if (!Number.isInteger(maxNodes) || maxNodes < 1) throw new RangeError('[SnapDOM] context-export: maxNodes must be a positive integer')
77
+ if (!Number.isInteger(maxTextLength) || maxTextLength < 1) throw new RangeError('[SnapDOM] context-export: maxTextLength must be a positive integer')
76
78
  // `needs` names how far the capture has to run. This export reads the live page in
77
79
  // beforeClone, so it does not need pixels: `contextExport({ needs: 'clone' })` skips the
78
80
  // render. It cannot go shallower than that any more, and it should not: a capture that
@@ -106,11 +108,12 @@ export function contextExport(options = {}) {
106
108
  return {
107
109
  context: async (ctx, opts = {}) => {
108
110
  const snap = ctx.__contextSnapshot
109
- if (!snap) throw new Error('[snapdom] context-export: this capture carries no snapshot (it never ran beforeClone). It is not re-read on demand — that would be a different instant.')
111
+ if (!snap) throw new Error('[SnapDOM] context-export: this capture has no snapshot (the plugin did not run with it). It is not re-read later, since that would be a different instant.')
110
112
  // Core carries its image format in every normalized export bag. Only this
111
113
  // plugin's two formats override its factory default.
112
114
  const _format = opts.format === 'json' || opts.format === 'outline' ? opts.format : format
113
115
  const _maxText = opts.maxTextLength ?? maxTextLength
116
+ if (!Number.isInteger(_maxText) || _maxText < 1) throw new RangeError('[SnapDOM] context-export: maxTextLength must be a positive integer')
114
117
  const _geometry = opts.geometry ?? geometry
115
118
 
116
119
  const root = snap.root && project(snap.root, _maxText, _geometry)
package/filter.js CHANGED
@@ -26,7 +26,7 @@ export function filter(options = {}) {
26
26
  } = options;
27
27
 
28
28
  // An unknown preset must not turn the plugin off when a filter string was also given.
29
- if (preset && !presets[preset]) console.warn(`[snapdom] filter: unknown preset ${JSON.stringify(preset)}`);
29
+ if (preset && !presets[preset]) console.warn(`[SnapDOM] filter: unknown preset ${JSON.stringify(preset)}`);
30
30
  const cssFilter = presets[preset] || filterValue;
31
31
 
32
32
  return {
package/gif-export.d.ts CHANGED
@@ -27,3 +27,9 @@ export interface GifExportOptions {
27
27
  /** Adds `result.toGif()`: records a sequence of captures into an animated GIF. */
28
28
  export declare function gifExport(options?: GifExportOptions): SnapdomPlugin;
29
29
  export default gifExport;
30
+
31
+ declare module '@zumer/snapdom' {
32
+ interface CaptureResult {
33
+ toGif(options?: GifExportOptions & { download?: boolean | string }): Promise<Blob>;
34
+ }
35
+ }
package/gif-export.js CHANGED
@@ -39,15 +39,15 @@ export function gifExport(options = {}) {
39
39
  return {
40
40
  gif: async (ctx, opts = {}) => {
41
41
  const el = ctx.element;
42
- if (!el) throw new Error('[snapdom] gif-export: no source element on context');
42
+ if (!el) throw new Error('[SnapDOM] gif-export: there is no element to record.');
43
43
 
44
44
  const recording = frameOptions(ctx, opts, { fps, duration, frames: frameOpt, scale, background }, 'gif-export');
45
45
  const { fps: _fps, count: _count, background: _bg } = recording;
46
46
  const colors = exportOption(ctx, opts, 'maxColors', maxColors);
47
- if (!Number.isFinite(colors)) throw new RangeError('[snapdom] gif-export: maxColors must be finite');
47
+ if (!Number.isFinite(colors)) throw new RangeError('[SnapDOM] gif-export: maxColors must be finite');
48
48
  const _max = Math.min(256, Math.max(2, Math.floor(colors)));
49
49
  const _repeat = exportOption(ctx, opts, 'repeat', repeat);
50
- if (!Number.isInteger(_repeat) || _repeat < -1 || _repeat > 65535) throw new RangeError('[snapdom] gif-export: repeat must be an integer from -1 to 65535');
50
+ if (!Number.isInteger(_repeat) || _repeat < -1 || _repeat > 65535) throw new RangeError('[SnapDOM] gif-export: repeat must be an integer from -1 to 65535');
51
51
  const delayCs = Math.max(2, Math.round(100 / _fps)); // GIF delay unit is 1/100 s
52
52
 
53
53
  let W = 0, H = 0;
package/html-export.d.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  import type { SnapdomPlugin } from '@zumer/snapdom';
9
9
 
10
10
  export interface HtmlExportOptions {
11
+ title?: string;
12
+ lang?: string;
11
13
  /** Wrap in a full `<!DOCTYPE html>` document; false returns `<style>` + fragment. */
12
14
  fullDocument?: boolean;
13
15
  /** Download filename when `toHtml({ download: true })` is used. */
@@ -29,3 +31,9 @@ export interface ToHtmlOptions extends HtmlExportOptions {
29
31
  export declare function htmlExport(options?: HtmlExportOptions): SnapdomPlugin;
30
32
 
31
33
  export default htmlExport;
34
+
35
+ declare module '@zumer/snapdom' {
36
+ interface CaptureResult {
37
+ toHtml(options?: ToHtmlOptions): Promise<string>;
38
+ }
39
+ }
package/html-export.js CHANGED
@@ -20,6 +20,8 @@ export function htmlExport(options = {}) {
20
20
  const {
21
21
  fullDocument = true,
22
22
  filename = 'capture.html',
23
+ title = 'Captured interface',
24
+ lang = 'en',
23
25
  } = options;
24
26
 
25
27
  // Where the serialized markup is parked between afterRender and toHtml. It has to hang
@@ -72,9 +74,8 @@ export function htmlExport(options = {}) {
72
74
  const frozenBody = ctx[BODY];
73
75
  if (typeof frozenBody !== 'string') {
74
76
  throw new Error(
75
- '[snapdom] html-export: this capture produced no render. The plugin\'s afterRender ' +
76
- 'hook never ran, so there is no markup to export (a capture stopped at ' +
77
- 'needs:\'clone\' never reaches the render stage).'
77
+ '[SnapDOM] html-export: this capture produced no render, so there is no HTML ' +
78
+ 'to export (it stopped at needs: \'clone\').'
78
79
  );
79
80
  }
80
81
  const a = ctx.artifacts;
@@ -82,8 +83,9 @@ export function htmlExport(options = {}) {
82
83
  const body = frozenBody;
83
84
 
84
85
  const asDoc = opts.fullDocument ?? fullDocument;
86
+ const escape = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
85
87
  const html = asDoc
86
- ? `<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8">\n<style>${css}</style>\n</head>\n<body>\n${body}\n</body>\n</html>\n`
88
+ ? `<!DOCTYPE html>\n<html lang="${escape(opts.lang ?? lang)}">\n<head>\n<meta charset="utf-8">\n<title>${escape(opts.title ?? title)}</title>\n<style>${css}</style>\n</head>\n<body>\n${body}\n</body>\n</html>\n`
87
89
  : `<style>${css}</style>\n${body}\n`;
88
90
 
89
91
  const dl = opts.download;
package/index.d.ts CHANGED
@@ -11,9 +11,10 @@ export * from './replace-text.js';
11
11
  export * from './timestamp-overlay.js';
12
12
  export * from './redact-inputs.js';
13
13
  export * from './ascii-export.js';
14
- export * from './pdf-image.js';
14
+ export * from './pdf.js';
15
15
  export * from './html-export.js';
16
16
  export * from './gif-export.js';
17
17
  export * from './video-export.js';
18
18
  export * from './agent-map.js';
19
19
  export * from './context-export.js';
20
+ export * from './vector.js';
package/index.js CHANGED
@@ -12,9 +12,10 @@ export { replaceText } from './replace-text.js';
12
12
  export { redactInputs } from './redact-inputs.js';
13
13
  export { filter } from './filter.js';
14
14
  export { colorTint } from './color-tint.js';
15
- export { pdfImage } from './pdf-image.js';
15
+ export { pdf } from './pdf.js';
16
16
  export { agentMap } from './agent-map.js';
17
17
  export { contextExport } from './context-export.js';
18
18
  export { htmlExport } from './html-export.js';
19
19
  export { gifExport } from './gif-export.js';
20
20
  export { videoExport } from './video-export.js';
21
+ export { vector } from './vector.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom-plugins",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Official plugins for SnapDOM",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,18 +11,29 @@
11
11
  "./redact-inputs": "./redact-inputs.js",
12
12
  "./filter": "./filter.js",
13
13
  "./color-tint": "./color-tint.js",
14
- "./pdf-image": "./pdf-image.js",
15
14
  "./agent-map": "./agent-map.js",
16
15
  "./context-export": "./context-export.js",
17
16
  "./html-export": "./html-export.js",
18
17
  "./gif-export": "./gif-export.js",
19
- "./video-export": "./video-export.js"
18
+ "./video-export": "./video-export.js",
19
+ "./pdf": "./pdf.js",
20
+ "./vector": "./vector.js"
20
21
  },
21
22
  "files": [
22
23
  "*.js",
23
24
  "*.d.ts",
24
25
  "README.md",
25
- "LICENSE"
26
+ "LICENSE",
27
+ "pdf/src/",
28
+ "pdf/README.md",
29
+ "pdf/REFERENCE.md",
30
+ "pdf/LICENSE",
31
+ "CHANGELOG.md",
32
+ "vector/src/",
33
+ "vector/svd/",
34
+ "vector/CONTRACT.md",
35
+ "vector/LICENSE",
36
+ "vector/README.md"
26
37
  ],
27
38
  "types": "./index.d.ts",
28
39
  "keywords": [
@@ -42,10 +53,15 @@
42
53
  "url": "https://github.com/zumerlab/snapdom/issues"
43
54
  },
44
55
  "homepage": "https://snapdom.dev/",
56
+ "funding": {
57
+ "type": "github",
58
+ "url": "https://github.com/sponsors/tinchox5"
59
+ },
45
60
  "peerDependencies": {
46
- "@zumer/snapdom": "^3.0.0"
61
+ "@zumer/snapdom": "^3.3.0"
47
62
  },
48
63
  "devDependencies": {
49
64
  "@zumer/snapdom": "file:../.."
50
- }
65
+ },
66
+ "sideEffects": false
51
67
  }
package/pdf/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ZumerLab
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/pdf/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # PDF plugin
2
+
3
+ MIT-licensed PDF export for SnapDOM, replacing the former PDF Pro and `pdf-image` implementations. This source is part of `@zumer/snapdom-plugins` 4.x and requires core 3.x. The 4.x release is available on npm.
4
+
5
+ ```js
6
+ import { snapdom } from '@zumer/snapdom';
7
+ import { pdf } from '@zumer/snapdom-plugins/pdf';
8
+
9
+ const result = await snapdom(element, { plugins: [pdf()] });
10
+ const blob = await result.toPdf({ page: 'a4', margin: 36 });
11
+ await result.toPdf({ page: 'letter', download: 'report.pdf' });
12
+ ```
13
+
14
+ `toPdf()` returns a `Blob`; downloading is opt-in. The exporter preserves the captured appearance and supports selectable, searchable text, links, pagination, repeated table headers, Unicode, transparency and optional form fields. Export options also cover headers, footers, metadata, tagging and encryption. Tagged output alone does not guarantee PDF/UA conformance.
15
+
16
+ Put capture-time measurement options on `pdf()` and paper/output options on `toPdf()`. Structurally redact sensitive source content before capture: covering pixels does not remove searchable text. See the [full reference](REFERENCE.md) and [public types](../pdf.d.ts).
17
+
18
+ ## Live demo and verification
19
+
20
+ From the monorepo root, run `npm run compile` and `npm run site`, then open `/pro/pdf/` or `/playground/`. These demos generate real PDFs from editable DOM samples. The retained `/pro/` URL does not denote a paid product.
21
+
22
+ Run `npm run test:pdf` for the PDF engine suites. See [verification notes](test/README.md).