Skip to content

Commit d87ac01

Browse files
committed
Document plugin system
1 parent 4e21b47 commit d87ac01

2 files changed

Lines changed: 496 additions & 205 deletions

File tree

README.md

Lines changed: 230 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ It captures any HTML element as a scalable SVG image, preserving styles, fonts,
7676
- [noShadows](#no-shadows)
7777
- [Cache control](#cache-control)
7878
- [preCache](#precache--optional-helper)
79+
- [Plugins (BETA)](#plugins-beta)
80+
- [Registering Plugins](#registering-plugins)
81+
- [Plugin Lifecycle Hooks](#plugin-lifecycle-hooks)
82+
- [Context Object](#context-object)
83+
- [Custom Exports via Plugins](#custom-exports-via-plugins)
84+
- [Example: Overlay Filter Plugin](#example-overlay-filter-plugin)
85+
- [Full Plugin Template](#full-plugin-template)
7986
- [Limitations](#limitations)
8087
- [⚡ Performance Benchmarks (Chromium)](#performance-benchmarks)
8188
- [Simple elements](#simple-elements)
@@ -409,6 +416,229 @@ await preCache({
409416
});
410417
```
411418
419+
## Plugins (BETA)
420+
421+
SnapDOM includes a lightweight **plugin system** that allows you to extend or override behavior at any stage of the capture and export process — without touching the core library.
422+
423+
A plugin is a simple object with a unique `name` and one or more lifecycle **hooks**.
424+
Hooks can be synchronous or `async`, and they receive a shared **`context`** object.
425+
426+
### Registering Plugins
427+
428+
**Global registration** (applies to all captures):
429+
430+
```js
431+
import { snapdom } from '@zumer/snapdom';
432+
433+
// You can register instances, factories, or [factory, options]
434+
snapdom.plugins(
435+
myPluginInstance,
436+
[myPluginFactory, { optionA: true }],
437+
{ plugin: anotherFactory, options: { level: 2 } }
438+
);
439+
```
440+
441+
**Per-capture registration** (only for that specific call):
442+
443+
```js
444+
const out = await snapdom(element, {
445+
plugins: [
446+
[overlayFilterPlugin, { color: 'rgba(0,0,0,0.25)' }],
447+
[myFullPlugin, { providePdf: true }]
448+
]
449+
});
450+
```
451+
452+
* **Execution order = registration order** (first registered, first executed).
453+
* **Per-capture plugins** run **before** global ones.
454+
* Duplicates are automatically skipped by `name`; a per-capture plugin with the same `name` overrides its global version.
455+
456+
### Plugin Lifecycle Hooks
457+
458+
| Hook | Purpose |
459+
| ------------------------------ | ------------------------------------------------------------------------------------ |
460+
| `beforeSnap(context)` | Before any clone/style work. Ideal for adjusting global capture options. |
461+
| `beforeClone(context)` | Before DOM cloning. Can modify live DOM (use carefully). |
462+
| `afterClone(context)` | After the element subtree has been cloned. Safe to modify styles in the cloned tree. |
463+
| `beforeRender(context)` | Right before SVG/dataURL serialization. |
464+
| `afterRender(context)` | After serialization (you can inspect `context.svgString` or `context.dataURL`). |
465+
| `beforeExport(context)` | Before each export call (`toPng`, `toSvg`, etc.). |
466+
| `afterExport(context, result)` | After each export call — can transform the returned result. |
467+
| `afterSnap(context)` | Runs **once**, after the **first export** finishes. Perfect for cleanup. |
468+
| `defineExports(context)` | Returns a map of **custom exporters**, e.g. `{ pdf: async (ctx, opts) => Blob }`. |
469+
470+
> Returned values from `afterExport` are chained to the next plugin (transform pipeline).
471+
472+
### Context Object
473+
474+
Every hook receives a single `context` object that contains normalized capture state:
475+
476+
* **Input & options:**
477+
`element`, `debug`, `fast`, `scale`, `dpr`, `width`, `height`, `backgroundColor`, `quality`, `useProxy`, `cache`, `straighten`, `noShadows`, `embedFonts`, `localFonts`, `iconFonts`, `excludeFonts`, `exclude`, `excludeMode`, `filter`, `filterMode`, `fallbackURL`.
478+
479+
* **Intermediate values (depending on stage):**
480+
`clone`, `classCSS`, `styleCache`, `fontsCSS`, `baseCSS`, `svgString`, `dataURL`.
481+
482+
* **During export:**
483+
`context.export = { type, options, url }`
484+
where `type` is the exporter name (`"png"`, `"jpeg"`, `"svg"`, `"blob"`, etc.), and `url` is the serialized SVG base.
485+
486+
> You may safely modify `context` (e.g., override `backgroundColor` or `quality`) — but do so early (`beforeSnap`) for global effects or in `beforeExport` for single-export changes.
487+
488+
489+
## Custom Exports via Plugins
490+
491+
Plugins can add new exports using `defineExports(context)`.
492+
For each export key you return (e.g., `"pdf"`), SnapDOM automatically exposes a helper method named **`toPdf()`** on the capture result.
493+
494+
**Register the plugin (global or per capture):**
495+
496+
```js
497+
import { snapdom } from '@zumer/snapdom';
498+
499+
// global
500+
snapdom.plugins(pdfExportPlugin());
501+
502+
// or per capture
503+
const out = await snapdom(element, { plugins: [pdfExportPlugin()] });
504+
```
505+
506+
**Call the custom export:**
507+
508+
```js
509+
const out = await snapdom(document.querySelector('#report'));
510+
511+
// because the plugin returns { pdf: async (ctx, opts) => ... }
512+
const pdfBlob = await out.toPdf({
513+
// exporter-specific options (width, height, quality, filename, etc.)
514+
});
515+
```
516+
517+
### Example: Overlay Filter Plugin
518+
519+
Adds a translucent overlay or color filter **only** to the captured clone (not your live DOM).
520+
Useful for highlighting or dimming sections before export.
521+
522+
```js
523+
/**
524+
* Ultra-simple overlay filter for SnapDOM (HTML-only).
525+
* Inserts a full-size <div> overlay on the cloned root.
526+
*
527+
* @param {{ color?: string; blur?: number }} [options]
528+
* color: overlay color (rgba/hex/hsl). Default: 'rgba(0,0,0,0.25)'
529+
* blur: optional blur in px (default: 0)
530+
*/
531+
export function overlayFilterPlugin(options = {}) {
532+
const color = options.color ?? 'rgba(0,0,0,0.25)';
533+
const blur = Math.max(0, options.blur ?? 0);
534+
535+
return {
536+
name: 'overlay-filter',
537+
538+
/**
539+
* Add a full-coverage overlay to the cloned HTML root.
540+
* @param {any} context
541+
*/
542+
async afterClone(context) {
543+
const root = context.clone;
544+
if (!(root instanceof HTMLElement)) return; // HTML-only
545+
546+
// Ensure containing block so absolute overlay anchors to the root
547+
if (getComputedStyle(root).position === 'static') {
548+
root.style.position = 'relative';
549+
}
550+
551+
const overlay = document.createElement('div');
552+
overlay.style.position = 'absolute';
553+
overlay.style.left = '0';
554+
overlay.style.top = '0';
555+
overlay.style.right = '0';
556+
overlay.style.bottom = '0';
557+
overlay.style.background = color;
558+
overlay.style.pointerEvents = 'none';
559+
if (blur) overlay.style.filter = `blur(${blur}px)`;
560+
561+
root.appendChild(overlay);
562+
}
563+
};
564+
}
565+
566+
```
567+
568+
**Usage:**
569+
570+
```js
571+
import { snapdom } from '@zumer/snapdom';
572+
573+
// Global registration
574+
snapdom.plugins([overlayFilterPlugin, { color: 'rgba(0,0,0,0.3)', blur: 2 }]);
575+
576+
// Per-capture
577+
const out = await snapdom(document.querySelector('#card'), {
578+
plugins: [[overlayFilterPlugin, { color: 'rgba(255,200,0,0.15)' }]]
579+
});
580+
581+
const png = await out.toPng();
582+
document.body.appendChild(png);
583+
```
584+
585+
> The overlay is injected **only in the cloned tree**, never in your live DOM, ensuring perfect fidelity and zero flicker.
586+
587+
588+
### Full Plugin Template
589+
590+
Use this as a starting point for custom logic or exporters.
591+
592+
```js
593+
export function myPlugin(options = {}) {
594+
return {
595+
/** Unique name used for de-duplication/overrides */
596+
name: 'my-plugin',
597+
598+
/** Early adjustments before any clone/style work. */
599+
async beforeSnap(context) {},
600+
601+
/** Before subtree cloning (use sparingly if touching the live DOM). */
602+
async beforeClone(context) {},
603+
604+
/** After subtree cloning (safe to modify the cloned tree). */
605+
async afterClone(context) {},
606+
607+
/** Right before serialization (SVG/dataURL). */
608+
async beforeRender(context) {},
609+
610+
/** After serialization; inspect context.svgString/context.dataURL if needed. */
611+
async afterRender(context) {},
612+
613+
/** Before EACH export call (toPng/toSvg/toBlob/...). */
614+
async beforeExport(context) {},
615+
616+
/**
617+
* After EACH export call.
618+
* If you return a value, it becomes the result for the next plugin (chaining).
619+
*/
620+
async afterExport(context, result) { return result; },
621+
622+
/**
623+
* Define custom exporters (auto-added as helpers like out.toPdf()).
624+
* Return a map { [key: string]: (ctx:any, opts:any) => Promise<any> }.
625+
*/
626+
async defineExports(context) { return {}; },
627+
628+
/** Runs ONCE after the FIRST export finishes (cleanup). */
629+
async afterSnap(context) {}
630+
};
631+
}
632+
```
633+
634+
**Quick recap:**
635+
636+
* Plugins can modify capture behavior (`beforeSnap`, `afterClone`, etc.).
637+
* You can inject visuals or transformations safely into the cloned tree.
638+
* New exporters defined in `defineExports()` automatically become helpers like `out.toPdf()`.
639+
* All hooks can be asynchronous, run in order, and share the same `context`.
640+
641+
412642
## Limitations
413643
414644
* External images should be CORS-accessible (use `useProxy` option for handling CORS denied)
@@ -431,7 +661,6 @@ Values are **average capture time (ms)** → lower is better.
431661
| Large Scroll (2000×1500) | **0.5 ms** | 0.8 ms | 186.3 ms | 3.2 ms |
432662
| Very Large (4000×2000) | **0.5 ms** | 0.9 ms | 425.9 ms | 3.3 ms |
433663
434-
---
435664
436665
### Complex elements
437666

0 commit comments

Comments
 (0)