Showing preview only (870K chars total). Download the full file or copy to clipboard to get everything.
Repository: zumerlab/snapdom
Branch: main
Commit: 1ba928d9d1c7
Files: 121
Total size: 830.3 KB
Directory structure:
gitextract_5bp_18n4/
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── labels.yml
│ ├── scripts/
│ │ ├── classify-issues.js
│ │ └── update-contributors.js
│ └── workflows/
│ ├── issue-triage.yml
│ ├── label-sync.yml
│ └── update-contributors.yml
├── .gitignore
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── README_CN.md
├── __tests__/
│ ├── api.preCache.more.test.js
│ ├── api.preCache.test.js
│ ├── api.snapdom.more.test.js
│ ├── api.snapdom.test.js
│ ├── core.cache.test.js
│ ├── core.capture.more.test.js
│ ├── core.capture.test.js
│ ├── core.clone.more.test.js
│ ├── core.clone.test.js
│ ├── core.context.test.js
│ ├── core.exporters.test.js
│ ├── core.prepare.test.js
│ ├── cssTools.utils.test.js
│ ├── exporter.download.test.js
│ ├── exporter.toCanvas.more.test.js
│ ├── exporter.toCanvas.test.js
│ ├── exporter.toImg.test.js
│ ├── exporters.jpg-png-svg-webp.test.js
│ ├── index.browser.test.js
│ ├── module.background.test.js
│ ├── module.changeCSS.test.js
│ ├── module.counter.test.js
│ ├── module.fonts.katex.test.js
│ ├── module.fonts.more.more.test.js
│ ├── module.fonts.more.test.js
│ ├── module.fonts.test.js
│ ├── module.iconFonts.more.test.js
│ ├── module.iconFonts.test.js
│ ├── module.lineClamp.test.js
│ ├── module.pseudo.test.js
│ ├── module.snapFetch.test.js
│ ├── module.styles.test.js
│ ├── module.svg.test.js
│ ├── modules.images.test.js
│ ├── snapdom.attributes.test.js
│ ├── snapdom.backgroundColor.test.js
│ ├── snapdom.benchmark.js
│ ├── snapdom.complex.benchmark.js
│ ├── snapdom.delete.test.js
│ ├── snapdom.precache.perf.test.js
│ ├── snapdom.test.js
│ ├── snapdom.vs.htm2canvas.outputfilesize.test.js
│ ├── snapdom.vs.modernscreenshot.outputfilesize.test.js
│ ├── three-shake.test.js
│ ├── utils.browser.more.test.js
│ ├── utils.browser.test.js
│ ├── utils.capture.helpers.test.js
│ ├── utils.css.test.js
│ ├── utils.helpers.test.js
│ ├── utils.image.test.js
│ └── utils.transforms.helpers.test.js
├── docs/
│ ├── CNAME
│ ├── assets/
│ │ └── favicon/
│ │ └── site.webmanifest
│ ├── index.html
│ └── labs.html
├── esbuild.config.mjs
├── eslint.config.cjs
├── package.json
├── plugins/
│ └── html-in-canvas.js
├── src/
│ ├── api/
│ │ ├── preCache.js
│ │ └── snapdom.js
│ ├── core/
│ │ ├── cache.js
│ │ ├── capture.js
│ │ ├── clone.js
│ │ ├── context.js
│ │ ├── exporters.js
│ │ ├── plugins.js
│ │ └── prepare.js
│ ├── exporters/
│ │ ├── download.js
│ │ ├── toBlob.js
│ │ ├── toCanvas.js
│ │ ├── toImg.js
│ │ ├── toJpg.js
│ │ ├── toPng.js
│ │ ├── toSvg.js
│ │ └── toWebp.js
│ ├── index.browser.js
│ ├── index.js
│ ├── modules/
│ │ ├── CSSVar.js
│ │ ├── background.js
│ │ ├── changeCSS.js
│ │ ├── counter.js
│ │ ├── fonts.js
│ │ ├── iconFonts.js
│ │ ├── images.js
│ │ ├── lineClamp.js
│ │ ├── pseudo.js
│ │ ├── rasterize.js
│ │ ├── snapFetch.js
│ │ ├── styles.js
│ │ └── svgDefs.js
│ └── utils/
│ ├── browser.js
│ ├── capture.helpers.js
│ ├── clone.helpers.js
│ ├── css.js
│ ├── debug.js
│ ├── helpers.js
│ ├── image.js
│ ├── index.js
│ ├── prepare.helpers.js
│ └── transforms.helpers.js
├── types/
│ └── snapdom.d.ts
└── vitest.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
martinmuda@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: .github/CONTRIBUTING.md
================================================
## 🤝 Contributing Guide
Thank you for your interest in contributing to **Snapdom**! Your help makes this project better for everyone. Below you’ll find some guidelines to get started.
---
### 📝 **Issues**
* If you find a **bug**, please [open an issue](https://github.com/zumerlab/snapdom/issues/new) and provide as much detail as possible (steps to reproduce, expected behavior, actual behavior, etc.).
* If you have a **feature request** or an idea for improvement:
* You can [open an issue](https://github.com/zumerlab/snapdom/issues/new).
* Or, start a [discussion](https://github.com/zumerlab/snapdom/discussions/new) to explore the idea with the community.
Even small things matter — if you spot a **typo** in the code, docs, or comments, please let us know or submit a fix!
---
### 💡 **Examples**
If you build something cool using **Snapdom**, we’d love to see it! Consider starting a **discussion** to share ideas and get feedback.
---
### 💬 **Discussions**
You’re welcome to use [GitHub Discussions](https://github.com/zumerlab/snapdom/discussions) for:
* Asking questions.
* Sharing feedback or ideas.
* Connecting with other users and contributors.
Let’s keep the tone friendly and constructive!
---
### 🚀 **Pull Requests (PRs)**
* Before starting, it’s a good idea to **discuss large changes** in an issue or discussion.
* Fork the repo, create a branch (e.g. `fix-xyz`, `feature-abc`), and submit your PR.
* Please make sure:
* Your code follows the project’s style (we can help review).
* You add tests or documentation if relevant.
* You link the related issue (if any) in the PR description.
All contributions are reviewed and merged after approval. Don’t worry if you’re new — we’re happy to help!
---
### ⚡ Quick links
* [Open an issue](https://github.com/zumerlab/snapdom/issues/new)
* [Start a discussion](https://github.com/zumerlab/snapdom/discussions/new)
* [Check open PRs](https://github.com/zumerlab/snapdom/pulls)
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: tinchox5
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug'
assignees: ''
---
### Describe it simply:
<!-- Tell us what's happening or what you'd like to see -->
<!-- If you need help please consider using https://github.com/zumerlab/snapdom/discussions -->
### Quick demo to reproduce
<!-- A visual or code issue, screenshots, help a ton -->
<!-- You can use the following codepen template or any playground code (CodeSandbox, etc) -->
<!-- https://codepen.io/pen?template=GgoWPay -->
<!-- Or just paste relevant code snippets -->
### Anything else we should know?
<!-- For example: Device/browser/ Snapdom version -->
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'enhancement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- If you need help please consider using https://github.com/zumerlab/snapdom/discussions -->
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
================================================
FILE: .github/labels.yml
================================================
# Priority labels
- name: "priority: critical"
color: "B60205"
description: "Core functionality broken, causes crashes or data loss, affects most users"
- name: "priority: high"
color: "D93F0B"
description: "Important feature broken or significantly degraded, no practical workaround"
- name: "priority: medium"
color: "E4E669"
description: "Feature partially broken or degraded, workaround exists"
- name: "priority: low"
color: "0E8A16"
description: "Minor issue, cosmetic problem, or question"
# Type labels
- name: "bug"
color: "D73A4A"
description: "Something isn't working"
- name: "enhancement"
color: "A2EEEF"
description: "New feature or request"
- name: "question"
color: "D876E3"
description: "Further information is requested"
- name: "documentation"
color: "0075CA"
description: "Improvements or additions to documentation"
# Status labels
- name: "🔧 in development"
color: "FBCA04"
description: "This issue is being actively worked on"
- name: "need repro"
color: "E4E669"
description: "A reproducible test case is needed to investigate"
- name: "duplicate"
color: "CFD3D7"
description: "This issue or pull request already exists"
- name: "wontfix"
color: "FFFFFF"
description: "This will not be worked on"
- name: "good first issue"
color: "7057FF"
description: "Good for newcomers"
# Browser/environment labels
- name: "safari-hates-me"
color: "0052CC"
description: "Issue specific to Safari browser"
- name: "Fails on Firefox"
color: "FF6B35"
description: "Issue specific to Firefox browser"
# Feature area labels
- name: "plugin idea"
color: "BFD4F2"
description: "Could be implemented as a plugin"
- name: "experimental"
color: "F9D0C4"
description: "Experimental feature or idea"
================================================
FILE: .github/scripts/classify-issues.js
================================================
// .github/scripts/classify-issues.js
// Classifies all open issues by importance and applies priority labels.
// Usage: GITHUB_TOKEN=<token> node .github/scripts/classify-issues.js
// Dry-run (no changes): GITHUB_TOKEN=<token> node .github/scripts/classify-issues.js --dry-run
import https from 'https';
const repo = 'zumerlab/snapdom';
const [owner, repoName] = repo.split('/');
const token = process.env.GITHUB_TOKEN;
const dryRun = process.argv.includes('--dry-run');
if (!token) {
console.error('Error: GITHUB_TOKEN environment variable is required.');
process.exit(1);
}
// ─── Classification rules ────────────────────────────────────────────────────
// Keywords for each priority tier
const CRITICAL_KEYWORDS = [
'the source image cannot be decoded',
'crash', 'data loss',
'报错', // Chinese: "reports an error"
];
const HIGH_PRIORITY_KEYWORDS = [
'pseudo-element', 'pseudo element',
'iframe',
'font', '字体',
'background-image', 'background image',
'cross-origin', 'cross origin',
'checkbox',
'radio button', 'radio ',
'webkit-text-stroke', 'text-stroke',
'transform', 'matrix',
'katex',
'scrollbar', 'scroll position',
'border style',
'svg image',
'video',
];
const LOW_KEYWORDS = [
'why ', 'how can', 'how do',
'is it possible', 'does it support',
'configuration option', 'timeout',
'cors',
'wicg', 'experiment',
'announcement', 'inactivity',
'plugin idea',
];
const SAFARI_KEYWORDS = ['safari', 'ios', 'webkit', 'iphone', 'ipad', 'apple'];
const FIREFOX_KEYWORDS = ['firefox', 'mozilla', 'gecko'];
/**
* Determines the priority label for a given issue based on its title and body.
* Returns one of: 'priority: critical', 'priority: high', 'priority: medium', 'priority: low'
*/
function classifyPriority(issue) {
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = title + ' ' + body;
if (CRITICAL_KEYWORDS.some(k => text.includes(k))) {
return 'priority: critical';
}
if (LOW_KEYWORDS.some(k => text.includes(k))) {
return 'priority: low';
}
if (HIGH_PRIORITY_KEYWORDS.some(k => text.includes(k))) {
return 'priority: high';
}
return 'priority: medium';
}
/**
* Additional type/browser labels to add based on content.
*/
function classifyExtra(issue) {
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = title + ' ' + body;
const extra = [];
if (SAFARI_KEYWORDS.some(k => text.includes(k))) extra.push('safari-hates-me');
if (FIREFOX_KEYWORDS.some(k => text.includes(k))) extra.push('Fails on Firefox');
return extra;
}
// ─── GitHub API helpers ───────────────────────────────────────────────────────
function request(method, path, body) {
return new Promise((resolve, reject) => {
const data = body ? JSON.stringify(body) : null;
const options = {
hostname: 'api.github.com',
path,
method,
headers: {
'User-Agent': 'classify-issues-script',
'Accept': 'application/vnd.github.v3+json',
'Authorization': `token ${token}`,
...(data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {})
}
};
const req = https.request(options, (res) => {
let buf = '';
res.on('data', c => buf += c);
res.on('end', () => {
if (res.statusCode >= 400) {
reject(new Error(`HTTP ${res.statusCode}: ${buf}`));
} else {
resolve(buf ? JSON.parse(buf) : null);
}
});
});
req.on('error', reject);
if (data) req.write(data);
req.end();
});
}
async function fetchOpenIssues() {
const issues = [];
let page = 1;
while (true) {
const batch = await request('GET', `/repos/${owner}/${repoName}/issues?state=open&per_page=100&page=${page}`);
if (!batch || batch.length === 0) break;
// Exclude pull requests (GitHub includes PRs in /issues endpoint)
issues.push(...batch.filter(i => !i.pull_request));
if (batch.length < 100) break;
page++;
}
return issues;
}
async function addLabels(issueNumber, labels) {
await request('POST', `/repos/${owner}/${repoName}/issues/${issueNumber}/labels`, { labels });
}
// ─── Main ─────────────────────────────────────────────────────────────────────
(async () => {
console.log(`Fetching open issues for ${repo}…`);
const issues = await fetchOpenIssues();
console.log(`Found ${issues.length} open issues.\n`);
if (dryRun) {
console.log('DRY RUN — no labels will be applied.\n');
}
const summary = { critical: [], high: [], medium: [], low: [] };
for (const issue of issues) {
const existingLabels = issue.labels.map(l => l.name);
const hasPriority = existingLabels.some(l => l.startsWith('priority:'));
const priorityLabel = classifyPriority(issue);
const extraLabels = classifyExtra(issue).filter(l => !existingLabels.includes(l));
const newLabels = [
...(hasPriority ? [] : [priorityLabel]),
...extraLabels
];
const level = priorityLabel.replace('priority: ', '');
summary[level].push(`#${issue.number}: ${issue.title}`);
if (newLabels.length === 0) {
console.log(`#${issue.number} — no new labels needed (existing: ${existingLabels.join(', ') || 'none'})`);
continue;
}
console.log(`#${issue.number} [${priorityLabel}] — adding: ${newLabels.join(', ')}`);
if (!dryRun) {
try {
await addLabels(issue.number, newLabels);
} catch (err) {
console.error(` ✗ Failed to label #${issue.number}: ${err.message}`);
}
}
}
console.log('\n── Classification Summary ──────────────────────────');
for (const [level, list] of Object.entries(summary)) {
console.log(`\n${level.toUpperCase()} (${list.length}):`);
list.forEach(t => console.log(` ${t}`));
}
})();
================================================
FILE: .github/scripts/update-contributors.js
================================================
// .github/scripts/update-contributors.js
import { writeFileSync, readFileSync } from 'fs';
import https from 'https';
const repo = 'zumerlab/snapdom';
const readmePaths = ['README.md', 'README_CN.md'];
function fetchContributors() {
const options = {
hostname: 'api.github.com',
path: `/repos/${repo}/contributors`,
headers: { 'User-Agent': 'GitHub Action', 'Accept': 'application/vnd.github.v3+json' }
};
return new Promise((resolve, reject) => {
https
.get(options, (res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
if (res.statusCode === 200) {
resolve(JSON.parse(body));
} else {
reject(new Error(`GitHub API error: ${res.statusCode}`));
}
});
})
.on('error', reject);
});
}
function buildHTML(contributors) {
return (
'\n<p>\n' +
contributors
.map((c) => {
const avatar = `<a href="${c.html_url}" title="${c.login}"><img src="${c.avatar_url}&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="${c.login}"/></a>`;
return avatar;
})
.join('\n') +
'\n</p>\n'
);
}
function updateReadmes(contributorHTML) {
for (const path of readmePaths) {
//try {
const content = readFileSync(path, 'utf8');
const updated = content.replace(
/<!-- CONTRIBUTORS:START -->([\s\S]*?)<!-- CONTRIBUTORS:END -->/,
`<!-- CONTRIBUTORS:START -->${contributorHTML}<!-- CONTRIBUTORS:END -->`
);
writeFileSync(path, updated);
//} catch () {
//}
}
}
fetchContributors()
.then((contributors) => {
const filtered = contributors.filter(
(c) => c.type !== 'Bot' && c.login !== 'github-actions[bot]'
);
const html = buildHTML(filtered);
updateReadmes(html);
})
.catch((err) => {
console.error('Error fetching contributors:', err);
process.exit(1);
});
================================================
FILE: .github/workflows/issue-triage.yml
================================================
name: Auto-label Issues
on:
issues:
types: [opened, edited]
jobs:
triage:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Label bug reports
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = title + ' ' + body;
const labelsToAdd = [];
const existingLabels = issue.labels.map(l => l.name);
// ── Keyword patterns ──────────────────────────────────────────────
const BUG_KEYWORDS = /bug|error|broken|crash|fail|not work|doesn't work|cannot|can't|unable|wrong|incorrect|missing|disappear|blank|empty|distort/;
const FEATURE_KEYWORDS = /feature request|suggestion|enhancement|would like|wish|please add/;
const QUESTION_KEYWORDS = /^(why|how|what|can you|is it possible|does it|do you)|\bquestion\b|\bhelp\b/;
const SAFARI_KEYWORDS = /safari|ios|webkit|iphone|ipad|apple/;
const FIREFOX_KEYWORDS = /firefox|mozilla|gecko/;
const CRITICAL_KEYWORDS = /crash|data loss|cannot capture|completely broken|decode|source image/;
const HIGH_KEYWORDS = /pseudo.?element|iframe|font|background.?image|cross.?origin|checkbox|radio|text.?stroke|transform|matrix/;
const LOW_BUG_KEYWORDS = /configuration option|timeout setting|cors support/;
// ── Detect issue type ─────────────────────────────────────────────
const isBug = BUG_KEYWORDS.test(text);
const isFeature = FEATURE_KEYWORDS.test(text) && !isBug;
const isQuestion = QUESTION_KEYWORDS.test(text) && !isBug;
// ── Apply type labels ─────────────────────────────────────────────
if (isBug && !existingLabels.includes('bug')) labelsToAdd.push('bug');
if (isFeature && !existingLabels.includes('enhancement')) labelsToAdd.push('enhancement');
if (isQuestion && !existingLabels.includes('question')) labelsToAdd.push('question');
// ── Apply browser labels ──────────────────────────────────────────
if (SAFARI_KEYWORDS.test(text) && !existingLabels.includes('safari-hates-me')) labelsToAdd.push('safari-hates-me');
if (FIREFOX_KEYWORDS.test(text) && !existingLabels.includes('Fails on Firefox')) labelsToAdd.push('Fails on Firefox');
// ── Assign priority for bugs (skip if already labelled) ───────────
const hasPriority = existingLabels.some(l => l.startsWith('priority:'));
if (isBug && !hasPriority) {
if (CRITICAL_KEYWORDS.test(text)) {
labelsToAdd.push('priority: critical');
} else if (HIGH_KEYWORDS.test(text)) {
labelsToAdd.push('priority: high');
} else if (LOW_BUG_KEYWORDS.test(text)) {
labelsToAdd.push('priority: low');
} else {
labelsToAdd.push('priority: medium');
}
}
if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labelsToAdd
});
console.log(`Added labels: ${labelsToAdd.join(', ')} to issue #${issue.number}`);
}
================================================
FILE: .github/workflows/label-sync.yml
================================================
name: Sync Labels
on:
push:
branches:
- main
paths:
- '.github/labels.yml'
workflow_dispatch:
jobs:
sync-labels:
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Sync labels
uses: EndBug/label-sync@v2
with:
config-file: .github/labels.yml
delete-other-labels: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/update-contributors.yml
================================================
name: Update Contributors in READMES
on:
push:
branches:
- main
schedule:
- cron: '0 0 * * 0'
workflow_dispatch:
jobs:
update-contributors:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Generate contributors list
run: node .github/scripts/update-contributors.js
- name: Commit and push if changed
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
if ! git diff --quiet; then
git add README.md README_CN.md
git commit -m "chore: update contributors list"
git push
fi
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
package-lock.json
drafts/
demos/
__tests__/__screenshots__
================================================
FILE: .vscode/settings.json
================================================
{
"liveServer.settings.port": 5501
}
================================================
FILE: CHANGELOG.md
================================================
### Changelog
All notable changes to this project will be documented in this file.
#### [v2.5.0](https://github.com/zumerlab/snapdom/compare/v2.1.0...v2.5.0)
> 17 March 2026
- fix: CSS vars perf, scrollbar styles, SVG image inline, nested line-clamp, iframe pseudos & isolation, Tailwind border (#334 #341 #348 #362 #371 #372 #386) [`#387`](https://github.com/zumerlab/snapdom/pull/387)
- fix: enable image download on iOS via Web Share API [`#384`](https://github.com/zumerlab/snapdom/pull/384)
- feat(scrollbar): implement custom scrollbar style collection for capture, ensuring styles are applied correctly. Closes #334 [`#334`](https://github.com/zumerlab/snapdom/issues/334)
- feat(styles): normalize Tailwind border styles in capture and inlineAllStyles to ensure consistent output. Closes #362 [`#362`](https://github.com/zumerlab/snapdom/issues/362)
- feat(lineClamp): introduce lineClampTree function to apply line-clamp to nested elements, enhancing ellipsis rendering. Closes #386 [`#386`](https://github.com/zumerlab/snapdom/issues/386)
- test(styles): add tests for excluding CSS properties from snapshots, ensuring fidelity with CSS variables. Closes #348 [`#348`](https://github.com/zumerlab/snapdom/issues/348)
- test(capture): add test for iframe CSS isolation to ensure wrapper div does not inherit iframe styles. Closes #372 [`#372`](https://github.com/zumerlab/snapdom/issues/372)
- refactor(capture): replace getComputedStyle with getStyle for improved iframe support and consistency across style retrieval. Closes #371 [`#371`](https://github.com/zumerlab/snapdom/issues/371)
- feat(pseudo): implement suppression of native ::before/::after pseudo-elements in cloned styles to prevent double rendering. Closes #359 [`#359`](https://github.com/zumerlab/snapdom/issues/359)
- fix(capture): update Safari padding logic to avoid edge clipping by applying padding only when necessary based on bounding box transforms. Closes #333 [`#333`](https://github.com/zumerlab/snapdom/issues/333)
- test(fonts): add test for cross-origin CSS support in embedCustomFonts function, verifying correct handling of custom CDN stylesheets. Closes #309 [`#309`](https://github.com/zumerlab/snapdom/issues/309)
- feat(fonts): add fontStylesheetDomains option to support cross-origin CSS fetching, enhancing font loading capabilities. Closes #309, closes #370 [`#309`](https://github.com/zumerlab/snapdom/issues/309) [`#370`](https://github.com/zumerlab/snapdom/issues/370)
- feat(styles): add support for capturing -webkit-text-stroke properties in Safari to enhance style snapshot accuracy. Closes #340 [`#340`](https://github.com/zumerlab/snapdom/issues/340)
- fix(styles): normalize inline styles to ensure !important rules in stylesheets correctly override inline styles in clones. Fixes #328. [`#328`](https://github.com/zumerlab/snapdom/issues/328)
- refactor: improve dimension handling in deepClone and createCheckboxRadioReplacement functions for better accuracy and consistency. Closes #321. See #378 [`#321`](https://github.com/zumerlab/snapdom/issues/321)
- refactor: enhance checkbox/radio replacement for Firefox with SVG implementation for consistent rendering and improved styling. Closes #290 [`#290`](https://github.com/zumerlab/snapdom/issues/290)
- fix(background): resolve relative URLs and fallback to `background` shorthand for url() when background-image is empty. Closes #343 [`#343`](https://github.com/zumerlab/snapdom/issues/343)
- fix: enable image download on iOS via Web Share API [`#383`](https://github.com/zumerlab/snapdom/issues/383)
- feat(tests): add comprehensive test coverage for various modules including exporters, utils, and modules to improve overall code reliability [`06cc896`](https://github.com/zumerlab/snapdom/commit/06cc8962709e5ce651b54b67c63c38fe5ecc498d)
- feat(debug): introduce debug option to log suppressed errors for troubleshooting, enhancing error visibility during capture processes [`f107bbe`](https://github.com/zumerlab/snapdom/commit/f107bbef52521fdd92b77987b44b512313f3b88d)
- fix: Firefox checkbox radio replacement [`b97e553`](https://github.com/zumerlab/snapdom/commit/b97e5539849d08dc871b9a2e486c9505bdfc081e)
- refactor(cache): implement EvictingMap for cache management to limit memory usage and improve performance [`212cd4f`](https://github.com/zumerlab/snapdom/commit/212cd4f0471613b68a47427a1e23f7d076d303de)
- refactor(styles): improve height handling for transparent wrappers to support margin collapsing and enhance layout stability [`c50ccef`](https://github.com/zumerlab/snapdom/commit/c50ccefe931e4809d12c8993dde6aba3f9b46a54)
- fix(styles): prevent overriding border styles when using border-image, and improve getStyle fallback handling [`a5857c5`](https://github.com/zumerlab/snapdom/commit/a5857c5c49febe3d39729f214ca29914928af34a)
- feat(images): add support for inlining SVG <image> elements as data URLs, addressing #341. [`f7d4616`](https://github.com/zumerlab/snapdom/commit/f7d46160913bcf5dad26be0103ef01dc7d29243c)
- feat(safari): implement font and image decode warmup for Safari to address WebKit Bug #219770, enhancing capture reliability [`ad450ce`](https://github.com/zumerlab/snapdom/commit/ad450ce8a10a7094ecbde0ee20db626fabe78423)
- test(getStyle): add tests to ensure getStyle never returns undefined for elements and pseudo-elements [`83c3854`](https://github.com/zumerlab/snapdom/commit/83c3854dc520a0e944a6cf1d23dbc5cdaf8e520a)
- refactor(snapdom): streamline plugin exports by consolidating export functions into a loop for improved maintainability [`ca35387`](https://github.com/zumerlab/snapdom/commit/ca353871b2b696252ddd21572ae31106b39d3c76)
- feat(capture): enhance DOM capture dimensions for root elements by measuring scroll dimensions and using a temporary container for accurate height and width calculations [`31e50f2`](https://github.com/zumerlab/snapdom/commit/31e50f27b3069527cc83f1fe8990c3a8c376e35c)
- fix(css): enhance getWindowForElement and getStyle functions to handle cross-document scenarios and improve fallback logic [`b0fbc8d`](https://github.com/zumerlab/snapdom/commit/b0fbc8d44745811a7c5627e34ef764e02ff188f7)
- refactor(context): remove inline cache policy normalization and import from cache module for improved code organization [`0492756`](https://github.com/zumerlab/snapdom/commit/04927566faf919c7a3df07287f799b152e5d4c8f)
- feat(safari): add `safariWarmupAttempts` option to optimize font and image decoding for improved capture performance [`2474c05`](https://github.com/zumerlab/snapdom/commit/2474c0528051940e503c08ca52861042e57cc880)
- fix(styles): prevent width constraints on inline and specific tags to avoid text wrapping issues [`674ef27`](https://github.com/zumerlab/snapdom/commit/674ef276bcad8e4107c57fee3e976083590a5dd0)
- chore: update contributors list [`b30de75`](https://github.com/zumerlab/snapdom/commit/b30de75ab66cac137334a0ef68ca2bf9133f88c4)
- docs: update README files to replace NPM version badge with weekly downloads badge [`8e12d01`](https://github.com/zumerlab/snapdom/commit/8e12d01fbeb861989eb1d60c534596c55ce9207a)
- chore(.gitignore): add 'demos/' directory to .gitignore to exclude demo files from version control [`fa34905`](https://github.com/zumerlab/snapdom/commit/fa34905450d889e48b9b943c941f29386627acc5)
- refactor(prepare): simplify deepClone call by removing redundant element argument for cleaner code [`399bfaf`](https://github.com/zumerlab/snapdom/commit/399bfaf127bb4416200068334dc069a5bfcef2ab)
- fix(styles): adjust inline style for timestamp demo to prevent text wrapping [`b88e8d7`](https://github.com/zumerlab/snapdom/commit/b88e8d74f67fe67e3ac610972c53ff49752f6b74)
- fix(snapdom): remove redundant safariWarmup reset to improve iteration logic [`72d3fb7`](https://github.com/zumerlab/snapdom/commit/72d3fb72968d136f1c71d234d57272ce0f3fb6e1)
- Merge PR #384: enable image download on iOS via Web Share API [`05bc67c`](https://github.com/zumerlab/snapdom/commit/05bc67c76dbe6cc02946773de8413d48e314b3d9)
- Merge main into dev (2.1.0) [`5f5ab34`](https://github.com/zumerlab/snapdom/commit/5f5ab345194832225e311421d177963ce3c4c59e)
#### [v2.1.0](https://github.com/zumerlab/snapdom/compare/v2.0.2...v2.1.0)
> 10 March 2026
- fix(background): inline background-image inside shadow DOM hosts [`#379`](https://github.com/zumerlab/snapdom/pull/379)
- Update URL handling to use location.origin in fonts.js [`#380`](https://github.com/zumerlab/snapdom/pull/380)
- fix: use nodeMap for source-clone alignment in inlinePseudoElements [`#381`](https://github.com/zumerlab/snapdom/pull/381)
- fix(background): properly inline background-image inside shadow DOM hosts [`#318`](https://github.com/zumerlab/snapdom/issues/318)
- update demo site [`4de1850`](https://github.com/zumerlab/snapdom/commit/4de1850d84d1698e8574fe405007fb47d6a677ea)
- feat: classify open issues by importance with priority labels and triage workflows [`246a4c4`](https://github.com/zumerlab/snapdom/commit/246a4c43eef13fe8b654e297f52a639a7ad670b1)
- fix: Enhanced font embedding functionality for dynamically injected stylesheets [`3d4985a`](https://github.com/zumerlab/snapdom/commit/3d4985a6d40963c30ff188207a62ac1e287709ba)
- fix: resolve CSS transform double-scale bug (issue #321) [`d41504b`](https://github.com/zumerlab/snapdom/commit/d41504b8dcf94454a331337c49d74928d533f49a)
- fix: improve demo capture functionality with Safari support and locking mechanism [`2cb3856`](https://github.com/zumerlab/snapdom/commit/2cb3856e55f0f1d8e0d2ac050d549705203fc6ae)
- refactor: update build configuration for legacy and ESM outputs, removing module structure and adding subpath exports [`94f6289`](https://github.com/zumerlab/snapdom/commit/94f62897054f37923c5cd3e4e2d3a57a0fde8db4)
- docs: update README to reflect changes in SnapDOM ESM build structure and usage instructions [`8e5a710`](https://github.com/zumerlab/snapdom/commit/8e5a7103b45ae2d326a93218c977a371407d8cec)
- fix: only change to location.origin when treating inline styles in font.js [`94c91c6`](https://github.com/zumerlab/snapdom/commit/94c91c61d57b79a083c75d27aa3025beb1dcb535)
- fix: validate fallback image data before setting source [`2c754fe`](https://github.com/zumerlab/snapdom/commit/2c754fec37e3ce18c512ff3ee61e386fcf780589)
- fix: ensure image is only appended if data URL is valid [`46957c5`](https://github.com/zumerlab/snapdom/commit/46957c51c49d766db5ad60357f5664a7cf500049)
- fix: ensure valid CSS text is fetched for font links [`201209f`](https://github.com/zumerlab/snapdom/commit/201209faad857dd21a01ebc2ae5dc740a33819ce)
- Merge pull request #301 from Amyuan23/fix/svg-root-font-size [`5bd53ba`](https://github.com/zumerlab/snapdom/commit/5bd53ba4887dba4664589b51651747809a89f8ab)
- Merge pull request #350 from ZiuChen/fix/remote-katex-font [`3cbbd57`](https://github.com/zumerlab/snapdom/commit/3cbbd577eae0c751fbac41cd3b3ff0aeb251ca0e)
- Merge pull request #378 from FlavioLimaMindera/fix-scale-image-issue-321 [`e53f2f8`](https://github.com/zumerlab/snapdom/commit/e53f2f8c4a83617a14c168c0c2615bde283d4696)
- Fix: Inherit root font-size in SVG output [`3bdf300`](https://github.com/zumerlab/snapdom/commit/3bdf300ce417d56b928ea3c1b7258103a35f2445)
- Merge pull request #374 from kohaiy/patch-1 [`0b21142`](https://github.com/zumerlab/snapdom/commit/0b21142b87d1874aaaa88bc7fc9630eb506ab958)
#### [v2.0.2](https://github.com/zumerlab/snapdom/compare/v2.0.1...v2.0.2)
> 20 January 2026
- Fix bug when captured element is SVG. Closes #324 [`#324`](https://github.com/zumerlab/snapdom/issues/324)
- Improve docs for blob [`#352`](https://github.com/zumerlab/snapdom/issues/352)
#### [v2.0.1](https://github.com/zumerlab/snapdom/compare/v2.0.0...v2.0.1)
> 26 November 2025
- Fix spaces. Closes #326 [`#326`](https://github.com/zumerlab/snapdom/issues/326)
- Fix download options. Closes #323 [`#323`](https://github.com/zumerlab/snapdom/issues/323)
- Fix Safari Image Issue. See #330 [`ba80b6f`](https://github.com/zumerlab/snapdom/commit/ba80b6f66393886298cdb2fc7ce134d1824184f7)
- Minify mjs version [`9155d4f`](https://github.com/zumerlab/snapdom/commit/9155d4fa2e2fc4bb6a0e3d7a991fc75818644084)
- Add a re-export for preCache. See #332 [`acc5b79`](https://github.com/zumerlab/snapdom/commit/acc5b79a0c8d38de9f6dea5b98bf1179b54ef671)
### [v2.0.0](https://github.com/zumerlab/snapdom/compare/v2.0.0-dev.4...v2.0.0)
> 18 November 2025
- V2 release!! [`#319`](https://github.com/zumerlab/snapdom/pull/319)
#### [v2.0.0-dev.4](https://github.com/zumerlab/snapdom/compare/v2.0.0-dev.3...v2.0.0-dev.4)
> 18 November 2025
- Feature enable tree-shakeable code [`ebb7b6a`](https://github.com/zumerlab/snapdom/commit/ebb7b6add3b47c75a450e0264d628837303def5d)
- Fix bug when img has height with % units. Closes #268 [`#268`](https://github.com/zumerlab/snapdom/issues/268)
- Fix regression to process MathJax [`7ef116c`](https://github.com/zumerlab/snapdom/commit/7ef116cdbbfcba0793cd918b2ba8ad94e063f74f)
- Fix bug See #316 [`7efbede`](https://github.com/zumerlab/snapdom/commit/7efbede5970ed09982c06f8cfa3fe45d990d8fdf)
#### [v2.0.0-dev.3](https://github.com/zumerlab/snapdom/compare/v2.0.0-dev.2...v2.0.0-dev.3)
> 11 November 2025
- Reorganice helper functions [`d1fd982`](https://github.com/zumerlab/snapdom/commit/d1fd98240459f07897311a42e09c1ad3e3a48c62)
- Perf improvement [`daf0eca`](https://github.com/zumerlab/snapdom/commit/daf0eca47c0d11828e1a702b6d64d8ab7450581d)
- Fix placeholder dimensions when image loading fails [`e44b9d9`](https://github.com/zumerlab/snapdom/commit/e44b9d947efaf28fec8976dc13b398788d461d52)
#### [v2.0.0-dev.2](https://github.com/zumerlab/snapdom/compare/v2.0.0-dev.1...v2.0.0-dev.2)
> 9 November 2025
- Integrate createBackground into toCanvas. Closes #297 [`#297`](https://github.com/zumerlab/snapdom/issues/297)
- Add Chinese translation of README.md (readme_cn.md) [`#298`](https://github.com/zumerlab/snapdom/issues/298)
- Adjust final dimensions when excludeMode: remove. See #294 [`a860827`](https://github.com/zumerlab/snapdom/commit/a8608271ffd9b891ec815fa7e3130c0e1be45307)
- Improve material icon / symbols. See #304 [`526c4c8`](https://github.com/zumerlab/snapdom/commit/526c4c8e6874b2dab6110c8ac3361a29b1dc91de)
- Add XHTML sanitize. See #282 [`0039301`](https://github.com/zumerlab/snapdom/commit/003930196ed6e11c5dd54fce75d814046a36637d)
- Add detection to Baidu on iOS. Also detect other apps/browsers on iOS. See #295 [`97e6dff`](https://github.com/zumerlab/snapdom/commit/97e6dffa34440d157b0b2b1afc5267d7b15c1d7b)
- add support for text-underline-offset. See #303 [`fb603bc`](https://github.com/zumerlab/snapdom/commit/fb603bca971a10577215c436b73ba5468a4255ae)
- add support for text-underline-offset. See#303 [`0b93c0d`](https://github.com/zumerlab/snapdom/commit/0b93c0de7a720a7c6708a50a153de86ba6f2684e)
- fix: improve CSS src property parsing in font faces [`dbe52a1`](https://github.com/zumerlab/snapdom/commit/dbe52a121a7d68441570c02ae32c607907e188dc)
#### [v2.0.0-dev.1](https://github.com/zumerlab/snapdom/compare/v2.0.0-dev.0...v2.0.0-dev.1)
> 23 October 2025
- Add basic support for icons with ligature such as material-icons. Closes #275 [`#275`](https://github.com/zumerlab/snapdom/issues/275)
- Replace straighten with outerTransforms, and noShadows with outerShadows [`902f032`](https://github.com/zumerlab/snapdom/commit/902f032a43ed4919701765d89818c7782902b403)
- Fix straighten regression [`60c6569`](https://github.com/zumerlab/snapdom/commit/60c6569ebcfbc9ea562c9bddfe9f01c6ea4136db)
#### [v2.0.0-dev.0](https://github.com/zumerlab/snapdom/compare/v1.9.14...v2.0.0-dev.0)
> 14 October 2025
- Document plugin system [`d87ac01`](https://github.com/zumerlab/snapdom/commit/d87ac01fcf0beb8779afbe2be709aa1b35cf7113)
- First plugin and exporter draft [`5da0948`](https://github.com/zumerlab/snapdom/commit/5da09483b82e18ca0fc6873393b9d6830632fcfc)
- Fix subpixel bug. See #261 [`465950b`](https://github.com/zumerlab/snapdom/commit/465950b18e68ce0faf94b229bd65511128cd18a7)
- Update demos with plugins [`50e6c4f`](https://github.com/zumerlab/snapdom/commit/50e6c4f85d74bb769fcd7f753f92a3c5be4536c6)
- Update plugin system [`4e21b47`](https://github.com/zumerlab/snapdom/commit/4e21b475fdc42c6eade9cb4dada5bb5ffeb71978)
- Enhance external SVG defs. See #262 [`cd4a7fb`](https://github.com/zumerlab/snapdom/commit/cd4a7fbf0e2d9fc1651c06d5c5f5a3a8f0f54329)
- First plugin system draft [`9c6b91b`](https://github.com/zumerlab/snapdom/commit/9c6b91bc4352de3d323c0746a4e3040058dad519)
- Fix complex canvas render on Safari. See #263 [`2697207`](https://github.com/zumerlab/snapdom/commit/2697207c98373867bcbb91d4afb73d3820c878d3)
- Enhance CSS vars detection. See #262 [`6655303`](https://github.com/zumerlab/snapdom/commit/665530377bceff05ff9c1570301019bd95370a9c)
- Fix counter CSS reset and bug when exist background-image. See #265 [`3c29997`](https://github.com/zumerlab/snapdom/commit/3c299978e2a4cd4c2daf07d64de5772b28e880b8)
- FIx excludeFonts defs. See #260 [`84b1770`](https://github.com/zumerlab/snapdom/commit/84b1770dd2e6e487e22afcd04c2d34cd0490528c)
- Fix local register [`9c4508e`](https://github.com/zumerlab/snapdom/commit/9c4508e0cdeb445b76babd71483fe154e0e2ee3e)
- Enable use built-in exporters in custom exporter [`5c6fe36`](https://github.com/zumerlab/snapdom/commit/5c6fe367101d8dfec710372d2b0a7362da3597a3)
- Fix background-repeat. See #259 [`0ad5fa4`](https://github.com/zumerlab/snapdom/commit/0ad5fa4ee56e417016ce495ea299cf4a3c586f6a)
- Fix export name format jpg -> jpeg [`47d532a`](https://github.com/zumerlab/snapdom/commit/47d532a98be096829b592295527c1a6428d6a5d8)
- Update roadmap [`dff59b9`](https://github.com/zumerlab/snapdom/commit/dff59b9ea62fed09a9146fd1e9d897dace30a37b)
- Fix local plugin registration [`0997618`](https://github.com/zumerlab/snapdom/commit/0997618fb2e716ead23bfbe4824bb8365987ccd4)
#### [v1.9.14](https://github.com/zumerlab/snapdom/compare/v1.9.13...v1.9.14)
> 5 October 2025
- Recompile builds [`fca9e00`](https://github.com/zumerlab/snapdom/commit/fca9e00d49bb675d6b6103ba2de3f898c85c5578)
#### [v1.9.13](https://github.com/zumerlab/snapdom/compare/v1.9.12-dev.4...v1.9.13)
> 5 October 2025
- Improve CSS vars detection. Closes #255 [`#255`](https://github.com/zumerlab/snapdom/issues/255)
- Fix toImg() dimensions when scale==1. Closes #254 [`#254`](https://github.com/zumerlab/snapdom/issues/254)
- Enhance web fonts detection on deph relative paths. See #253 [`e2a8c45`](https://github.com/zumerlab/snapdom/commit/e2a8c454fbab7590f39f77da544193f8e5af13ab)
- Add two new options to control transforms and shadows on root element [`8c9a75f`](https://github.com/zumerlab/snapdom/commit/8c9a75f77940221107a6005e458514cca981b2eb)
- Add toSvg() in replacement of toImg() [`10e2043`](https://github.com/zumerlab/snapdom/commit/10e2043182672b42dfbda4268fb17f75bc3b561a), [`122317e`](https://github.com/zumerlab/snapdom/commit/122317eb0301f8375cc23ea8f3fd2361d1b759a4)
- Improve relative path detection. See #253 [`34158e0`](https://github.com/zumerlab/snapdom/commit/34158e0f5cf8f797cd49416f090d06d2d233542d)
- Lint code [`c754981`](https://github.com/zumerlab/snapdom/commit/c7549812a018d28809e0e2b314c973abe0cd542c)
- Lint tests [`ee974ed`](https://github.com/zumerlab/snapdom/commit/ee974ede694f324f674b688aa7cce16f7ec30a90)
#### [v1.9.12-dev.4](https://github.com/zumerlab/snapdom/compare/v1.9.12-dev.3...v1.9.12-dev.4)
> 30 September 2025
- Add basic support to sticky elements. See #232 [`02893e6`](https://github.com/zumerlab/snapdom/commit/02893e60e7f3244c1274d64232e7dbf338a283ec)
- Update types [`19317e6`](https://github.com/zumerlab/snapdom/commit/19317e6ff34599028e627f891ae5a2d28036bac1)
- fix formating [`58ca761`](https://github.com/zumerlab/snapdom/commit/58ca7616e3e82ec81122804264466f33d79c45c2)
- Enhance browser detection. See #251 [`cfe753c`](https://github.com/zumerlab/snapdom/commit/cfe753c280ab0c0cda8aca88978a550257caf23f)
- Fix pseudo capture. See #252 [`e85678e`](https://github.com/zumerlab/snapdom/commit/e85678e54fac6736c3dac8a0eaac8f1607dcbb6a)
- Merge pull request #249 from K1ender/dev [`e497bba`](https://github.com/zumerlab/snapdom/commit/e497bbae30e2c539c53715f9dd0bed585d10cf9a)
#### [v1.9.12-dev.3](https://github.com/zumerlab/snapdom/compare/v1.9.12-dev.2...v1.9.12-dev.3)
> 26 September 2025
- Captures CSS shadows [`050365f`](https://github.com/zumerlab/snapdom/commit/050365f8ab2087a912c71c468b4b1c234b21dd6a)
- Improve counter simulation [`4c9e21d`](https://github.com/zumerlab/snapdom/commit/4c9e21d6bc0135614b882e1940ce31b64c5e40b2)
- Fix scale, width, height options [`8ef48cb`](https://github.com/zumerlab/snapdom/commit/8ef48cb2e038c286eea9b7ce2baafac30c062a5e)
- Just run safariWarmup if it is needed [`a32846d`](https://github.com/zumerlab/snapdom/commit/a32846d6f48ab8c583b1f26c41d19f3cff67ab55)
- Safari, in case of scale, width or height options use png to ensure fidelity [`0711a77`](https://github.com/zumerlab/snapdom/commit/0711a7774f6f545cd05e0cce86f3354fe377b02d)
- Sanitize container [`a6ba396`](https://github.com/zumerlab/snapdom/commit/a6ba396c52406bf9c5dd0ffd27f9460cd34b028e)
#### [v1.9.12-dev.2](https://github.com/zumerlab/snapdom/compare/v1.9.12-dev.1...v1.9.12-dev.2)
> 22 September 2025
- Fix margin collapsing in some cases. See #243 [`7fe0a3f`](https://github.com/zumerlab/snapdom/commit/7fe0a3ffe6e9827b276f0c0337c60c8c02c4129c)
#### [v1.9.12-dev.1](https://github.com/zumerlab/snapdom/compare/v1.9.12-dev.0...v1.9.12-dev.1)
> 20 September 2025
- Modularize counters [`024a7f9`](https://github.com/zumerlab/snapdom/commit/024a7f9b2d1b4805c7b12b5cbb62f0200295cc8f)
- Improve CSS counter() and counters() handling. See #120, see #235 [`8fb0385`](https://github.com/zumerlab/snapdom/commit/8fb03859301075ea9b096197ec5b4dbca4223a95)
- Feat lineClamp. See #241 [`4082cd6`](https://github.com/zumerlab/snapdom/commit/4082cd6c39ff43bcb842a81768e11b858c5e8559)
- Fix width/height options [`7cd2111`](https://github.com/zumerlab/snapdom/commit/7cd21114d1337e48b5d743cb94989feb1a1a0d20)
- Fix bug that hangs snapDOM on some browsers. See #236 [`bc3c400`](https://github.com/zumerlab/snapdom/commit/bc3c400356a6f32f8d1d8a986c9a427e6dd13c7f)
- Fix bug that overrides options.width/heigth. See #241 [`49fbb63`](https://github.com/zumerlab/snapdom/commit/49fbb63ac6c28a66a8e6d9076618bee7b6beab62)
- Improve webFonts render. See #229 [`3082a3a`](https://github.com/zumerlab/snapdom/commit/3082a3ae019f424e127f3c065cbcfa7bad59bb39)
- Feat. detect wechat browser. See #223 [`e7c4723`](https://github.com/zumerlab/snapdom/commit/e7c4723a24ad3a9c52da5a2e021c115b354bcf66)
- Ensure donwload file measure. See #241 [`eed1995`](https://github.com/zumerlab/snapdom/commit/eed1995a56664c0ace5d94422ba6bb8b5ef82324)
- Add iframe support [`ec59e4b`](https://github.com/zumerlab/snapdom/commit/ec59e4bad3dbb639cde37aed929dccb42b54e6b5)
#### [v1.9.12-dev.0](https://github.com/zumerlab/snapdom/compare/v1.9.11...v1.9.12-dev.0)
> 11 September 2025
- Try fix fallback images [`67bedd3`](https://github.com/zumerlab/snapdom/commit/67bedd3c7cb6bda3e2291fe494805a58263e6dce)
- Two separate mode: filterMode and excludeMode [`394e7f4`](https://github.com/zumerlab/snapdom/commit/394e7f4ca2171fb1028eb382b2331d4718f6a350)
- Workaround Safari See #231 [`593ad59`](https://github.com/zumerlab/snapdom/commit/593ad59383d0b3adbcb139f6892ae321a08c60d5)
- Try new approach for solve Safari fonts/images decoding [`5b77738`](https://github.com/zumerlab/snapdom/commit/5b7773847f02809826a9ed459321100cfbd50518)
#### [v1.9.11](https://github.com/zumerlab/snapdom/compare/v1.9.10...v1.9.11)
> 9 September 2025
- Fix Safari bug that prevents capture [`6a43e59`](https://github.com/zumerlab/snapdom/commit/6a43e59d1c311452c7d16e1adc9bb12bb89132b4)
#### [v1.9.10](https://github.com/zumerlab/snapdom/compare/v1.9.10-dev.2...v1.9.10)
> 9 September 2025
- Merge dev branch [`#225`](https://github.com/zumerlab/snapdom/pull/225)
- Strip dev comments [`e02066b`](https://github.com/zumerlab/snapdom/commit/e02066b6ef8e6c2b1d50b86096500f914ac025af)
- increase test coverage [`0c59fa0`](https://github.com/zumerlab/snapdom/commit/0c59fa0d276b7899cf2b721d32e7934e701df76b)
- Update types defs [`648f4a9`](https://github.com/zumerlab/snapdom/commit/648f4a965106c58c6f63d384bf8db600a661146c)
- Improves mask handling [`f3915ea`](https://github.com/zumerlab/snapdom/commit/f3915ea919b927b5f2ff0a9f3c865beb7b08b231)
- fix backgroundColor regression [`21a6a39`](https://github.com/zumerlab/snapdom/commit/21a6a3923d81d2f733d6bc61136a9d4eda8f1a62)
#### [v1.9.10-dev.2](https://github.com/zumerlab/snapdom/compare/v1.9.10-dev.1...v1.9.10-dev.2)
> 8 September 2025
- Fix cache disabled bug. Closes #221 [`#221`](https://github.com/zumerlab/snapdom/issues/221)
- Add extra margin when element has transform [`6688eee`](https://github.com/zumerlab/snapdom/commit/6688eee661de2d91249f9db28948629f421b14b4)
- Feat. handkles css trasnforms and scale rotate new props. Ref #216 [`d151da1`](https://github.com/zumerlab/snapdom/commit/d151da1993f1374d2bae16ed1a076a05f9ff8d45)
- Add same-origin iframe support .See #222 [`f50720f`](https://github.com/zumerlab/snapdom/commit/f50720fe76d8d114c1de31ffe802ade1edd7060e)
- Fix regression that doesnt reset origial translate [`800c427`](https://github.com/zumerlab/snapdom/commit/800c427d327f41fbcc9703dfa5a3e99b9b7c789f)
- Fix duplicated values on textArea [`0915f8d`](https://github.com/zumerlab/snapdom/commit/0915f8d4f1b9e58800407ba28ad86e16b6cc4621)
- 增强图像处理功能,添加图像加载失败时的后备图像源支持,并记录原始图像尺寸以便于使用。更新类型定义以包含新选项。 [`011620a`](https://github.com/zumerlab/snapdom/commit/011620a3c8dbabed9c2e509766c4516205fbad66)
- ✨ feat: [`e3a4556`](https://github.com/zumerlab/snapdom/commit/e3a4556a4085c0968bcce9f54d7d0fb9bbcfc6a7)
- remove iframe limitation [`77abf8f`](https://github.com/zumerlab/snapdom/commit/77abf8f617fd1942565ee141406757c3704f45ae)
- Merge pull request #220 from Jarvis2018/main [`adb6455`](https://github.com/zumerlab/snapdom/commit/adb6455fd6ff9db128dda5a59ac7556a16c851fa)
- Merge pull request #215 from xiaobai-web715/dev [`37be327`](https://github.com/zumerlab/snapdom/commit/37be327f7c55ae20ee65001a8469f59284dbe12f)
#### [v1.9.10-dev.1](https://github.com/zumerlab/snapdom/compare/v1.9.10-dev.0...v1.9.10-dev.1)
> 3 September 2025
- Fix flickering on Safari. Closes #197 [`#197`](https://github.com/zumerlab/snapdom/issues/197)
- Prevents default svg values overwrite custom ones. Closes #217 [`#217`](https://github.com/zumerlab/snapdom/issues/217)
- Feature: add placeholders option to disable rendered placeholder for iframes and fallback images. Closes #137 [`#137`](https://github.com/zumerlab/snapdom/issues/137)
- FIx textarea styles. Closes #212 [`#212`](https://github.com/zumerlab/snapdom/issues/212)
#### [v1.9.10-dev.0](https://github.com/zumerlab/snapdom/compare/v1.9.9...v1.9.10-dev.0)
> 29 August 2025
- Code refactor, cache improve, options centralized [`3bd7182`](https://github.com/zumerlab/snapdom/commit/3bd71822cf72614b3bb5993039482fdb05833ceb)
- Improve performance and cache [`8882025`](https://github.com/zumerlab/snapdom/commit/88820259d4000fd36dbf4f59bb4940a6e12e6611)
- Enhance font handling [`cb1e04a`](https://github.com/zumerlab/snapdom/commit/cb1e04af0551f097b44eeb84ba64a97e869b6f60)
- Improve capture fidelity [`e05f027`](https://github.com/zumerlab/snapdom/commit/e05f027b8eb3b5b90e22a9d8ce7a7279f7b1614b)
- fix font fetching [`70dd092`](https://github.com/zumerlab/snapdom/commit/70dd092adb14899031f0596d83ec354c9ab023b9)
- Set compress as default [`7e5ab00`](https://github.com/zumerlab/snapdom/commit/7e5ab007f653f995b09ecbbf7711d69937fdef4a)
- optimice code [`df52437`](https://github.com/zumerlab/snapdom/commit/df524379288fcb08dd19b8957d627a24b898685e)
- Core update: increase X3 speed capture compared 1.9.9 [`94bc57d`](https://github.com/zumerlab/snapdom/commit/94bc57dc53cb63d82532467b4a041ab26da7481a)
- Ensure custom fonts are capured [`8125689`](https://github.com/zumerlab/snapdom/commit/81256893249b2313edfebacb9d68ec6ed4fa9ed2)
- Fix first custom font bug on Safari [`971d976`](https://github.com/zumerlab/snapdom/commit/971d9762dd73263689057e16fb47c00d2e0eba1b)
- Fix bug that affects overall capture fidelity [`35539a5`](https://github.com/zumerlab/snapdom/commit/35539a50da67c30e28c39272a4c1efefbf24a2e2)
- update to avoid vitest issues [`51ef80d`](https://github.com/zumerlab/snapdom/commit/51ef80d24b693ceee3e33d75e2b64ba7037e49ea)
#### [v1.9.9](https://github.com/zumerlab/snapdom/compare/v1.9.8...v1.9.9)
> 14 August 2025
- Improves external fonts handling. Closes #139, closes #146, closes #186 [`#139`](https://github.com/zumerlab/snapdom/issues/139) [`#146`](https://github.com/zumerlab/snapdom/issues/146) [`#186`](https://github.com/zumerlab/snapdom/issues/186)
- Handles srcset. Closes #190 [`#190`](https://github.com/zumerlab/snapdom/issues/190)
- Fix speed regression. [`542ed00`](https://github.com/zumerlab/snapdom/commit/542ed003e3f35c19bd51fd5317f5572c12ba1ac8)
- Handles Blob scr [`fe27239`](https://github.com/zumerlab/snapdom/commit/fe27239ac3efef9a0e9e7f0feeb223dd74fd9086). Closes [`#169`](https://github.com/zumerlab/snapdom/issues/169)
#### [v1.9.8](https://github.com/zumerlab/snapdom/compare/v1.9.7...v1.9.8)
> 10 August 2025
- fix(types): update `preCache` [`#166`](https://github.com/zumerlab/snapdom/pull/166)
- Fix defs & symbols outside captured element and hidden visibility, closes #178. Stabilize layout before cloning, closes #179. Fix inline styles, closes #177 [`#178`](https://github.com/zumerlab/snapdom/issues/178) [`#177`](https://github.com/zumerlab/snapdom/issues/177) [`#179`](https://github.com/zumerlab/snapdom/issues/179)
- Fix icontFont alignment and rendered size. Closes #176 [`#176`](https://github.com/zumerlab/snapdom/issues/176)
- Ensure skip empty pseudo elements. Closes #168 [`#168`](https://github.com/zumerlab/snapdom/issues/168)
- Add basic border-image support. Closes #159 [`#159`](https://github.com/zumerlab/snapdom/issues/159)
- Fix input css styles. Closes #144, closes #147 [`#144`](https://github.com/zumerlab/snapdom/issues/144) [`#147`](https://github.com/zumerlab/snapdom/issues/147)
#### [v1.9.7](https://github.com/zumerlab/snapdom/compare/v1.9.6...v1.9.7)
> 27 July 2025
- Fix input css styles. Closes #144, closes #147 [`#144`](https://github.com/zumerlab/snapdom/issues/144) [`#147`](https://github.com/zumerlab/snapdom/issues/147)
- Fix Safari scale. Closes #133 [`#133`](https://github.com/zumerlab/snapdom/issues/133)
- Fix @font-face. Closes #145 [`#145`](https://github.com/zumerlab/snapdom/issues/145)
- Fix edge case that generates blank images on Safari. Closes #129 [`#129`](https://github.com/zumerlab/snapdom/issues/129)
- Improve pseudo elements detection. Closes # 143 [`539e488`](https://github.com/zumerlab/snapdom/commit/539e488c018a1bf7be05e0d9d969e350c9ed4291)
- Remove default backgroundColor on download(). Ref dissussion #142 [`a875fe3`](https://github.com/zumerlab/snapdom/commit/a875fe31c1c1fc9a1d0d59ef2934b5930a6b7c88)
- Update docs, thanks @kohaiy[`e38d67b`](https://github.com/zumerlab/snapdom/commit/e38d67b0102edf75a9f6e742bd45eacc43be51c1)
#### [v1.9.6](https://github.com/zumerlab/snapdom/compare/v1.9.5...v1.9.6)
> 20 July 2025
- Add options argument to toBlob function. Thanks @rbbydotdev [`#118`](https://github.com/zumerlab/snapdom/pull/118)
- Keep canvas CSS style. Fixes #121. [`#121`](https://github.com/zumerlab/snapdom/issues/121)
- Improve: handles local() source font. See #114 [`c088aa0`](https://github.com/zumerlab/snapdom/commit/c088aa01422bf6ea6c1be70a88d09d540eae5038)
- Improve webcomponent clone [`a0f37a5`](https://github.com/zumerlab/snapdom/commit/a0f37a57079057548e97a973bc6c734b4141769d)
- Perf: unifies cache [`fe3a368`](https://github.com/zumerlab/snapdom/commit/fe3a3680ddef2736fc0176dcbc210fc760149038)
- Improve cache handling. [`183ae2f`](https://github.com/zumerlab/snapdom/commit/183ae2f90debfb472164df01616f2558136a9f8f)
- Adjust cache reset [`ff33ed3`](https://github.com/zumerlab/snapdom/commit/ff33ed374dcc02272225ca0154a84c304e6fc19a)
- Improve regex [`1b4d5ad`](https://github.com/zumerlab/snapdom/commit/1b4d5ada356bce55caecd008df746f891d379c48)
- Add primitive support to css counter. See #120 [`160bc2e`](https://github.com/zumerlab/snapdom/commit/160bc2eaf984051e23031664040ea91166bca061)
- Fix bug background-color on export formats. See #90 [`47a34a9`](https://github.com/zumerlab/snapdom/commit/47a34a971cc875ec4d9eab772266df81a94438e7)
- Fix regression textArea duplication [`1759fd0`](https://github.com/zumerlab/snapdom/commit/1759fd0ae1681e17df946d507ae4d704efff1b18)
- Prevent process local ids. See #128 [`659e862`](https://github.com/zumerlab/snapdom/commit/659e8627bb6545f7843de1bcf808dc6bfb4dff3e)
- Add node version and improve docs. See #123. Thanks @miusuncle [`e457bf6`](https://github.com/zumerlab/snapdom/commit/e457bf64ca2b4294aaf237c165f865ea50cc0c14)
#### [v1.9.5](https://github.com/zumerlab/snapdom/compare/v1.9.3...v1.9.5)
> 14 July 2025
Fix: add type def for `SnapOptions`. Thanks @simon1uo [`#111`](https://github.com/zumerlab/snapdom/pull/111)
- Add `checkbox.indeterminate`. Thanks @titoBouzout [`#104`](https://github.com/zumerlab/snapdom/pull/104)
- Add mask-image CSS detection (closes #106) [`#106`](https://github.com/zumerlab/snapdom/issues/106)
- Add slot detection (closes # 97) / Fix textarea content duplication (closes #110) [`#110`](https://github.com/zumerlab/snapdom/issues/110)
- Add html-to-image to benchmark. Closes #103 [`#103`](https://github.com/zumerlab/snapdom/issues/103)
#### [v1.8.0](https://github.com/zumerlab/snapdom/compare/v1.7.1...v1.8.0)
> 30 June 2025
- fix: encode same uri multiple times [`#65`](https://github.com/zumerlab/snapdom/pull/65)
- Add Lucide to icon font detection [`#50`](https://github.com/zumerlab/snapdom/pull/50)
- Avoid background-image logic duplication, closes #66 [`#66`](https://github.com/zumerlab/snapdom/issues/66)
- Feat: sanitize rootElement to avoid CSS layout conflicts. Fixes #56, fixes #24 [`#56`](https://github.com/zumerlab/snapdom/issues/56) [`#24`](https://github.com/zumerlab/snapdom/issues/24)
- Fix: canvas style props, closes #63 [`#63`](https://github.com/zumerlab/snapdom/issues/63)
- Feat: handling @import and optimice cache, closes #61 [`#61`](https://github.com/zumerlab/snapdom/issues/61)
- Compile .js to es2015, closes #58 [`#58`](https://github.com/zumerlab/snapdom/issues/58)
- Fix background image handling, closes #57 [`#57`](https://github.com/zumerlab/snapdom/issues/57)
- Improve inlinePseudoElements() to handle decorative properties, closes #55 [`#55`](https://github.com/zumerlab/snapdom/issues/55)
- Add ::first-letter detection, closes #52 [`#52`](https://github.com/zumerlab/snapdom/issues/52)
- test: increases coverage [`7ebc871`](https://github.com/zumerlab/snapdom/commit/7ebc87143101a9e5c8573f5ae76ede2884b59eb8)
- Increase test coverage [`0c63478`](https://github.com/zumerlab/snapdom/commit/0c634785157ca9f611973976000b2f25ba7c9549)
- Improve split multiple backgrounds [`0e67a9b`](https://github.com/zumerlab/snapdom/commit/0e67a9b72fb1ea7ea4a625d5f6dc2eb40438d7cd)
- chore: update contributors list [`da22404`](https://github.com/zumerlab/snapdom/commit/da2240490b46ff4a0747f7db741b822dbc6ba3c4)
- chore: update contributors list [`ec7c275`](https://github.com/zumerlab/snapdom/commit/ec7c27590318df95e7aa903ec7cbd92112b6c2e8)
- Add check [`bf9a888`](https://github.com/zumerlab/snapdom/commit/bf9a888525e99dd663c17455755bb1478f1cb9d7)
- Create update-contributors.js [`453dff0`](https://github.com/zumerlab/snapdom/commit/453dff07d0fd8f333627ed22a6e8a64373dbd62d)
- Document width and height options [`0f7fb7a`](https://github.com/zumerlab/snapdom/commit/0f7fb7a02d9159a831a1dfc4cfbd9f6f3420bca7)
- Create update-contributors.yml [`b48e334`](https://github.com/zumerlab/snapdom/commit/b48e334043e2a18212620df413b8742d72959468)
- Update [`7da2892`](https://github.com/zumerlab/snapdom/commit/7da2892e69d04903111bbd24421a574e0034a83b)
- Bumped version [`f322f51`](https://github.com/zumerlab/snapdom/commit/f322f51e2369bfdbbc1218c15e8375b0858bf73d)
- Update README.md [`99c51a8`](https://github.com/zumerlab/snapdom/commit/99c51a89e2b486bbfc2810d94350428cbc9595f2)
- Update update-contributors.js [`46a868b`](https://github.com/zumerlab/snapdom/commit/46a868baa45bd068be687b19bbd50cb06ceb9cf0)
- Update update-contributors.js [`2a77e4c`](https://github.com/zumerlab/snapdom/commit/2a77e4c82dab36803b005cc6bceab06689a0e52c)
- chore: update contributors list [`020eff8`](https://github.com/zumerlab/snapdom/commit/020eff873c18fc601c145f957bdc566403e18649)
- Update update-contributors.js [`b4cf877`](https://github.com/zumerlab/snapdom/commit/b4cf87709f632be2e03f40b0af5661393f8f8793)
- chore: update contributors list [`a2d28d9`](https://github.com/zumerlab/snapdom/commit/a2d28d952b18787d8e7aabf1b6d12cd8e45fa436)
- Update doc [`7cf19de`](https://github.com/zumerlab/snapdom/commit/7cf19de5df40735b17958359251c481c1b517d8c)
- Update update-contributors.js [`962c7c6`](https://github.com/zumerlab/snapdom/commit/962c7c6a4e23d5b8a0ef4c72111a617ffac3add4)
- Update README.md [`183de8c`](https://github.com/zumerlab/snapdom/commit/183de8ce06c8df35fcbc3a32bb4204c14a657310)
- Update README.md [`4352ae7`](https://github.com/zumerlab/snapdom/commit/4352ae75fb445857c14c64eb9a2ea7dbe82733c3)
- Update update-contributors.js [`7dca4a1`](https://github.com/zumerlab/snapdom/commit/7dca4a1d3bbaacc14295f0e891095db1b39a76d0)
- Update README.md [`ebb7f32`](https://github.com/zumerlab/snapdom/commit/ebb7f3204892d0d42713e7a2a14d261177a24d31)
- Clean transform RootElement prop [`f293e5b`](https://github.com/zumerlab/snapdom/commit/f293e5be0e3ca6a97d43467976d80175d988916d)
- Check if getStyle is iterable [`24dfe05`](https://github.com/zumerlab/snapdom/commit/24dfe056f6d35fe56ab39325b9c4492f84e64cd5)
- chore: update contributors list [`8ac4aa1`](https://github.com/zumerlab/snapdom/commit/8ac4aa1f5a21373e73f86b22d0cdad8def37a8ea)
- chore: update contributors list [`9987328`](https://github.com/zumerlab/snapdom/commit/9987328a796bb3eb70eb45cda4485dc8f5906688)
- Update README.md [`4b52b87`](https://github.com/zumerlab/snapdom/commit/4b52b87e33b6a2f5d34460b2bff13e47ad011a73)
#### [v1.7.1](https://github.com/zumerlab/snapdom/compare/v1.3.0...v1.7.1)
> 19 June 2025
- Improve inlineBackgroundImages to support multiple background-image values. [`#46`](https://github.com/zumerlab/snapdom/pull/46)
- Add @font-face / FontFace() deteccion, closes #43 [`#43`](https://github.com/zumerlab/snapdom/issues/43)
- update [`7c5441e`](https://github.com/zumerlab/snapdom/commit/7c5441ed4b2c602bcee60b314162f10412b260c5)
- Add benchmark against html2canvas [`f196afe`](https://github.com/zumerlab/snapdom/commit/f196afeb43b23624680a77e52a80222a476f055d)
- Add description [`bcae4af`](https://github.com/zumerlab/snapdom/commit/bcae4af3ce9e0953fea8410303e6d77fe3e01e3e)
- Update issue templates [`352dba3`](https://github.com/zumerlab/snapdom/commit/352dba3e53452f09fb5d056a0c5fb9216701a0f4)
- add options.crossOrigin [`49f8ac6`](https://github.com/zumerlab/snapdom/commit/49f8ac6524e3f54e67505d048a4ad34c529ab6c9)
- Update issue templates [`d832dbd`](https://github.com/zumerlab/snapdom/commit/d832dbd14df70f07d3f3ec9b62016dc4d19d8c9a)
- Create CONTRIBUTING.md [`9a7be15`](https://github.com/zumerlab/snapdom/commit/9a7be151f6b36abd5a582aebbcaacfe759716c3a)
- handle multiple background image in inlineBackgroundImages function [`95a5490`](https://github.com/zumerlab/snapdom/commit/95a5490f2de5a139f39c0286111eb4e84990fd00)
- Update issue templates [`b69b5a4`](https://github.com/zumerlab/snapdom/commit/b69b5a4cb72e3bd0ca5f8ae5b43448c8aab95752)
- update [`57d6b15`](https://github.com/zumerlab/snapdom/commit/57d6b1529c56e890a43cc427f817c731784f6ca0)
- Update index.html [`f002bca`](https://github.com/zumerlab/snapdom/commit/f002bca6ee6330ae9d6f2550d36ce59414de29b0)
- Update issue templates [`24d478f`](https://github.com/zumerlab/snapdom/commit/24d478f32795b42b13f70b4319b5e2cd0ba3fa70)
- Bumped version [`d109fd7`](https://github.com/zumerlab/snapdom/commit/d109fd739197bbc37089f5acfe65ed10a6f48050)
- Add files via upload [`0aecf4e`](https://github.com/zumerlab/snapdom/commit/0aecf4e46093743ca854397509a8be91e08cb666)
- update [`e444762`](https://github.com/zumerlab/snapdom/commit/e444762ddb173d283b761e13a1e5e16c8853e325)
- Update index.html [`997dab3`](https://github.com/zumerlab/snapdom/commit/997dab3293df81dc906116acbf7b4f388a270b39)
- update [`0355286`](https://github.com/zumerlab/snapdom/commit/035528627f957213d35f1c63d9f73528deb972cf)
- Prevent erasing non url background [`0d626cb`](https://github.com/zumerlab/snapdom/commit/0d626cb32b8958afd7e7fd6f96d5a71c6795113b)
- docs: add @jhbae200 as contributor for PR #46 [`afe3094`](https://github.com/zumerlab/snapdom/commit/afe3094360f14712a55c1be134ab993c094a670b)
- Merge pull request #44 from elliots/support-use-credentials-on-images [`005f23e`](https://github.com/zumerlab/snapdom/commit/005f23e529962d73e7550f9f20e92bdc7c8eb8ab)
- Update index.html [`25d970f`](https://github.com/zumerlab/snapdom/commit/25d970fb1142c07bf10c8d9eba491ecdb3bf3e37)
- update [`ea624c3`](https://github.com/zumerlab/snapdom/commit/ea624c362acb7c0f953f3c202dd78f83c84742ce)
- update [`1cf93b7`](https://github.com/zumerlab/snapdom/commit/1cf93b7e25eaa39878f2334e9c240e98ed98f847)
- update [`3a547df`](https://github.com/zumerlab/snapdom/commit/3a547dfccc46e835ac585057d80b03ef5b324e7b)
- update [`2d4380b`](https://github.com/zumerlab/snapdom/commit/2d4380b4c900d3230a44a5af0380d149e55caca9)
- Update index.html [`bedf815`](https://github.com/zumerlab/snapdom/commit/bedf815299c421e3fe810a480f32bb291aae40b1)
- Update issue templates [`48a56fb`](https://github.com/zumerlab/snapdom/commit/48a56fb7f5006b20e64de6a592ec38c7a59b3cd8)
- Create config.yml [`51700c4`](https://github.com/zumerlab/snapdom/commit/51700c4457abb070df34520887991508ce32ad7f)
- update image [`0ec788c`](https://github.com/zumerlab/snapdom/commit/0ec788c562011990a008edb2b8f9b0cf18da8940)
#### [v1.3.0](https://github.com/zumerlab/snapdom/compare/v1.2.5...v1.3.0)
> 14 June 2025
- fix: double scaled images [`#38`](https://github.com/zumerlab/snapdom/pull/38)
- Fix: background img & img base64 in pseudo elements, closes #36 [`#36`](https://github.com/zumerlab/snapdom/issues/36)
- Feat: captures input values, closes #35 [`#35`](https://github.com/zumerlab/snapdom/issues/35)
- Improve: Device Pixel Ratio handling, thanks @jswhisperer [`1a14f69`](https://github.com/zumerlab/snapdom/commit/1a14f69d340e935126b5388febe5d711c4b94e14)
- Bumped version [`489be08`](https://github.com/zumerlab/snapdom/commit/489be081e6c7e50f1e4ba08d932d79c0ae242d45)
- Update description [`4db784b`](https://github.com/zumerlab/snapdom/commit/4db784b4250b6eac6da8932e651872147fbc8bc1)
#### [v1.2.5](https://github.com/zumerlab/snapdom/compare/v1.2.2...v1.2.5)
> 9 June 2025
- Fix duplicated font-icon when embedFonts is true, closes #30 [`#30`](https://github.com/zumerlab/snapdom/issues/30)
- Fix url with encode url, closes #29 [`#29`](https://github.com/zumerlab/snapdom/issues/29)
- Fix .toCanvas scale [`fb47284`](https://github.com/zumerlab/snapdom/commit/fb4728463a65620bd4f4f8f50cd8b2263ba7bbe7)
- Bumped version [`75b917a`](https://github.com/zumerlab/snapdom/commit/75b917a4fefc5fa9b55da3c028c43955f0656087)
- Update cdn [`37533a2`](https://github.com/zumerlab/snapdom/commit/37533a2c2a858000e93d8d33009241a4be5f8726)
- add homepage [`aa85c5d`](https://github.com/zumerlab/snapdom/commit/aa85c5d9f1777c437b07e624d874f7f1a0fac6a9)
#### [v1.2.2](https://github.com/zumerlab/snapdom/compare/v1.2.1...v1.2.2)
> 4 June 2025
- Patch: type script definitions, closes #23 [`#23`](https://github.com/zumerlab/snapdom/issues/23)
- Bumped version [`548d7f3`](https://github.com/zumerlab/snapdom/commit/548d7f30a889d34ae4dac28a8dedc43262089325)
#### [v1.2.1](https://github.com/zumerlab/snapdom/compare/v1.1.0...v1.2.1)
> 31 May 2025
- feat(embedFonts): also embed icon fonts when embedFonts is true [`#18`](https://github.com/zumerlab/snapdom/issues/18)
- Fix expose snapdom and preCache on browser compilation, closes #26 [`#26`](https://github.com/zumerlab/snapdom/issues/26)
- Improve icon-font conversion [`7bac4ee`](https://github.com/zumerlab/snapdom/commit/7bac4ee3b152d6364c218aaa6d2bed4ad9997943)
- Fix compress mode [`652cfe9`](https://github.com/zumerlab/snapdom/commit/652cfe9a8947029e31db6b089829fe8da87c0b42)
- Bumped version [`0cd7973`](https://github.com/zumerlab/snapdom/commit/0cd797320b92310d86df0ce6296706d1f7f0ad5d)
- Remove some logs [`4348b39`](https://github.com/zumerlab/snapdom/commit/4348b390ab8bb88c59ba9b0d24adbe58051b277a)
- Chore: delete old comments [`ff81a40`](https://github.com/zumerlab/snapdom/commit/ff81a40e8a1b4baa8bacca2ed2ec59124df40b6e)
- Chore: add dry bump script [`5c421c7`](https://github.com/zumerlab/snapdom/commit/5c421c75a1775a3b8c1fbd6a688fcfe409f676af)
#### [v1.1.0](https://github.com/zumerlab/snapdom/compare/v1.0.0...v1.1.0)
> 28 May 2025
- Add typescript declaration, closes #23 [`#23`](https://github.com/zumerlab/snapdom/issues/23)
- Feat. support scrolling state, closes #20 [`#20`](https://github.com/zumerlab/snapdom/issues/20)
- Fix bug by removing trim spaces, closes #21 [`#21`](https://github.com/zumerlab/snapdom/issues/21)
- fix margin on mobile [`36297c8`](https://github.com/zumerlab/snapdom/commit/36297c89c085f605922f88ac5113f2f176c6a1a9)
- mobile friendly [`42dada8`](https://github.com/zumerlab/snapdom/commit/42dada88bdbe886033890071e8e76499358a6b91)
- Update index.html [`1bf3bc1`](https://github.com/zumerlab/snapdom/commit/1bf3bc1b15f4d28b50363c73410cea25ad589cda)
- Create FUNDING.yml [`ddf914c`](https://github.com/zumerlab/snapdom/commit/ddf914c96727b3a82bbea4694d19dc0eb2b518e3)
- Bumped version [`7a9f3d8`](https://github.com/zumerlab/snapdom/commit/7a9f3d8662099d15bcc2046ba88043eb3d3b1bfb)
- add ga [`6d8a73f`](https://github.com/zumerlab/snapdom/commit/6d8a73fd52997e9e1a91944bd9a46d95c8c8507c)
- Update index.html [`46e4b41`](https://github.com/zumerlab/snapdom/commit/46e4b41209c44766425e96fb9be94e1d1c08b6ae)
- Update index.html [`1a2a04c`](https://github.com/zumerlab/snapdom/commit/1a2a04cbb3f4e81e2713d823d5b8dcdeb508591d)
- Ignore generated screenshots tests [`cce8ead`](https://github.com/zumerlab/snapdom/commit/cce8ead47c470280761a34f7c98f9a2fd0796a34)
- Update index.html [`5dd6749`](https://github.com/zumerlab/snapdom/commit/5dd67495df0a5cd48eda168565a81969d5639f40)
- FIx bug that prevent scale on png format [`77a5265`](https://github.com/zumerlab/snapdom/commit/77a52651bd0ea8ccb451f199bd3d8f9e2478bf84)
- Update README.md [`d8440f3`](https://github.com/zumerlab/snapdom/commit/d8440f3864931509f1b369d7e301d6ecccb63b14)
- update [`ffa3a9a`](https://github.com/zumerlab/snapdom/commit/ffa3a9ad942987a5b52a7c9080914bed912db558)
- Update README.md [`9c79e6e`](https://github.com/zumerlab/snapdom/commit/9c79e6e406ff9cb4df1539480e057b6828ef1788)
- Update index.html [`7585674`](https://github.com/zumerlab/snapdom/commit/7585674ed21bb7009b84d1f948ceed2d5ed5ae69)
- Update index.html [`8f4fb95`](https://github.com/zumerlab/snapdom/commit/8f4fb95a8f839159bd00c3338c7c3dc9fb23071c)
- Update index.html [`eebc2bc`](https://github.com/zumerlab/snapdom/commit/eebc2bc01a6581f25995d5a9e946aa6bde08dfdc)
### [v1.0.0](https://github.com/zumerlab/snapdom/compare/v1.0.0-pre.1747581859131...v1.0.0)
> 19 May 2025
- format code [`146fd95`](https://github.com/zumerlab/snapdom/commit/146fd95ec93d6b842acb28272aad43f787dc954a)
- new demo gallery [`b8b2b6e`](https://github.com/zumerlab/snapdom/commit/b8b2b6eb4373999af5e67fc87418d6c6ab96199f)
- Update code documentation [`6f933bc`](https://github.com/zumerlab/snapdom/commit/6f933bca3f1e9a9054f2e0e63807dfd52dda6270)
- Add benchmarks section [`6becbb1`](https://github.com/zumerlab/snapdom/commit/6becbb12014d3cf33ec49264ca088486f08a5ce1)
- Update documentation - add precache() [`a689566`](https://github.com/zumerlab/snapdom/commit/a6895665858f9eb574b0195dc918cef680c1651b)
- Bumped version [`50d48c0`](https://github.com/zumerlab/snapdom/commit/50d48c05458e971a16375ca89da08eedad049e0c)
- chore [`9f76e0c`](https://github.com/zumerlab/snapdom/commit/9f76e0cb1e7761604693588092ac8b1796cc892e)
- update [`d84d395`](https://github.com/zumerlab/snapdom/commit/d84d39599abbd8fbd31727ff3a6650278ec0e28c)
#### [v1.0.0-pre.1747581859131](https://github.com/zumerlab/snapdom/compare/v0.9.9...v1.0.0-pre.1747581859131)
> 18 May 2025
- Fix retina and scale bug, closes #15 [`#15`](https://github.com/zumerlab/snapdom/issues/15)
- Improve public API, closes #16 [`#16`](https://github.com/zumerlab/snapdom/issues/16)
- Fix bug to render canvas with precache compress mode, closes #13 [`#13`](https://github.com/zumerlab/snapdom/issues/13)
- Update to reflect new public API [`b6024cb`](https://github.com/zumerlab/snapdom/commit/b6024cb800b848103411d4e8f4be9a7ffdb84f48)
- Update tests and benckmarks [`f06a0f8`](https://github.com/zumerlab/snapdom/commit/f06a0f835e42036a19761152cf5bf941b53d2f27)
- Add helper to check Safari [`6c9ee04`](https://github.com/zumerlab/snapdom/commit/6c9ee0484c598dd56d52e62f3de37499024ad5e5)
- Remove preWarm [`d3bd582`](https://github.com/zumerlab/snapdom/commit/d3bd582c144775617fc6221c4504466eb4cd6bef)
- Bumped version [`fb0855d`](https://github.com/zumerlab/snapdom/commit/fb0855d55eafdbcae79f537b7e1a51e2cd4d1dfc)
#### [v0.9.9](https://github.com/zumerlab/snapdom/compare/v0.9.8...v0.9.9)
> 14 May 2025
- Bumped version [`676b00d`](https://github.com/zumerlab/snapdom/commit/676b00d71b5b51ea3a90c1aa95776a9b226378a3)
- Fix bug on collectUsedTagNames() [`d627f18`](https://github.com/zumerlab/snapdom/commit/d627f18b6c0512545ab695bfae660cac8f64a9f0)
- update [`c0e64d0`](https://github.com/zumerlab/snapdom/commit/c0e64d00905898660db68f054f5f5598c3fb9581)
- Fix menu options [`8e87681`](https://github.com/zumerlab/snapdom/commit/8e876810c721fa0306c0f7d1b427ba6b111f8afe)
#### [v0.9.8](https://github.com/zumerlab/snapdom/compare/v0.9.7...v0.9.8)
> 14 May 2025
- Add font example [`26c59c8`](https://github.com/zumerlab/snapdom/commit/26c59c864aeaae80b54c22ace32e96396cb9eae6)
- Bumped version [`0819d89`](https://github.com/zumerlab/snapdom/commit/0819d89bc52af1417e31b54e695af8491b709969)
- update tests [`3cd5b70`](https://github.com/zumerlab/snapdom/commit/3cd5b70427613d7d595dd15736cb545db6411d88)
- Fix capture output format [`2afa36a`](https://github.com/zumerlab/snapdom/commit/2afa36a1c41ff798ded5b7f8ecef1632e08ab716)
- Update index.html [`0345fb1`](https://github.com/zumerlab/snapdom/commit/0345fb1f177297db0e17141c5737f9b3b510e6ca)
- Add demo site [`88d0faa`](https://github.com/zumerlab/snapdom/commit/88d0faa1b27db0d305e8b78c7280c8a5e83384a5)
- Disable user zoom [`3813580`](https://github.com/zumerlab/snapdom/commit/381358028159c51b9ed0da11e25928da490170fb)
#### [v0.9.7](https://github.com/zumerlab/snapdom/compare/v0.9.2...v0.9.7)
> 14 May 2025
- Update Dev branch [`#11`](https://github.com/zumerlab/snapdom/pull/11)
- Delete functions [`c5040d9`](https://github.com/zumerlab/snapdom/commit/c5040d90b6276daa04e919ca4b0ecdf205f73af9)
- improve cache handling [`27d7b19`](https://github.com/zumerlab/snapdom/commit/27d7b19cfafeed83f4b30a824638ee7edd63e10b)
- add some examples [`3ce9dd2`](https://github.com/zumerlab/snapdom/commit/3ce9dd2807c8b84ed927c186621850a2518dfd2a)
- Reorganice and add helpers [`c4f4182`](https://github.com/zumerlab/snapdom/commit/c4f4182a3e9ce636a2a263a05d75e64b33b25d7b)
- Add tests [`455e7f2`](https://github.com/zumerlab/snapdom/commit/455e7f20e8a72f6a646a7d1e900f41fb22a18666)
- Check if element to capture exists [`dfa96f2`](https://github.com/zumerlab/snapdom/commit/dfa96f2f720238fdff5df6e24b4572691ad6198f)
- Improve capture logic [`79ab1b9`](https://github.com/zumerlab/snapdom/commit/79ab1b9e165dd08a34338fe0d837b0330be48539)
- Update readme [`fdc2877`](https://github.com/zumerlab/snapdom/commit/fdc2877fd9e6fb73bc5d7bc9cf1f4a405f088be0)
- Add preCache [`48bd910`](https://github.com/zumerlab/snapdom/commit/48bd910743a638ae8ce35ab7d617ad05a75d29a2)
- Optimice [`cc638e7`](https://github.com/zumerlab/snapdom/commit/cc638e7f0f2e63a24eeee65ab4d87755e7207dec)
- Bumped version [`3b26632`](https://github.com/zumerlab/snapdom/commit/3b266324c747c4bc139b99e4978493df79a5555c)
- update [`111fdb4`](https://github.com/zumerlab/snapdom/commit/111fdb444b3c6d61dcb0e6bb2e21c871f5e73587)
- Add cache Maps [`091484c`](https://github.com/zumerlab/snapdom/commit/091484c00941822684afc9148a59cb23e4b34627)
- update [`bdbba7a`](https://github.com/zumerlab/snapdom/commit/bdbba7aeff458a60d5a83b5ead2d4f9402492fd3)
- Update README.md [`c1756a9`](https://github.com/zumerlab/snapdom/commit/c1756a9192f8e3af90fd66da7e19c5fb883dbe0a)
- Expose preCache [`1e96db1`](https://github.com/zumerlab/snapdom/commit/1e96db14c6c4e697361ceed2fb6f9c618801a138)
- Chore [`38c08c0`](https://github.com/zumerlab/snapdom/commit/38c08c0c5a9eda486619855b9df47f33f490a921)
- fix url [`bebec7f`](https://github.com/zumerlab/snapdom/commit/bebec7fd70141b3a82d41a5f6cc0849dcfb0c715)
- Update README.md [`fb0ab3a`](https://github.com/zumerlab/snapdom/commit/fb0ab3ae528d4b37223e4eef03135e9be6a62b0b)
- Update README.md [`1a76186`](https://github.com/zumerlab/snapdom/commit/1a76186d938a7a776a33c0e42ecc6813e86a9262)
- Update README.md [`90d18a1`](https://github.com/zumerlab/snapdom/commit/90d18a165725ca3369fb5ebf48c281e0dd1377ae)
#### [v0.9.2](https://github.com/zumerlab/snapdom/compare/v0.9.2-pre.1746130901718...v0.9.2)
> 1 May 2025
- chore [`2f788af`](https://github.com/zumerlab/snapdom/commit/2f788afd3b25ae6391af6a41086e0b5c3595a701)
#### [v0.9.2-pre.1746130901718](https://github.com/zumerlab/snapdom/compare/v0.9.1...v0.9.2-pre.1746130901718)
> 1 May 2025
- This PR dramatically improves the speed and accuracy of snapDOM. It increases the result size and may produce some long tasks, but it provides a solid foundation to address these side effects in the future. [`#6`](https://github.com/zumerlab/snapdom/pull/6)
- Add as draft new default approach - not implemented [`6f4ec41`](https://github.com/zumerlab/snapdom/commit/6f4ec41c7146525c9db5cfce103e131bb3f19616)
- Add tests [`bdd5a7f`](https://github.com/zumerlab/snapdom/commit/bdd5a7f491561966cd04bf72ca74185dc8e5a766)
- Feat: captures icon fonts [`7b39e5f`](https://github.com/zumerlab/snapdom/commit/7b39e5fb964bc023f6d6fad555b357de5ab113f0)
- Omit process default styles - temporary [`2953196`](https://github.com/zumerlab/snapdom/commit/2953196e00aa6bf9d026df95089d3fc81812f24d)
- update to v.0.9.2 [`e0179a1`](https://github.com/zumerlab/snapdom/commit/e0179a160e361a1e7d58ee5e83747f385cacb887)
- Add options as Object and allow bgColor on jpg and webp [`e5abaa7`](https://github.com/zumerlab/snapdom/commit/e5abaa72de77f75ebe6901935c5f539cda253db2)
- Update commented docs [`cfd2272`](https://github.com/zumerlab/snapdom/commit/cfd2272b065e8c11fff1a729c6cbec1f14000668)
- Update README.md [`fef6751`](https://github.com/zumerlab/snapdom/commit/fef6751ffa90d379c8d829998277825daddc27b8)
- update [`26ff7ea`](https://github.com/zumerlab/snapdom/commit/26ff7ea0528d569820bed8748520a7d02c6506cd)
- Update README.md [`3fda999`](https://github.com/zumerlab/snapdom/commit/3fda999bbdefb5aa32186bb07c59249f9a86e7e9)
- Update README.md [`8ee616b`](https://github.com/zumerlab/snapdom/commit/8ee616baf0059eefcf7e83e7930f5ab8f3850eb5)
- Omit delay function - temporary [`0f04721`](https://github.com/zumerlab/snapdom/commit/0f04721c458ba921694ee38117b8e0b8231a8c1a)
- update unpkg url [`13ce66b`](https://github.com/zumerlab/snapdom/commit/13ce66bfee83802c32edfd9019959540d260cf84)
- Bumped version [`9e4f518`](https://github.com/zumerlab/snapdom/commit/9e4f51885bddebd5364fb4ca96647233304e0dc7)
- Update README.md [`3733476`](https://github.com/zumerlab/snapdom/commit/373347665ca89249244038eaf48731f6d7ee37b8)
- Update README.md [`00c74b0`](https://github.com/zumerlab/snapdom/commit/00c74b07881373275d8c0e5144696d594b031e7e)
- Update README.md [`dd2c9c5`](https://github.com/zumerlab/snapdom/commit/dd2c9c5dd507a432e4dc75e67c5d2d311073e791)
- Update README.md [`d271cf7`](https://github.com/zumerlab/snapdom/commit/d271cf77f5747ee69df07785fef34e8c5e63649e)
- Update README.md [`02bf650`](https://github.com/zumerlab/snapdom/commit/02bf6506ae7e3cf03507e10d8f76983c07f39c66)
#### [v0.9.1](https://github.com/zumerlab/snapdom/compare/v0.9.0...v0.9.1)
> 27 April 2025
- update [`d90fcb9`](https://github.com/zumerlab/snapdom/commit/d90fcb97bdeb75a2adaaa14b25bd6ebced4a70e2)
- Bumped version [`99c286a`](https://github.com/zumerlab/snapdom/commit/99c286a8883ede66ff93aa96a62d008411e4ded0)
- fix change files prop [`548adbe`](https://github.com/zumerlab/snapdom/commit/548adbe9490b0ed4fd7e9fb77e7d6e69a6dc28c9)
- update [`f70a917`](https://github.com/zumerlab/snapdom/commit/f70a9173c7b11d659e6bf80c6ef60b9f71e652b7)
#### v0.9.0
> 27 April 2025
- first public version [`aac1d99`](https://github.com/zumerlab/snapdom/commit/aac1d997836362dd008d6372173c9dd84a76197f)
- Initial commit [`fb1c063`](https://github.com/zumerlab/snapdom/commit/fb1c06307b4b822bb898477beca46f88109ac196)
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 ZumerLab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<p align="center">
<a href="http://zumerlab.github.io/snapdom">
<img src="https://raw.githubusercontent.com/zumerlab/snapdom/main/docs/assets/newhero.png" width="80%">
</a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@zumer/snapdom">
<img alt="NPM version" src="https://img.shields.io/npm/v/@zumer/snapdom?style=flat-square&label=Version">
</a>
<a href="https://www.npmjs.com/package/@zumer/snapdom">
<img alt="NPM weekly downloads" src="https://img.shields.io/npm/dw/@zumer/snapdom?style=flat-square&label=Downloads">
</a>
<a href="https://github.com/zumerlab/snapdom/graphs/contributors">
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/zumerlab/snapdom?style=flat-square&label=Contributors">
</a>
<a href="https://github.com/zumerlab/snapdom/stargazers">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/zumerlab/snapdom?style=flat-square&label=Stars">
</a>
<a href="https://github.com/zumerlab/snapdom/network/members">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/zumerlab/snapdom?style=flat-square&label=Forks">
</a>
<a href="https://github.com/sponsors/tinchox5">
<img alt="Sponsor tinchox5" src="https://img.shields.io/github/sponsors/tinchox5?style=flat-square&label=Sponsor">
</a>
<a href="https://github.com/zumerlab/snapdom/blob/main/LICENSE">
<img alt="License" src="https://img.shields.io/github/license/zumerlab/snapdom?style=flat-square">
</a>
</p>
<p align="center">English | <a href="README_CN.md">简体中文</a></p>
# SnapDOM
**SnapDOM** is a next-generation **DOM Capture Engine** — ultra-fast, modular, and extensible.
It converts any DOM subtree into a self-contained representation that can be exported to SVG, PNG, JPG, WebP, Canvas, Blob, or **any custom format** through plugins.
* Full DOM capture
* Embedded styles, pseudo-elements, and fonts
* Export to SVG, PNG, JPG, WebP, `canvas`, or Blob
* ⚡ Ultra fast, no dependencies
* 100% based on standard Web APIs
* Support same-origin `ìframe`
* Support CSS counter() and CSS counters()
* Support `...` line-clamp
## Demo
[https://snapdom.dev](https://snapdom.dev)
## Quick Start
**Capture any DOM element to PNG in one line:**
```js
import { snapdom } from '@zumer/snapdom';
const img = await snapdom.toPng(document.querySelector('#card'));
document.body.appendChild(img);
```
**Reusable capture** (one clone, multiple exports):
```js
const result = await snapdom(document.querySelector('#card'));
await result.toPng(); // → HTMLImageElement
await result.toSvg(); // → SVG as Image
await result.download({ format: 'jpg', filename: 'card.jpg' });
```
---
## Capture Flow
SnapDOM transforms your DOM element through these stages:
```
DOM Element
↓
Clone
↓
Styles & Pseudo
↓
Images & Backgrounds
↓
Fonts
↓
SVG foreignObject
↓
data:image/svg+xml
↓
toPng / toSvg / toBlob / download
```
| Stage | What happens |
|-------|--------------|
| **Clone** | Deep clone with styles, Shadow DOM, iframes. Exclude/filter nodes. |
| **Styles & Pseudo** | Inline `::before`/`::after` as elements, resolve `counter()`/`counters()`. |
| **Images & Backgrounds** | Fetch and inline external images/backgrounds as data URLs. |
| **Fonts** | Embed `@font-face` (optional) and icon fonts. |
| **SVG** | Wrap clone in `<foreignObject>`, serialize to `data:image/svg+xml`. |
| **Export** | Convert SVG to PNG/JPG/WebP/Blob or trigger download. |
Plugin hooks: `beforeSnap` → `beforeClone` → `afterClone` → `beforeRender` → `afterRender` → `beforeExport` → `afterExport`.
## Table of Contents
- [Quick Start](#quick-start)
- [Capture Flow](#capture-flow)
- [Installation](#installation)
- [NPM / Yarn (stable)](#npm--yarn-stable)
- [NPM / Yarn (dev builds)](#npm--yarn-dev-builds)
- [CDN (stable)](#cdn-stable)
- [CDN (dev builds)](#cdn-dev-builds)
- [Build Outputs](#build-outputs--tree-shaking)
- [Usage](#usage)
- [Reusable capture](#reusable-capture)
- [One-step shortcuts](#one-step-shortcuts)
- [API](#api)
- [snapdom(el, options?)](#snapdomel-options)
- [Shortcut methods](#shortcut-methods)
- [Options](#options)
- [debug](#debug)
- [Fallback image on `<img>` load failure](#fallback-image-on-img-load-failure)
- [Dimensions (`scale`, `width`, `height`)](#dimensions-scale-width-height)
- [Cross-Origin Images & Fonts (`useProxy`)](#cross-origin-images--fonts-useproxy)
- [Fonts](#fonts)
- [embedFonts](#embedfonts)
- [localFonts](#localfonts)
- [iconFonts](#iconfonts)
- [excludeFonts](#excludefonts)
- [Filtering nodes: `exclude` vs `filter`](#filtering-nodes-exclude-vs-filter)
- [outerTransforms](#outerTransforms)
- [outerShadows](#no-shadows)
- [Cache control](#cache-control)
- [preCache](#precache--optional-helper)
- [Plugins (BETA)](#plugins-beta)
- [Registering Plugins](#registering-plugins)
- [Plugin Lifecycle Hooks](#plugin-lifecycle-hooks)
- [Context Object](#context-object)
- [Custom Exports via Plugins](#custom-exports-via-plugins)
- [Example: Overlay Filter Plugin](#example-overlay-filter-plugin)
- [Full Plugin Template](#full-plugin-template)
- [Limitations](#limitations)
- [⚡ Performance Benchmarks (Chromium)](#performance-benchmarks)
- [Simple elements](#simple-elements)
- [Complex elements](#complex-elements)
- [Run the benchmarks](#run-the-benchmarks)
- [Roadmap](#roadmap)
- [Development](#development)
- [Contributors 🙌](#contributors)
- [💖 Sponsors](#sponsors)
- [Star History](#star-history)
- [License](#license)
## Installation
### NPM / Yarn (stable)
```bash
npm i @zumer/snapdom
yarn add @zumer/snapdom
```
### NPM / Yarn (dev builds)
For early access to new features and fixes:
```bash
npm i @zumer/snapdom@dev
yarn add @zumer/snapdom@dev
```
⚠️ The `@dev` tag usually includes improvements before they reach production, but may be less stable.
### CDN (stable)
```html
<!-- Minified build -->
<script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
<!-- Minified ES Module build -->
<script type="module">
import { snapdom } from "https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs";
</script>
```
### CDN (dev builds)
```html
<!-- Minified build (dev) -->
<script src="https://unpkg.com/@zumer/snapdom@dev/dist/snapdom.js"></script>
<!-- Minified ES Module build (dev) -->
<script type="module">
import { snapdom } from "https://unpkg.com/@zumer/snapdom@dev/dist/snapdom.mjs";
</script>
```
## Build Outputs
| Variant | File | Use case |
|---------|------|----------|
| **ESM** (tree-shakeable) | `dist/snapdom.mjs` | Bundlers (Vite, webpack), `import` |
| **IIFE** (global) | `dist/snapdom.js` | Script tag, legacy `require` |
**Bundler (npm):**
```js
import { snapdom } from '@zumer/snapdom'; // → dist/snapdom.mjs
```
**Script tag (CDN):**
```html
<script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
<script> snapdom.toPng(document.body).then(img => document.body.appendChild(img)); </script>
```
**Subpath imports** (lighter bundle if you only need one):
```js
import { preCache } from '@zumer/snapdom/preCache';
import { plugins } from '@zumer/snapdom/plugins';
```
## Usage
| Pattern | When to use |
|---------|-------------|
| **Reusable** `snapdom(el)` | One clone → many exports (PNG + JPG + download). |
| **Shortcuts** `snapdom.toPng(el)` | Single export, less code. |
### Reusable capture
Capture once, export many times (no re-clone):
```js
const el = document.querySelector('#target');
const result = await snapdom(el);
const img = await result.toPng();
document.body.appendChild(img);
await result.download({ format: 'jpg', filename: 'my-capture.jpg' });
```
### One-step shortcuts
Direct export when you need a single format:
```js
const png = await snapdom.toPng(el);
const blob = await snapdom.toBlob(el);
document.body.appendChild(png);
```
## API
### `snapdom(el, options?)`
Returns an object with reusable export methods:
```js
{
url: string;
toRaw(): string;
toImg(): Promise<HTMLImageElement>; // deprecated
toSvg(): Promise<HTMLImageElement>;
toCanvas(): Promise<HTMLCanvasElement>;
toBlob(options?): Promise<Blob>;
toPng(options?): Promise<HTMLImageElement>;
toJpg(options?): Promise<HTMLImageElement>;
toWebp(options?): Promise<HTMLImageElement>;
download(options?): Promise<void>;
}
```
### Shortcut methods
| Method | Description |
| ------------------------------ | --------------------------------- |
| `snapdom.toImg(el, options?)` | Returns an SVG `HTMLImageElement` (deprecated) |
| `snapdom.toSvg(el, options?)` | Returns an SVG `HTMLImageElement` |
| `snapdom.toCanvas(el, options?)` | Returns a `Canvas` |
| `snapdom.toBlob(el, options?)` | Returns an SVG or raster `Blob` |
| `snapdom.toPng(el, options?)` | Returns a PNG image |
| `snapdom.toJpg(el, options?)` | Returns a JPG image |
| `snapdom.toWebp(el, options?)` | Returns a WebP image |
| `snapdom.download(el, options?)` | Triggers a download |
### Exporter-specific options
Some exporters accept a small set of **export-only options** in addition to the global capture options.
#### `download()`
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `filename` | `string` | `snapdom` | Download name. |
| `format` | `"png" \| "jpeg" \| "jpg" \| "webp" \| "svg"` | `"png"` | Output format for the downloaded file. |
**Example:**
```js
await result.download({
format: 'jpg',
quality: 0.92,
filename: 'my-capture'
});
```
#### `toBlob()`
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `"svg" \| "png" \| "jpeg" \| "jpg" \| "webp"` | `"svg"` | Blob type to generate. |
**Example:**
```js
const blob = await result.toBlob({ type: 'jpeg', quality: 0.92 });
```
## Options
All capture methods accept an `options` object:
| Option | Type | Default | Description |
| ----------------- | -------- | -------- | ----------------------------------------------- |
| `debug` | boolean | `false` | When `true`, logs suppressed errors to `console.warn` for troubleshooting |
| `fast` | boolean | `true` | Skips small idle delays for faster results |
| `embedFonts` | boolean | `false` | Inlines non-icon fonts (icon fonts always on) |
| `localFonts` | array | `[]` | Local fonts `{ family, src, weight?, style? }` |
| `iconFonts` | string\|RegExp\|Array | `[]` | Extra icon font matchers |
| `excludeFonts` | object | `{}` | Exclude families/domains/subsets during embedding |
| `scale` | number | `1` | Output scale multiplier |
| `dpr` | number | `devicePixelRatio` | Device pixel ratio |
| `width` | number | - | Output width |
| `height` | number | - | Output height |
| `backgroundColor` | string | `"#fff"` | Fallback color for JPG/WebP |
| `quality` | number | `1` | Quality for JPG/WebP (0 to 1) |
| `useProxy` | string | `''` | Proxy base for CORS fallbacks |
| `exclude` | string[] | - | CSS selectors to exclude |
| `excludeMode` | `"hide"`\|`"remove"` | `"hide"` | How `exclude` is applied |
| `filter` | function | - | Custom predicate `(el) => boolean` |
| `filterMode` | `"hide"`\|`"remove"` | `"hide"` | How `filter` is applied |
| `cache` | string | `"soft"` | `disabled` \| `soft` \| `auto` \| `full` |
| `placeholders` | boolean | `true` | Show placeholders for images/CORS iframes |
| `fallbackURL` | string \| function | - | Fallback image for `<img>` load failure |
| `outerTransforms` | boolean | `true` | When `false` removes `translate/rotate` but preserves `scale/skew`, producing a flat, reusable capture |
| `outerShadows` | boolean | `false` | Do not expand the root’s bounding box for shadows/blur/outline, and strip those visual effects from the cloned root |
| `safariWarmupAttempts` | number | `3` | Safari only: iterations to prime font/decode (WebKit #219770). Use `1` if 3 causes lag |
### debug
When `debug: true`, SnapDOM logs normally suppressed errors to `console.warn` (with the `[snapdom]` prefix). Useful for troubleshooting capture issues (canvas failures, blob resolution, style stripping, etc.) without noisy output in production.
```js
await snapdom.toPng(el, { debug: true });
```
### Fallback image on `<img>` load failure
Provide a default image for failed `<img>` loads. You can pass a fixed URL or a callback that receives measured dimensions and returns a URL (handy to generate dynamic placeholders).
```js
// 1) Fixed URL fallback
await snapdom.toSvg(element, {
fallbackURL: '/images/fallback.png'
});
// 2) Dynamic placeholder via callback
await snapdom.toSvg(element, {
fallbackURL: ({ width: 300, height: 150 }) =>
`https://placehold.co/${width}x${height}`
});
// 3) With proxy (if your fallback host has no CORS)
await snapdom.toSvg(element, {
fallbackURL: ({ width = 300, height = 150 }) =>
`https://dummyimage.com/${width}x${height}/cccccc/666.png&text=img`,
useProxy: 'https://proxy.corsfix.com/?'
});
```
Notes:
- If the fallback image also fails to load, snapDOM replaces the `<img>` with a placeholder block preserving width/height.
- Width/height used by the callback are gathered from the original element (dataset, style/attrs, etc.) when available.
### Dimensions (`scale`, `width`, `height`)
* If `scale` is provided, it **takes precedence** over `width`/`height`.
* If only `width` is provided, height scales proportionally (and vice versa).
* Providing both `width` and `height` forces an exact size (may distort).
### Cross-Origin Images & Fonts (`useProxy`)
By default snapDOM tries `crossOrigin="anonymous"` (or `use-credentials` for same-origin). If an asset is CORS-blocked, you can set `useProxy` to a prefix URL that forwards the actual `src`:
```js
await snapdom.toPng(el, {
useProxy: 'https://proxy.corsfix.com/?' // Note: Any cors proxy could be used 'https://proxy.corsfix.com/?'
});
```
* The proxy is only used as a **fallback**; same-origin and CORS-enabled assets skip it.
### Fonts
#### `embedFonts`
When `true`, snapDOM embeds **non-icon** `@font-face` rules detected as used within the captured subtree. Icon fonts (Font Awesome, Material Icons, etc.) are embedded **always**.
#### `localFonts`
If you serve fonts yourself or have data URLs, you can declare them here to avoid extra CSS discovery:
```js
await snapdom.toPng(el, {
embedFonts: true,
localFonts: [
{ family: 'Inter', src: '/fonts/Inter-Variable.woff2', weight: 400, style: 'normal' },
{ family: 'Inter', src: '/fonts/Inter-Italic.woff2', style: 'italic' }
]
});
```
#### `iconFonts`
Add custom icon families (names or regex matchers). Useful for private icon sets:
```js
await snapdom.toPng(el, {
iconFonts: ['MyIcons', /^(Remix|Feather) Icons?$/i]
});
```
#### `excludeFonts`
Skip specific non-icon fonts to speed up capture or avoid unnecessary downloads.
```js
await snapdom.toPng(el, {
embedFonts: true,
excludeFonts: {
families: ['Noto Serif', 'SomeHeavyFont'], // skip by family name
domains: ['fonts.gstatic.com', 'cdn.example'], // skip by source host
subsets: ['cyrillic-ext'] // skip by unicode-range subset tag
}
});
```
*Notes*
- `excludeFonts` only applies to **non-icon** fonts. Icon fonts are always embedded.
- Matching is case-insensitive for `families`. Hosts are matched by substring against the resolved URL.
#### Filtering nodes: `exclude` vs `filter`
* `exclude`: remove by **selector**.
* `excludeMode`: `hide` applies `visibility:hidden` CSS rule on excluded nodes and the layout remains as the original. `remove` do not clone excluded nodes at all.
* `filter`: advanced predicate per element (return `false` to drop).
* `filterMode`: `hide` applies `visibility:hidden` CSS rule on filtered nodes and the layout remains as the original. `remove` do not clone filtered nodes at all.
**Example: filter out elements with `display:none`:**
```js
/**
* Example filter: skip elements with display:none
* @param {Element} el
* @returns {boolean} true = keep, false = exclude
*/
function filterHidden(el) {
const cs = window.getComputedStyle(el);
if (cs.display === 'none') return false;
return true;
}
await snapdom.toPng(document.body, { filter: filterHidden });
```
**Example with `exclude`:** remove banners or tooltips by selector
```js
await snapdom.toPng(el, {
exclude: ['.cookie-banner', '.tooltip', '[data-test="debug"]']
});
```
### outerTransforms
When capturing rotated or translated elements, you may want use **outerTransforms: false** option if you want to eliminate those external transforms. So, the output is **flat, upright, and ready** to use elsewhere.
- **`outerTransforms: true (default)`**
**Keeps the original `transforms` and `rotate`**.
### outerShadows
- **`outerShadows: false (default)`**
Prevents expanding the bounding box for shadows, blur, or outline on the root, and also strips `box-shadow`, `text-shadow`, `filter: blur()/drop-shadow()`, and `outline` from the cloned root.
> 💡 **Tip:** Using both (`outerTransforms: false` + `outerShadows: false`) produces a strict, minimal bounding box with no visual bleed.
**Example**
```js
// outerTransforms and remove shadow bleed
await snapdom.toSvg(el, { outerTransforms: true, outerShadows: true });
```
## Cache control
SnapDOM maintains internal caches for images, backgrounds, resources, styles, and fonts.
You can control how they are cleared between captures using the `cache` option:
| Mode | Description |
| ----------- | --------------------------------------------------------------------------- |
| `"disabled"`| No cache |
| `"soft"` | Clears session caches (`styleMap`, `nodeMap`, `styleCache`) _(default)_ |
| `"auto"` | Minimal cleanup: only clears transient maps |
| `"full"` | Keeps all caches (nothing is cleared, maximum performance) |
**Examples:**
```js
// Use minimal but fast cache
await snapdom.toPng(el, { cache: 'auto' });
// Keep everything in memory between captures
await snapdom.toPng(el, { cache: 'full' });
// Force a full cleanup on every capture
await snapdom.toPng(el, { cache: 'disabled' });
```
## `preCache()` – Optional helper
Preloads external resources to avoid first-capture stalls (helpful for big/complex trees).
```js
import { preCache } from '@zumer/snapdom';
await preCache({
root: document.body,
embedFonts: true,
localFonts: [{ family: 'Inter', src: '/fonts/Inter.woff2', weight: 400 }],
useProxy: 'https://proxy.corsfix.com/?'
});
```
## Plugins (BETA)
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.
A plugin is a simple object with a unique `name` and one or more lifecycle **hooks**.
Hooks can be synchronous or `async`, and they receive a shared **`context`** object.
### Registering Plugins
**Global registration** (applies to all captures):
```js
import { snapdom } from '@zumer/snapdom';
// You can register instances, factories, or [factory, options]
snapdom.plugins(
myPluginInstance,
[myPluginFactory, { optionA: true }],
{ plugin: anotherFactory, options: { level: 2 } }
);
```
**Per-capture registration** (only for that specific call):
```js
const out = await snapdom(element, {
plugins: [
[overlayFilterPlugin, { color: 'rgba(0,0,0,0.25)' }],
[myFullPlugin, { providePdf: true }]
]
});
```
* **Execution order = registration order** (first registered, first executed).
* **Per-capture plugins** run **before** global ones.
* Duplicates are automatically skipped by `name`; a per-capture plugin with the same `name` overrides its global version.
### Plugin Lifecycle Hooks
Hooks run in capture order (see [Capture Flow](#capture-flow)):
| Hook | Stage | Purpose |
|------|-------|---------|
| `beforeSnap` | Start | Adjust options before any work. |
| `beforeClone` | Pre-clone | Before DOM clone (modify live DOM carefully). |
| `afterClone` | Post-clone | Modify cloned tree safely (e.g. inject overlay). |
| `beforeRender` | Pre-serialize | Right before SVG → data URL. |
| `afterRender` | Post-serialize | Inspect `context.svgString` / `context.dataURL`. |
| `beforeExport` | Per export | Before each `toPng`, `toSvg`, etc. |
| `afterExport` | Per export | Transform returned result. |
| `afterSnap` | Once | After first export; cleanup. |
| `defineExports` | Setup | Add custom exporters (e.g. `toPdf`). |
> Returned values from `afterExport` are chained to the next plugin (transform pipeline).
### Context Object
Every hook receives a single `context` object that contains normalized capture state:
* **Input & options:**
`element`, `debug`, `fast`, `scale`, `dpr`, `width`, `height`, `backgroundColor`, `quality`, `useProxy`, `cache`, `outerTransforms`, `outerShadows`, `safariWarmupAttempts`, `embedFonts`, `localFonts`, `iconFonts`, `excludeFonts`, `exclude`, `excludeMode`, `filter`, `filterMode`, `fallbackURL`.
* **Intermediate values (depending on stage):**
`clone`, `classCSS`, `styleCache`, `fontsCSS`, `baseCSS`, `svgString`, `dataURL`.
* **During export:**
`context.export = { type, options, url }`
where `type` is the exporter name (`"png"`, `"jpeg"`, `"svg"`, `"blob"`, etc.), and `url` is the serialized SVG base.
> 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.
## Custom Exports via Plugins
Plugins can add new exports using `defineExports(context)`.
For each export key you return (e.g., `"pdf"`), SnapDOM automatically exposes a helper method named **`toPdf()`** on the capture result.
**Register the plugin (global or per capture):**
```js
import { snapdom } from '@zumer/snapdom';
// global
snapdom.plugins(pdfExportPlugin());
// or per capture
const out = await snapdom(element, { plugins: [pdfExportPlugin()] });
```
**Call the custom export:**
```js
const out = await snapdom(document.querySelector('#report'));
// because the plugin returns { pdf: async (ctx, opts) => ... }
const pdfBlob = await out.toPdf({
// exporter-specific options (width, height, quality, filename, etc.)
});
```
### Example: Overlay Filter Plugin
Adds a translucent overlay or color filter **only** to the captured clone (not your live DOM).
Useful for highlighting or dimming sections before export.
```js
/**
* Ultra-simple overlay filter for SnapDOM (HTML-only).
* Inserts a full-size <div> overlay on the cloned root.
*
* @param {{ color?: string; blur?: number }} [options]
* color: overlay color (rgba/hex/hsl). Default: 'rgba(0,0,0,0.25)'
* blur: optional blur in px (default: 0)
*/
export function overlayFilterPlugin(options = {}) {
const color = options.color ?? 'rgba(0,0,0,0.25)';
const blur = Math.max(0, options.blur ?? 0);
return {
name: 'overlay-filter',
/**
* Add a full-coverage overlay to the cloned HTML root.
* @param {any} context
*/
async afterClone(context) {
const root = context.clone;
if (!(root instanceof HTMLElement)) return; // HTML-only
// Ensure containing block so absolute overlay anchors to the root
if (getComputedStyle(root).position === 'static') {
root.style.position = 'relative';
}
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.left = '0';
overlay.style.top = '0';
overlay.style.right = '0';
overlay.style.bottom = '0';
overlay.style.background = color;
overlay.style.pointerEvents = 'none';
if (blur) overlay.style.filter = `blur(${blur}px)`;
root.appendChild(overlay);
}
};
}
```
**Usage:**
```js
import { snapdom } from '@zumer/snapdom';
// Global registration
snapdom.plugins([overlayFilterPlugin, { color: 'rgba(0,0,0,0.3)', blur: 2 }]);
// Per-capture
const out = await snapdom(document.querySelector('#card'), {
plugins: [[overlayFilterPlugin, { color: 'rgba(255,200,0,0.15)' }]]
});
const png = await out.toPng();
document.body.appendChild(png);
```
> The overlay is injected **only in the cloned tree**, never in your live DOM, ensuring perfect fidelity and zero flicker.
### Full Plugin Template
Use this as a starting point for custom logic or exporters.
```js
export function myPlugin(options = {}) {
return {
/** Unique name used for de-duplication/overrides */
name: 'my-plugin',
/** Early adjustments before any clone/style work. */
async beforeSnap(context) {},
/** Before subtree cloning (use sparingly if touching the live DOM). */
async beforeClone(context) {},
/** After subtree cloning (safe to modify the cloned tree). */
async afterClone(context) {},
/** Right before serialization (SVG/dataURL). */
async beforeRender(context) {},
/** After serialization; inspect context.svgString/context.dataURL if needed. */
async afterRender(context) {},
/** Before EACH export call (toPng/toSvg/toBlob/...). */
async beforeExport(context) {},
/**
* After EACH export call.
* If you return a value, it becomes the result for the next plugin (chaining).
*/
async afterExport(context, result) { return result; },
/**
* Define custom exporters (auto-added as helpers like out.toPdf()).
* Return a map { [key: string]: (ctx:any, opts:any) => Promise<any> }.
*/
async defineExports(context) { return {}; },
/** Runs ONCE after the FIRST export finishes (cleanup). */
async afterSnap(context) {}
};
}
```
**Quick recap:**
* Plugins can modify capture behavior (`beforeSnap`, `afterClone`, etc.).
* You can inject visuals or transformations safely into the cloned tree.
* New exporters defined in `defineExports()` automatically become helpers like `out.toPdf()`.
* All hooks can be asynchronous, run in order, and share the same `context`.
## Limitations
* External images should be CORS-accessible (use `useProxy` option for handling CORS denied)
* When WebP format is used on Safari, it will fallback to PNG rendering.
* `@font-face` CSS rule is well supported, but if need to use JS `FontFace()`, see this workaround [`#43`](https://github.com/zumerlab/snapdom/issues/43)
* **Safari**: captures with `embedFonts` or background/mask images run slower due to [WebKit #219770](https://bugs.webkit.org/show_bug.cgi?id=219770) (font decode timing). SnapDOM does pre-captures + `drawImage` to prime the pipeline; configurable via `safariWarmupAttempts` (default 3).
* **Custom scrollbar styles** (`::-webkit-scrollbar`): Applied only when the element has *not* been scrolled. When scrolled, the viewport content is captured without the scrollbar.
## Performance Benchmarks
**Setup.** Vitest benchmarks on Chromium, repo tests. Hardware may affect results.
Values are **average capture time (ms)** → lower is better.
### Simple elements
| Scenario | SnapDOM current | SnapDOM v1.9.9 | html2canvas | html-to-image |
| ------------------------ | --------------- | -------------- | ----------- | ------------- |
| Small (200×100) | **0.5 ms** | 0.8 ms | 67.7 ms | 3.1 ms |
| Modal (400×300) | **0.5 ms** | 0.8 ms | 75.5 ms | 3.6 ms |
| Page View (1200×800) | **0.5 ms** | 0.8 ms | 114.2 ms | 3.3 ms |
| Large Scroll (2000×1500) | **0.5 ms** | 0.8 ms | 186.3 ms | 3.2 ms |
| Very Large (4000×2000) | **0.5 ms** | 0.9 ms | 425.9 ms | 3.3 ms |
### Complex elements
| Scenario | SnapDOM current | SnapDOM v1.9.9 | html2canvas | html-to-image |
| ------------------------ | --------------- | -------------- | ----------- | ------------- |
| Small (200×100) | **1.6 ms** | 3.3 ms | 68.0 ms | 14.3 ms |
| Modal (400×300) | **2.9 ms** | 6.8 ms | 87.5 ms | 34.8 ms |
| Page View (1200×800) | **17.5 ms** | 50.2 ms | 178.0 ms | 429.0 ms |
| Large Scroll (2000×1500) | **54.0 ms** | 201.8 ms | 735.2 ms | 984.2 ms |
| Very Large (4000×2000) | **171.4 ms** | 453.7 ms | 1,800.4 ms | 2,611.9 ms |
### Run the benchmarks
```sh
git clone https://github.com/zumerlab/snapdom.git
cd snapdom
npm install
npm run test:benchmark
```
## Roadmap
Planned improvements for future versions of SnapDOM:
* [X] **Implement plugin system**
SnapDOM will support external plugins to extend or override internal behavior (e.g. custom node transformers, exporters, or filters).
* [ ] **Refactor to modular architecture**
Internal logic will be split into smaller, focused modules to improve maintainability and code reuse.
* [X] **Decouple internal logic from global options**
Functions will be redesigned to avoid relying directly on `options`. A centralized capture context will improve clarity, autonomy, and testability. See [`next` branch](https://github.com/zumerlab/snapdom/tree/main)
* [X] **Expose cache control**
Users will be able to manually clear image and font caches or configure their own caching strategies.
* [X] **Auto font preloading**
Required fonts will be automatically detected and preloaded before capture, reducing the need for manual `preCache()` calls.
* [X] **Document plugin development**
A full guide will be provided for creating and registering custom SnapDOM plugins.
* [ ] **Make export utilities tree-shakeable**
Export functions like `toPng`, `toJpg`, `toBlob`, etc. will be restructured into independent modules to support tree shaking and minimal builds.
Have ideas or feature requests?
Feel free to share suggestions or feedback in [GitHub Discussions](https://github.com/zumerlab/snapdom/discussions).
## Development
**Source layout:**
- `src/api/` – Public API (`snapdom`, `preCache`)
- `src/core/` – Capture pipeline, clone, prepare, plugins
- `src/modules/` – Images, fonts, pseudo-elements, backgrounds, SVG
- `src/exporters/` – toPng, toSvg, toBlob, etc.
- `dist/` – Build output (`snapdom.js`, `snapdom.mjs`, `preCache.mjs`, `plugins.mjs`)
**Build:**
```sh
git clone https://github.com/zumerlab/snapdom.git
cd snapdom
git checkout dev
npm install
npm run compile
```
**Test:**
```sh
npx playwright install # Required for browser tests
npm test
npm run test:benchmark
```
For detailed guidelines, see [CONTRIBUTING](https://github.com/zumerlab/snapdom/blob/main/CONTRIBUTING.md).
## Contributors
<!-- CONTRIBUTORS:START -->
<p>
<a href="https://github.com/tinchox5" title="tinchox5"><img src="https://avatars.githubusercontent.com/u/11557901?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="tinchox5"/></a>
<a href="https://github.com/Jarvis2018" title="Jarvis2018"><img src="https://avatars.githubusercontent.com/u/36788851?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="Jarvis2018"/></a>
<a href="https://github.com/tarwin" title="tarwin"><img src="https://avatars.githubusercontent.com/u/646149?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="tarwin"/></a>
<a href="https://github.com/Amyuan23" title="Amyuan23"><img src="https://avatars.githubusercontent.com/u/25892910?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="Amyuan23"/></a>
<a href="https://github.com/airamhr9" title="airamhr9"><img src="https://avatars.githubusercontent.com/u/57371081?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="airamhr9"/></a>
<a href="https://github.com/FlavioLimaMindera" title="FlavioLimaMindera"><img src="https://avatars.githubusercontent.com/u/96424442?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="FlavioLimaMindera"/></a>
<a href="https://github.com/jswhisperer" title="jswhisperer"><img src="https://avatars.githubusercontent.com/u/1177690?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="jswhisperer"/></a>
<a href="https://github.com/K1ender" title="K1ender"><img src="https://avatars.githubusercontent.com/u/146767945?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="K1ender"/></a>
<a href="https://github.com/kohaiy" title="kohaiy"><img src="https://avatars.githubusercontent.com/u/15622127?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="kohaiy"/></a>
<a href="https://github.com/17biubiu" title="17biubiu"><img src="https://avatars.githubusercontent.com/u/13295895?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="17biubiu"/></a>
<a href="https://github.com/av01d" title="av01d"><img src="https://avatars.githubusercontent.com/u/6247646?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="av01d"/></a>
<a href="https://github.com/CHOYSEN" title="CHOYSEN"><img src="https://avatars.githubusercontent.com/u/25995358?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="CHOYSEN"/></a>
<a href="https://github.com/pedrocateexte" title="pedrocateexte"><img src="https://avatars.githubusercontent.com/u/207524750?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="pedrocateexte"/></a>
<a href="https://github.com/domialex" title="domialex"><img src="https://avatars.githubusercontent.com/u/4694217?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="domialex"/></a>
<a href="https://github.com/elliots" title="elliots"><img src="https://avatars.githubusercontent.com/u/622455?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="elliots"/></a>
<a href="https://github.com/stypr" title="stypr"><img src="https://avatars.githubusercontent.com/u/6625978?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="stypr"/></a>
<a href="https://github.com/mon-jai" title="mon-jai"><img src="https://avatars.githubusercontent.com/u/91261297?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="mon-jai"/></a>
<a href="https://github.com/sharuzzaman" title="sharuzzaman"><img src="https://avatars.githubusercontent.com/u/7421941?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="sharuzzaman"/></a>
<a href="https://github.com/simon1uo" title="simon1uo"><img src="https://avatars.githubusercontent.com/u/60037549?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="simon1uo"/></a>
<a href="https://github.com/titoBouzout" title="titoBouzout"><img src="https://avatars.githubusercontent.com/u/64156?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="titoBouzout"/></a>
<a href="https://github.com/ZiuChen" title="ZiuChen"><img src="https://avatars.githubusercontent.com/u/64892985?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="ZiuChen"/></a>
<a href="https://github.com/harshasiddartha" title="harshasiddartha"><img src="https://avatars.githubusercontent.com/u/147021873?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="harshasiddartha"/></a>
<a href="https://github.com/karasHou" title="karasHou"><img src="https://avatars.githubusercontent.com/u/27048083?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="karasHou"/></a>
<a href="https://github.com/jhbae200" title="jhbae200"><img src="https://avatars.githubusercontent.com/u/20170610?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="jhbae200"/></a>
<a href="https://github.com/xiaobai-web715" title="xiaobai-web715"><img src="https://avatars.githubusercontent.com/u/81091224?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="xiaobai-web715"/></a>
<a href="https://github.com/miusuncle" title="miusuncle"><img src="https://avatars.githubusercontent.com/u/7549857?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="miusuncle"/></a>
<a href="https://github.com/rbbydotdev" title="rbbydotdev"><img src="https://avatars.githubusercontent.com/u/101137670?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="rbbydotdev"/></a>
<a href="https://github.com/zhanghaotian2018" title="zhanghaotian2018"><img src="https://avatars.githubusercontent.com/u/169218899?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="zhanghaotian2018"/></a>
</p>
<!-- CONTRIBUTORS:END -->
## Sponsors
Special thanks to [@megaphonecolin](https://github.com/megaphonecolin), [@sdraper69](https://github.com/sdraper69), [@reynaldichernando](https://github.com/reynaldichernando), [@gamma-app](https://github.com/gamma-app) and [@jrjohnson](https://github.com/jrjohnson),for supporting this project!
If you'd like to support this project too, you can [become a sponsor](https://github.com/sponsors/tinchox5).
## Star History
[](https://www.star-history.com/#zumerlab/snapdom&Date)
## License
MIT © Zumerlab
================================================
FILE: README_CN.md
================================================
<p align="center">
<a href="http://zumerlab.github.io/snapdom">
<img src="https://raw.githubusercontent.com/zumerlab/snapdom/main/docs/assets/newhero.png" width="80%">
</a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@zumer/snapdom">
<img alt="NPM version" src="https://img.shields.io/npm/v/@zumer/snapdom?style=flat-square&label=Version">
</a>
<a href="https://www.npmjs.com/package/@zumer/snapdom">
<img alt="NPM weekly downloads" src="https://img.shields.io/npm/dw/@zumer/snapdom?style=flat-square&label=Downloads">
</a>
<a href="https://github.com/zumerlab/snapdom/graphs/contributors">
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/zumerlab/snapdom?style=flat-square&label=Contributors">
</a>
<a href="https://github.com/zumerlab/snapdom/stargazers">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/zumerlab/snapdom?style=flat-square&label=Stars">
</a>
<a href="https://github.com/zumerlab/snapdom/network/members">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/zumerlab/snapdom?style=flat-square&label=Forks">
</a>
<a href="https://github.com/sponsors/tinchox5">
<img alt="Sponsor tinchox5" src="https://img.shields.io/github/sponsors/tinchox5?style=flat-square&label=Sponsor">
</a>
<a href="https://github.com/zumerlab/snapdom/blob/main/LICENSE">
<img alt="License" src="https://img.shields.io/github/license/zumerlab/snapdom?style=flat-square">
</a>
</p>
<p align="center"><a href="README.md">English</a> | 简体中文</p>
# snapDOM
**SnapDOM** 是新一代的 **DOM 捕获引擎(DOM Capture Engine)**——超高速、模块化、可扩展。
它可以将任意 DOM 子树转换为自包含的结构,并导出为 SVG、PNG、JPG、WebP、Canvas、Blob,或通过插件系统生成 **任何自定义格式**。
SnapDOM 会保留样式、字体、背景图像、伪元素、Shadow DOM 等所有视觉特性,并通过可扩展的架构实现强大的灵活性和最高级别的捕获质量。
* 完整的 DOM 捕获
* 内嵌样式、伪元素和字体
* 导出为 SVG、PNG、JPG、WebP、`canvas` 或 Blob
* ⚡ 超快速度,无依赖
* 100% 基于标准 Web API
* 支持同源 `iframe`
* 支持 CSS counter() 和 CSS counters()
* 支持 `...` 文本截断(line-clamp)
## 演示
[https://snapdom.dev](https://snapdom.dev)
## 快速开始
**一行代码将任意 DOM 元素导出为 PNG:**
```js
import { snapdom } from '@zumer/snapdom';
const img = await snapdom.toPng(document.querySelector('#card'));
document.body.appendChild(img);
```
**可复用捕获**(一次克隆,多次导出):
```js
const result = await snapdom(document.querySelector('#card'));
await result.toPng(); // → HTMLImageElement
await result.toSvg(); // → SVG 图片
await result.download({ format: 'jpg', filename: 'card.jpg' });
```
---
## 捕获流程
SnapDOM 将 DOM 元素按以下阶段转换:
```
DOM Element
↓
Clone
↓
Styles & Pseudo
↓
Images & Backgrounds
↓
Fonts
↓
SVG foreignObject
↓
data:image/svg+xml
↓
toPng / toSvg / toBlob / download
```
| 阶段 | 说明 |
|------|------|
| **Clone** | 深度克隆,含样式、Shadow DOM、iframe。排除/过滤节点。 |
| **Styles & Pseudo** | 将 `::before`/`::after` 内联为元素,解析 `counter()`/`counters()`。 |
| **Images & Backgrounds** | 拉取并内联外部图片/背景为 data URL。 |
| **Fonts** | 嵌入 `@font-face`(可选)及图标字体。 |
| **SVG** | 将克隆包裹在 `<foreignObject>` 中,序列化为 `data:image/svg+xml`。 |
| **Export** | 转换为 PNG/JPG/WebP/Blob 或触发下载。 |
插件钩子顺序:`beforeSnap` → `beforeClone` → `afterClone` → `beforeRender` → `afterRender` → `beforeExport` → `afterExport`。
## 目录
- [快速开始](#快速开始)
- [捕获流程](#捕获流程)
- [安装](#安装)
- [NPM / Yarn (稳定版)](#npm--yarn-稳定版)
- [NPM / Yarn (开发版)](#npm--yarn-开发版)
- [CDN (稳定版)](#cdn-稳定版)
- [CDN (开发版)](#cdn-开发版)
- [构建产物](#构建产物与摇树优化)
- [用法](#基本用法)
- [可复用的捕获](#可复用的捕获)
- [一步式快捷方法](#一步式快捷方法)
- [API](#api)
- [snapdom(el, options?)](#snapdomel-options)
- [快捷方法](#快捷方法)
- [选项](#选项)
- [debug](#debug)
- [`<img>` 加载失败时的备用图片](#img-加载失败时的备用图片)
- [尺寸 (`scale`, `width`, `height`)](#尺寸-scale-width-height)
- [跨域图片和字体 (`useProxy`)](#跨域图片和字体-useproxy)
- [字体](#字体)
- [embedFonts](#embedfonts)
- [localFonts](#localfonts)
- [iconFonts](#iconfonts)
- [excludeFonts](#excludefonts)
- [节点过滤:`exclude` vs `filter`](#节点过滤-exclude-vs-filter)
- [outerTransforms](#outertransforms)
- [outerShadows](#outerShadows)
- [缓存控制](#缓存控制)
- [preCache](#precache--可选辅助函数)
- [插件(测试版)](#插件测试版)
- [注册插件](#注册插件)
- [插件生命周期钩子](#插件生命周期钩子)
- [上下文对象](#上下文对象)
- [通过插件自定义导出](#通过插件自定义导出)
- [示例:叠加滤镜插件](#示例叠加滤镜插件)
- [完整插件模板](#完整插件模板)
- [限制](#限制)
- [⚡ 性能基准测试(Chromium)](#性能基准测试chromium)
- [简单元素](#简单元素)
- [复杂元素](#复杂元素)
- [运行基准测试](#运行基准测试)
- [路线图](#路线图)
- [开发](#开发)
- [贡献者 🙌](#贡献者)
- [💖 赞助者](#赞助者)
- [Star 历史](#star-历史)
- [许可证](#许可证)
## 安装
### NPM / Yarn (稳定版)
```bash
npm i @zumer/snapdom
yarn add @zumer/snapdom
```
### NPM / Yarn (开发版)
想要提前体验新功能和修复:
```bash
npm i @zumer/snapdom@dev
yarn add @zumer/snapdom@dev
```
⚠️ `@dev` 标签通常包含在正式发布前的改进,但可能不够稳定。
### CDN (稳定版)
```html
<!-- 压缩的 构建 -->
<script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
<!-- ES 模块构建 -->
<script type="module">
import { snapdom } from "https://unpkg.com/@zumer/snapdom/dist/snapdom.mjs";
</script>
```
### CDN (开发版)
```html
<!-- 压缩的 UMD 构建(开发版) -->
<script src="https://unpkg.com/@zumer/snapdom@dev/dist/snapdom.js"></script>
<!-- ES 模块构建(开发版) -->
<script type="module">
import { snapdom } from "https://unpkg.com/@zumer/snapdom@dev/dist/snapdom.mjs";
</script>
```
## 构建产物
| 变体 | 文件 | 使用场景 |
|------|------|----------|
| **ESM**(可摇树) | `dist/snapdom.mjs` | 打包工具(Vite、webpack),`import` |
| **IIFE**(全局) | `dist/snapdom.js` | script 标签、传统 `require` |
**打包工具 (npm):**
```js
import { snapdom } from '@zumer/snapdom'; // → dist/snapdom.mjs
```
**script 标签 (CDN):**
```html
<script src="https://unpkg.com/@zumer/snapdom/dist/snapdom.js"></script>
<script> snapdom.toPng(document.body).then(img => document.body.appendChild(img)); </script>
```
**子路径导入**(仅需部分功能时可减小体积):
```js
import { preCache } from '@zumer/snapdom/preCache';
import { plugins } from '@zumer/snapdom/plugins';
```
## 基本用法
| 模式 | 适用场景 |
|------|----------|
| **可复用** `snapdom(el)` | 一次克隆 → 多次导出(PNG + JPG + 下载)。 |
| **快捷** `snapdom.toPng(el)` | 单次导出,代码更简洁。 |
### 可复用的捕获
一次捕获,多次导出(不会重新克隆):
```js
const el = document.querySelector('#target');
const result = await snapdom(el);
const img = await result.toPng();
document.body.appendChild(img);
await result.download({ format: 'jpg', filename: 'my-capture.jpg' });
```
### 一步式快捷方法
直接导出单一格式:
```js
const png = await snapdom.toPng(el);
const blob = await snapdom.toBlob(el);
document.body.appendChild(png);
```
## API
### `snapdom(el, options?)`
返回一个包含可复用导出方法的对象:
```js
{
url: string;
toRaw(): string;
toImg(): Promise<HTMLImageElement>; // 已废弃
toSvg(): Promise<HTMLImageElement>;
toCanvas(): Promise<HTMLCanvasElement>;
toBlob(options?): Promise<Blob>;
toPng(options?): Promise<HTMLImageElement>;
toJpg(options?): Promise<HTMLImageElement>;
toWebp(options?): Promise<HTMLImageElement>;
download(options?): Promise<void>;
}
```
### 快捷方法
| 方法 | 描述 |
| ------------------------------ | --------------------------------- |
| `snapdom.toImg(el, options?)` | 返回一个 SVG `HTMLImageElement`(已废弃) |
| `snapdom.toSvg(el, options?)` | 返回一个 SVG `HTMLImageElement` |
| `snapdom.toCanvas(el, options?)` | 返回一个 `Canvas` |
| `snapdom.toBlob(el, options?)` | 返回一个 SVG 或光栅 `Blob` |
| `snapdom.toPng(el, options?)` | 返回一个 PNG 图片 |
| `snapdom.toJpg(el, options?)` | 返回一个 JPG 图片 |
| `snapdom.toWebp(el, options?)` | 返回一个 WebP 图片 |
| `snapdom.download(el, options?)` | 触发下载 |
### 导出器专用选项
除了全局的捕获选项之外,部分导出器还支持一小组 **仅用于导出** 的选项。
#### `download()`
| Option | Type | Default | Description |
| ---------- | --------------------------------------------- | --------- | ----------- |
| `filename` | `string` | `snapdom` | 下载文件名。 |
| `format` | `"png" \| "jpeg" \| "jpg" \| "webp" \| "svg"` | `"png"` | 下载文件的输出格式。 |
**示例:**
```js
await result.download({
format: 'jpg',
quality: 0.92,
filename: 'my-capture'
});
```
#### `toBlob()`
| Option | Type | Default | Description |
| ------ | --------------------------------------------- | ------- | ------------ |
| `type` | `"svg" \| "png" \| "jpeg" \| "jpg" \| "webp"` | `"svg"` | 生成的 Blob 类型。 |
**示例:**
```js
const blob = await result.toBlob({ type: 'jpeg', quality: 0.92 });
```
## 选项
所有捕获方法都接受一个 `options` 对象:
| 选项 | 类型 | 默认值 | 描述 |
| ----------------- | -------- | -------- | ----------------------------------------------- |
| `debug` | boolean | `false` | 设为 `true` 时,将静默处理的错误输出到 `console.warn`,便于排查问题 |
| `fast` | boolean | `true` | 跳过小的空闲延迟以获得更快的结果 |
| `embedFonts` | boolean | `false` | 内嵌非图标字体(图标字体始终内嵌) |
| `localFonts` | array | `[]` | 本地字体 `{ family, src, weight?, style? }` |
| `iconFonts` | string\|RegExp\|Array | `[]` | 额外的图标字体匹配器 |
| `excludeFonts` | object | `{}` | 在嵌入时排除字体族/域名/子集 |
| `scale` | number | `1` | 输出缩放倍数 |
| `dpr` | number | `devicePixelRatio` | 设备像素比 |
| `width` | number | - | 输出宽度 |
| `height` | number | - | 输出高度 |
| `backgroundColor` | string | `"#fff"` | JPG/WebP 的备用颜色 |
| `quality` | number | `1` | JPG/WebP 的质量(0 到 1) |
| `useProxy` | string | `''` | CORS 备用代理基础 URL |
| `exclude` | string[] | - | 要排除的 CSS 选择器 |
| `excludeMode` | `"hide"`\|`"remove"` | `"hide"` | `exclude` 的应用方式 |
| `filter` | function | - | 自定义谓词函数 `(el) => boolean` |
| `filterMode` | `"hide"`\|`"remove"` | `"hide"` | `filter` 的应用方式 |
| `cache` | string | `"soft"` | `disabled` \| `soft` \| `auto` \| `full` |
| `placeholders` | boolean | `true` | 为图片/CORS iframe 显示占位符 |
| `fallbackURL` | string \| function | - | `<img>` 加载失败时的备用图片 |
| `outerTransforms` | boolean | `true` | 当为 `false` 时移除 `translate/rotate` 但保留 `scale/skew`,产生扁平、可复用的捕获 |
| `outerShadows` | boolean | `false` | 不为根元素的阴影/模糊/轮廓扩展边界框,并从克隆的根元素中移除这些视觉效果 |
| `safariWarmupAttempts` | number | `3` | 仅 Safari:预热的迭代次数(WebKit #219770)。若 3 次过慢可设为 `1` |
### debug
当 `debug: true` 时,SnapDOM 会将通常静默处理的错误输出到 `console.warn`(带 `[snapdom]` 前缀)。便于排查捕获问题(如 canvas 失败、blob 解析、样式剥离等),而无需在生产环境中产生冗余输出。
```js
await snapdom.toPng(el, { debug: true });
```
### `<img>` 加载失败时的备用图片
为失败的 `<img>` 加载提供默认图片。您可以传递一个固定 URL 或一个接收测量尺寸并返回 URL 的回调函数(便于生成动态占位符)。
```js
// 1) 固定 URL 备用
await snapdom.toSvg(element, {
fallbackURL: '/images/fallback.png'
});
// 2) 通过回调生成动态占位符
await snapdom.toSvg(element, {
fallbackURL: ({ width: 300, height: 150 }) =>
`https://placehold.co/${width}x${height}`
});
// 3) 使用代理(如果您的备用图片主机没有 CORS)
await snapdom.toSvg(element, {
fallbackURL: ({ width = 300, height = 150 }) =>
`https://dummyimage.com/${width}x${height}/cccccc/666.png&text=img`,
useProxy: 'https://proxy.corsfix.com/?'
});
```
注意:
- 如果备用图片也加载失败,snapDOM 会用保留宽度/高度的占位符块替换 `<img>`。
- 回调使用的宽度/高度从原始元素(dataset、style/attrs 等)中收集(如果可用)。
### 尺寸 (`scale`, `width`, `height`)
* 如果提供了 `scale`,它将**优先于** `width`/`height`。
* 如果只提供 `width`,高度按比例缩放(反之亦然)。
* 同时提供 `width` 和 `height` 会强制使用精确尺寸(可能会失真)。
### 跨域图片和字体 (`useProxy`)
默认情况下,snapDOM 尝试使用 `crossOrigin="anonymous"`(或同源时使用 `use-credentials`)。如果资源被 CORS 阻止,您可以将 `useProxy` 设置为转发实际 `src` 的前缀 URL:
```js
await snapdom.toPng(el, {
useProxy: 'https://proxy.corsfix.com/?' // 注意:可以使用任何 CORS 代理 'https://proxy.corsfix.com/?'
});
```
* 代理仅用作**备用**;同源和启用 CORS 的资源会跳过它。
### 字体
#### `embedFonts`
当为 `true` 时,snapDOM 会嵌入在捕获子树中检测到使用的**非图标** `@font-face` 规则。图标字体(Font Awesome、Material Icons 等)**始终**被嵌入。
#### `localFonts`
如果您自己提供字体或拥有 data URL,可以在此处声明它们以避免额外的 CSS 发现:
```js
await snapdom.toPng(el, {
embedFonts: true,
localFonts: [
{ family: 'Inter', src: '/fonts/Inter-Variable.woff2', weight: 400, style: 'normal' },
{ family: 'Inter', src: '/fonts/Inter-Italic.woff2', style: 'italic' }
]
});
```
#### `iconFonts`
添加自定义图标字体族(名称或正则表达式匹配器)。对私有图标集很有用:
```js
await snapdom.toPng(el, {
iconFonts: ['MyIcons', /^(Remix|Feather) Icons?$/i]
});
```
#### `excludeFonts`
跳过特定的非图标字体以加快捕获速度或避免不必要的下载。
```js
await snapdom.toPng(el, {
embedFonts: true,
excludeFonts: {
families: ['Noto Serif', 'SomeHeavyFont'], // 按字体族名称跳过
domains: ['fonts.gstatic.com', 'cdn.example'], // 按源主机跳过
subsets: ['cyrillic-ext'] // 按 unicode-range 子集标签跳过
}
});
```
*注意*
- `excludeFonts` 仅适用于**非图标**字体。图标字体始终被嵌入。
- `families` 的匹配不区分大小写。主机通过子字符串与解析后的 URL 进行匹配。
#### 节点过滤:`exclude` vs `filter`
* `exclude`: 通过**选择器**移除。
* `excludeMode`: `hide` 对排除的节点应用 `visibility:hidden` CSS 规则,布局保持原样。`remove` 完全不克隆排除的节点。
* `filter`: 每个元素的高级谓词函数(返回 `false` 以丢弃)。
* `filterMode`: `hide` 对过滤的节点应用 `visibility:hidden` CSS 规则,布局保持原样。`remove` 完全不克隆过滤的节点。
**示例:过滤掉 `display:none` 的元素:**
```js
/**
* 示例过滤器:跳过 display:none 的元素
* @param {Element} el
* @returns {boolean} true = 保留, false = 排除
*/
function filterHidden(el) {
const cs = window.getComputedStyle(el);
if (cs.display === 'none') return false;
return true;
}
await snapdom.toPng(document.body, { filter: filterHidden });
```
**使用 `exclude` 的示例:** 通过选择器移除横幅或工具提示
```js
await snapdom.toPng(el, {
exclude: ['.cookie-banner', '.tooltip', '[data-test="debug"]']
});
```
### outerTransforms
捕获旋转或平移的元素时,如果您想消除这些外部变换,可以使用 **outerTransforms: false** 选项。这样,输出是**扁平、直立且可直接**在其他地方使用的。
- **`outerTransforms: true (默认)`**
**保留原始的 `transforms` 和 `rotate`**。
### outerShadows
- **`outerShadows: false (默认)`**
防止为根元素的阴影、模糊或轮廓扩展边界框,并从克隆的根元素中移除 `box-shadow`、`text-shadow`、`filter: blur()/drop-shadow()` 和 `outline`。
> 💡 **提示:** 同时使用两者(`outerTransforms: false` + `outerShadows: false`)会产生严格、最小化的边界框,没有视觉溢出。
**示例**
```js
// outerTransforms 和移除阴影溢出
await snapdom.toSvg(el, { outerTransforms: true, outerShadows: true });
```
## 缓存控制
SnapDOM 为图片、背景、资源、样式和字体维护内部缓存。
您可以使用 `cache` 选项控制它们在捕获之间的清除方式:
| 模式 | 描述 |
| ----------- | --------------------------------------------------------------------------- |
| `"disabled"`| 无缓存 |
| `"soft"` | 清除会话缓存(`styleMap`、`nodeMap`、`styleCache`)_(默认)_ |
| `"auto"` | 最小清理:仅清除临时映射 |
| `"full"` | 保留所有缓存(不清除任何内容,最大性能) |
**示例:**
```js
// 使用最小但快速的缓存
await snapdom.toPng(el, { cache: 'auto' });
// 在捕获之间将所有内容保留在内存中
await snapdom.toPng(el, { cache: 'full' });
// 强制在每次捕获时完全清理
await snapdom.toPng(el, { cache: 'disabled' });
```
## `preCache()` – 可选辅助函数
预加载外部资源以避免首次捕获时的停顿(对大型/复杂树很有帮助)。
```js
import { preCache } from '@zumer/snapdom';
await preCache({
root: document.body,
embedFonts: true,
localFonts: [{ family: 'Inter', src: '/fonts/Inter.woff2', weight: 400 }],
useProxy: 'https://proxy.corsfix.com/?'
});
```
## 插件(测试版)
SnapDOM 包含一个轻量级**插件系统**,允许您在捕获和导出过程的任何阶段扩展或覆盖行为——无需修改核心库。
插件是一个简单的对象,具有唯一的 `name` 和一个或多个生命周期**钩子**。
钩子可以是同步的或 `async`,它们接收一个共享的 **`context`** 对象。
### 注册插件
**全局注册**(适用于所有捕获):
```js
import { snapdom } from '@zumer/snapdom';
// 您可以注册实例、工厂函数或 [工厂函数, 选项]
snapdom.plugins(
myPluginInstance,
[myPluginFactory, { optionA: true }],
{ plugin: anotherFactory, options: { level: 2 } }
);
```
**单次捕获注册**(仅适用于该特定调用):
```js
const out = await snapdom(element, {
plugins: [
[overlayFilterPlugin, { color: 'rgba(0,0,0,0.25)' }],
[myFullPlugin, { providePdf: true }]
]
});
```
* **执行顺序 = 注册顺序**(先注册,先执行)。
* **单次捕获插件**在全局插件**之前**运行。
* 重复项通过 `name` 自动跳过;具有相同 `name` 的单次捕获插件会覆盖其全局版本。
### 插件生命周期钩子
钩子按捕获顺序执行(见[捕获流程](#捕获流程)):
| 钩子 | 阶段 | 目的 |
|------|------|------|
| `beforeSnap` | 开始 | 任何工作之前调整选项。 |
| `beforeClone` | 克隆前 | DOM 克隆之前(谨慎修改实时 DOM)。 |
| `afterClone` | 克隆后 | 安全修改克隆树(如注入叠加层)。 |
| `beforeRender` | 序列化前 | SVG 转 data URL 之前。 |
| `afterRender` | 序列化后 | 检查 `context.svgString` / `context.dataURL`。 |
| `beforeExport` | 每次导出前 | 每次 `toPng`、`toSvg` 等之前。 |
| `afterExport` | 每次导出后 | 转换返回结果。 |
| `afterSnap` | 一次 | 第一次导出后;清理。 |
| `defineExports` | 设置 | 添加自定义导出器(如 `toPdf`)。 |
> `afterExport` 的返回值会链接到下一个插件(转换管道)。
### 上下文对象
每个钩子都接收一个包含规范化捕获状态的 `context` 对象:
* **输入和选项:**
`element`, `debug`, `fast`, `scale`, `dpr`, `width`, `height`, `backgroundColor`, `quality`, `useProxy`, `cache`, `outerTransforms`, `outerShadows`, `safariWarmupAttempts`, `embedFonts`, `localFonts`, `iconFonts`, `excludeFonts`, `exclude`, `excludeMode`, `filter`, `filterMode`, `fallbackURL`。
* **中间值(取决于阶段):**
`clone`, `classCSS`, `styleCache`, `fontsCSS`, `baseCSS`, `svgString`, `dataURL`。
* **导出期间:**
`context.export = { type, options, url }`
其中 `type` 是导出器名称(`"png"`、`"jpeg"`、`"svg"`、`"blob"` 等),`url` 是序列化的 SVG 基础。
> 您可以安全地修改 `context`(例如,覆盖 `backgroundColor` 或 `quality`)——但要在早期(`beforeSnap`)进行以获得全局效果,或在 `beforeExport` 中进行以获得单次导出更改。
## 通过插件自定义导出
插件可以使用 `defineExports(context)` 添加新的导出。
对于您返回的每个导出键(例如,`"pdf"`),SnapDOM 会在捕获结果上自动公开一个名为 **`toPdf()`** 的辅助方法。
**注册插件(全局或单次捕获):**
```js
import { snapdom } from '@zumer/snapdom';
// 全局
snapdom.plugins(pdfExportPlugin());
// 或单次捕获
const out = await snapdom(element, { plugins: [pdfExportPlugin()] });
```
**调用自定义导出:**
```js
const out = await snapdom(document.querySelector('#report'));
// 因为插件返回 { pdf: async (ctx, opts) => ... }
const pdfBlob = await out.toPdf({
// 导出器特定选项(width, height, quality, filename 等)
});
```
### 示例:叠加滤镜插件
仅在捕获的克隆中添加半透明叠加层或颜色滤镜(不在您的实时 DOM 中)。
在导出前用于高亮显示或变暗部分很有用。
```js
/**
* SnapDOM 的超简单叠加滤镜(仅 HTML)。
* 在克隆的根元素上插入全尺寸 <div> 叠加层。
*
* @param {{ color?: string; blur?: number }} [options]
* color: 叠加颜色(rgba/hex/hsl)。默认: 'rgba(0,0,0,0.25)'
* blur: 可选的模糊像素值(默认: 0)
*/
export function overlayFilterPlugin(options = {}) {
const color = options.color ?? 'rgba(0,0,0,0.25)';
const blur = Math.max(0, options.blur ?? 0);
return {
name: 'overlay-filter',
/**
* 在克隆的 HTML 根元素上添加全覆盖叠加层。
* @param {any} context
*/
async afterClone(context) {
const root = context.clone;
if (!(root instanceof HTMLElement)) return; // 仅 HTML
// 确保包含块,以便绝对定位的叠加层锚定到根元素
if (getComputedStyle(root).position === 'static') {
root.style.position = 'relative';
}
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.left = '0';
overlay.style.top = '0';
overlay.style.right = '0';
overlay.style.bottom = '0';
overlay.style.background = color;
overlay.style.pointerEvents = 'none';
if (blur) overlay.style.filter = `blur(${blur}px)`;
root.appendChild(overlay);
}
};
}
```
**用法:**
```js
import { snapdom } from '@zumer/snapdom';
// 全局注册
snapdom.plugins([overlayFilterPlugin, { color: 'rgba(0,0,0,0.3)', blur: 2 }]);
// 单次捕获
const out = await snapdom(document.querySelector('#card'), {
plugins: [[overlayFilterPlugin, { color: 'rgba(255,200,0,0.15)' }]]
});
const png = await out.toPng();
document.body.appendChild(png);
```
> 叠加层仅注入到**克隆的树中**,永远不会注入到您的实时 DOM 中,确保完美保真度和零闪烁。
### 完整插件模板
使用此模板作为自定义逻辑或导出器的起点。
```js
export function myPlugin(options = {}) {
return {
/** 用于去重/覆盖的唯一名称 */
name: 'my-plugin',
/** 在任何克隆/样式工作之前的早期调整。 */
async beforeSnap(context) {},
/** 子树克隆之前(如果触及实时 DOM,请谨慎使用)。 */
async beforeClone(context) {},
/** 子树克隆之后(可以安全地修改克隆的树)。 */
async afterClone(context) {},
/** 序列化之前(SVG/dataURL)。 */
async beforeRender(context) {},
/** 序列化之后;如果需要,检查 context.svgString/context.dataURL。 */
async afterRender(context) {},
/** 每次导出调用之前(toPng/toSvg/toBlob/...)。 */
async beforeExport(context) {},
/**
* 每次导出调用之后。
* 如果您返回一个值,它将成为下一个插件的结果(链式)。
*/
async afterExport(context, result) { return result; },
/**
* 定义自定义导出器(自动添加为辅助方法,如 out.toPdf())。
* 返回映射 { [key: string]: (ctx:any, opts:any) => Promise<any> }。
*/
async defineExports(context) { return {}; },
/** 在第一次导出完成后运行一次(清理)。 */
async afterSnap(context) {}
};
}
```
**快速回顾:**
* 插件可以修改捕获行为(`beforeSnap`、`afterClone` 等)。
* 您可以安全地将视觉效果或转换注入到克隆的树中。
* 在 `defineExports()` 中定义的新导出器会自动成为辅助方法,如 `out.toPdf()`。
* 所有钩子都可以是异步的,按顺序运行,并共享相同的 `context`。
## 限制
* 外部图片应该是 CORS 可访问的(使用 `useProxy` 选项处理 CORS 拒绝)
* 在 Safari 上使用 WebP 格式时,将回退到 PNG 渲染。
* `@font-face` CSS 规则得到良好支持,但如果需要使用 JS `FontFace()`,请参阅此解决方案 [`#43`](https://github.com/zumerlab/snapdom/issues/43)
* **Safari**:启用 `embedFonts` 或包含背景/蒙版图片的捕获会较慢,因 [WebKit #219770](https://bugs.webkit.org/show_bug.cgi?id=219770)(字体解码时机)。SnapDOM 通过预捕获和 `drawImage` 预热管道;可通过 `safariWarmupAttempts` 调整(默认 3)。
* **自定义滚动条样式**(`::-webkit-scrollbar`):仅在元素*未滚动*时生效。若已滚动,将捕获视口内容且不显示滚动条。
## ⚡ 性能基准测试(Chromium)
**设置说明。** 在 Chromium 上使用 Vitest 基准测试,仓库测试。硬件可能影响结果。
数值为**平均捕获时间(毫秒)** → 越低越好。
### 简单元素
| 场景 | SnapDOM 当前版本 | SnapDOM v1.9.9 | html2canvas | html-to-image |
| ------------------------ | --------------- | -------------- | ----------- | ------------- |
| 小尺寸 (200×100) | **0.5 ms** | 0.8 ms | 67.7 ms | 3.1 ms |
| 模态框 (400×300) | **0.5 ms** | 0.8 ms | 75.5 ms | 3.6 ms |
| 页面视图 (1200×800) | **0.5 ms** | 0.8 ms | 114.2 ms | 3.3 ms |
| 大滚动 (2000×1500) | **0.5 ms** | 0.8 ms | 186.3 ms | 3.2 ms |
| 超大尺寸 (4000×2000) | **0.5 ms** | 0.9 ms | 425.9 ms | 3.3 ms |
### 复杂元素
| 场景 | SnapDOM 当前版本 | SnapDOM v1.9.9 | html2canvas | html-to-image |
| ------------------------ | --------------- | -------------- | ----------- | ------------- |
| 小尺寸 (200×100) | **1.6 ms** | 3.3 ms | 68.0 ms | 14.3 ms |
| 模态框 (400×300) | **2.9 ms** | 6.8 ms | 87.5 ms | 34.8 ms |
| 页面视图 (1200×800) | **17.5 ms** | 50.2 ms | 178.0 ms | 429.0 ms |
| 大滚动 (2000×1500) | **54.0 ms** | 201.8 ms | 735.2 ms | 984.2 ms |
| 超大尺寸 (4000×2000) | **171.4 ms** | 453.7 ms | 1,800.4 ms | 2,611.9 ms |
### 运行基准测试
```sh
git clone https://github.com/zumerlab/snapdom.git
cd snapdom
npm install
npm run test:benchmark
```
## 路线图
SnapDOM 未来版本的计划改进:
* [X] **实现插件系统**
SnapDOM 将支持外部插件以扩展或覆盖内部行为(例如自定义节点转换器、导出器或过滤器)。
* [ ] **重构为模块化架构**
内部逻辑将被拆分为更小、更专注的模块,以提高可维护性和代码复用。
* [X] **将内部逻辑与全局选项解耦**
函数将重新设计以避免直接依赖 `options`。集中式捕获上下文将提高清晰度、自主性和可测试性。参见 [`next` 分支](https://github.com/zumerlab/snapdom/tree/main)
* [X] **暴露缓存控制**
用户将能够手动清除图片和字体缓存或配置自己的缓存策略。
* [X] **自动字体预加载**
所需的字体将在捕获前自动检测和预加载,减少手动调用 `preCache()` 的需要。
* [X] **文档化插件开发**
将提供完整的指南,用于创建和注册自定义 SnapDOM 插件。
* [ ] **使导出工具支持 tree-shaking**
`toPng`、`toJpg`、`toBlob` 等导出函数将被重构为独立模块,以支持 tree shaking 和最小化构建。
有想法或功能请求?
欢迎在 [GitHub Discussions](https://github.com/zumerlab/snapdom/discussions) 中分享建议或反馈。
## 开发
**源码结构:**
- `src/api/` – 公共 API(`snapdom`、`preCache`)
- `src/core/` – 捕获流程、克隆、准备、插件
- `src/modules/` – 图片、字体、伪元素、背景、SVG
- `src/exporters/` – toPng、toSvg、toBlob 等
- `dist/` – 构建产物(`snapdom.js`、`snapdom.mjs`、`preCache.mjs`、`plugins.mjs`)
**构建:**
```sh
git clone https://github.com/zumerlab/snapdom.git
cd snapdom
git checkout dev
npm install
npm run compile
```
**测试:**
```sh
npx playwright install # 浏览器测试所需
npm test
npm run test:benchmark
```
详细指南请参阅 [CONTRIBUTING](https://github.com/zumerlab/snapdom/blob/main/CONTRIBUTING.md)。
## 贡献者 🙌
<!-- CONTRIBUTORS:START -->
<p>
<a href="https://github.com/tinchox5" title="tinchox5"><img src="https://avatars.githubusercontent.com/u/11557901?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="tinchox5"/></a>
<a href="https://github.com/Jarvis2018" title="Jarvis2018"><img src="https://avatars.githubusercontent.com/u/36788851?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="Jarvis2018"/></a>
<a href="https://github.com/tarwin" title="tarwin"><img src="https://avatars.githubusercontent.com/u/646149?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="tarwin"/></a>
<a href="https://github.com/Amyuan23" title="Amyuan23"><img src="https://avatars.githubusercontent.com/u/25892910?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="Amyuan23"/></a>
<a href="https://github.com/airamhr9" title="airamhr9"><img src="https://avatars.githubusercontent.com/u/57371081?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="airamhr9"/></a>
<a href="https://github.com/FlavioLimaMindera" title="FlavioLimaMindera"><img src="https://avatars.githubusercontent.com/u/96424442?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="FlavioLimaMindera"/></a>
<a href="https://github.com/jswhisperer" title="jswhisperer"><img src="https://avatars.githubusercontent.com/u/1177690?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="jswhisperer"/></a>
<a href="https://github.com/K1ender" title="K1ender"><img src="https://avatars.githubusercontent.com/u/146767945?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="K1ender"/></a>
<a href="https://github.com/kohaiy" title="kohaiy"><img src="https://avatars.githubusercontent.com/u/15622127?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="kohaiy"/></a>
<a href="https://github.com/17biubiu" title="17biubiu"><img src="https://avatars.githubusercontent.com/u/13295895?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="17biubiu"/></a>
<a href="https://github.com/av01d" title="av01d"><img src="https://avatars.githubusercontent.com/u/6247646?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="av01d"/></a>
<a href="https://github.com/CHOYSEN" title="CHOYSEN"><img src="https://avatars.githubusercontent.com/u/25995358?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="CHOYSEN"/></a>
<a href="https://github.com/pedrocateexte" title="pedrocateexte"><img src="https://avatars.githubusercontent.com/u/207524750?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="pedrocateexte"/></a>
<a href="https://github.com/domialex" title="domialex"><img src="https://avatars.githubusercontent.com/u/4694217?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="domialex"/></a>
<a href="https://github.com/elliots" title="elliots"><img src="https://avatars.githubusercontent.com/u/622455?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="elliots"/></a>
<a href="https://github.com/stypr" title="stypr"><img src="https://avatars.githubusercontent.com/u/6625978?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="stypr"/></a>
<a href="https://github.com/mon-jai" title="mon-jai"><img src="https://avatars.githubusercontent.com/u/91261297?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="mon-jai"/></a>
<a href="https://github.com/sharuzzaman" title="sharuzzaman"><img src="https://avatars.githubusercontent.com/u/7421941?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="sharuzzaman"/></a>
<a href="https://github.com/simon1uo" title="simon1uo"><img src="https://avatars.githubusercontent.com/u/60037549?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="simon1uo"/></a>
<a href="https://github.com/titoBouzout" title="titoBouzout"><img src="https://avatars.githubusercontent.com/u/64156?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="titoBouzout"/></a>
<a href="https://github.com/ZiuChen" title="ZiuChen"><img src="https://avatars.githubusercontent.com/u/64892985?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="ZiuChen"/></a>
<a href="https://github.com/harshasiddartha" title="harshasiddartha"><img src="https://avatars.githubusercontent.com/u/147021873?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="harshasiddartha"/></a>
<a href="https://github.com/karasHou" title="karasHou"><img src="https://avatars.githubusercontent.com/u/27048083?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="karasHou"/></a>
<a href="https://github.com/jhbae200" title="jhbae200"><img src="https://avatars.githubusercontent.com/u/20170610?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="jhbae200"/></a>
<a href="https://github.com/xiaobai-web715" title="xiaobai-web715"><img src="https://avatars.githubusercontent.com/u/81091224?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="xiaobai-web715"/></a>
<a href="https://github.com/miusuncle" title="miusuncle"><img src="https://avatars.githubusercontent.com/u/7549857?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="miusuncle"/></a>
<a href="https://github.com/rbbydotdev" title="rbbydotdev"><img src="https://avatars.githubusercontent.com/u/101137670?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="rbbydotdev"/></a>
<a href="https://github.com/zhanghaotian2018" title="zhanghaotian2018"><img src="https://avatars.githubusercontent.com/u/169218899?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="zhanghaotian2018"/></a>
</p>
<!-- CONTRIBUTORS:END -->
## 💖 赞助者
特别感谢 [@megaphonecolin](https://github.com/megaphonecolin)、[@sdraper69](https://github.com/sdraper69)、[@reynaldichernando](https://github.com/reynaldichernando) 和 [@gamma-app](https://github.com/gamma-app),感谢他们对本项目的支持!
如果您也想支持这个项目,您可以[成为赞助者](https://github.com/sponsors/tinchox5)。
## Star 历史
[](https://www.star-history.com/#zumerlab/snapdom&Date)
## 许可证
MIT © Zumerlab
================================================
FILE: __tests__/api.preCache.more.test.js
================================================
// __tests__/api.preCache.more.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
vi.mock('../src/utils', async () => {
const actual = await vi.importActual('../src/utils')
return {
...actual,
fetchImage: vi.fn(async () => 'data:image/png;base64,iVBORw0KGgo='),
precacheCommonTags: vi.fn(),
isSafari: vi.fn(() => false), // queda como vi.fn() invocable
}
})
// preCache importa inlineBackgroundImages desde ../modules/background.js
// (si en tu código real lo seguís importando desde ../utils, cambiá esta ruta acá)
vi.mock('../src/modules/background.js', () => ({
inlineBackgroundImages: vi.fn(async () => {}),
}))
vi.mock('../src/modules/fonts.js', () => ({
embedCustomFonts: vi.fn(async () => ''),
collectUsedFontVariants: vi.fn(() => new Set(['Mansalva__700__italic__100'])),
collectUsedCodepoints: vi.fn(() => new Set([65])),
ensureFontsReady: vi.fn(async () => {}),
}))
// ⬇️ recién ahora importamos SUT + símbolos mocked
import { preCache } from '../src/api/preCache.js'
import { cache } from '../src/core/cache.js'
import * as utils from '../src/utils'
import { inlineBackgroundImages } from '../src/modules/background.js'
import {
embedCustomFonts,
collectUsedFontVariants,
collectUsedCodepoints,
ensureFontsReady,
} from '../src/modules/fonts.js'
describe('preCache – líneas difíciles', () => {
beforeEach(() => {
vi.clearAllMocks()
utils.isSafari.mockReset?.()
utils.isSafari.mockReturnValue(false)
if (!cache.session) cache.session = {}
cache.session.styleCache = new WeakMap()
})
it('pasa cache.session.styleCache a inlineBackgroundImages (líneas 49–50)', async () => {
const root = document.createElement('div')
const ref = cache.session.styleCache
await expect(preCache(root, { embedFonts: false })).resolves.toBeUndefined()
expect(inlineBackgroundImages).toHaveBeenCalledTimes(1)
const args = inlineBackgroundImages.mock.calls[0] // [source, mirror, styleCache, options]
expect(args[0]).toBe(root)
expect(args[2]).toStrictEqual(ref) // MISMA referencia => cubre 49–50
expect(args[3]).toMatchObject({ useProxy: '' })
})
it('si inlineBackgroundImages falla, preCache resuelve igual (catch 56–65)', async () => {
inlineBackgroundImages.mockRejectedValueOnce(new Error('boom'))
const el = document.createElement('section')
await expect(preCache(el, { embedFonts: false })).resolves.toBeUndefined()
})
it('Safari warmup + embed de fuentes con params correctos (84–91)', async () => {
utils.isSafari.mockReturnValue(true)
const root = document.createElement('div')
const excludeFonts = { subsets: ['latin'], domains: ['bad.example'] }
const localFonts = [{ family: 'Foo', src: 'data:font/woff2;base64,AA==' }]
await preCache(root, {
embedFonts: true,
useProxy: '/proxy/',
excludeFonts,
localFonts,
})
expect(ensureFontsReady).toHaveBeenCalledTimes(1)
const [families, reps] = ensureFontsReady.mock.calls[0]
expect(families instanceof Set).toBe(true)
expect(Array.from(families)).toContain('Mansalva')
expect(reps).toBe(3)
expect(embedCustomFonts).toHaveBeenCalledTimes(1)
const call = embedCustomFonts.mock.calls[0][0]
expect(call.required).toEqual(collectUsedFontVariants())
expect(call.usedCodepoints).toEqual(collectUsedCodepoints())
expect(call.exclude).toEqual(excludeFonts)
expect(call.localFonts).toEqual(localFonts)
expect(call.useProxy).toBe('/proxy/')
})
it('crea styleCache si no existe y lo inyecta a inlineBackgroundImages (49–50)', async () => {
// Aseguramos estado inicial sin styleCache
cache.session = cache.session || {}
delete cache.session.styleCache
const root = document.createElement('main')
await expect(preCache(root, { embedFonts: false })).resolves.toBeUndefined()
// Se llamó una vez y con el WeakMap recién creado
expect(inlineBackgroundImages).toHaveBeenCalledTimes(1)
const args = inlineBackgroundImages.mock.calls[0] // [source, mirror, styleCache, options]
expect(args[0]).toBe(root)
// preCache debió crear y colgar el WeakMap en cache.session.styleCache
expect(cache.session.styleCache).toBeInstanceOf(WeakMap)
expect(args[2]).toBe(cache.session.styleCache) // MISMA referencia => cubre 49–50
})
it('si inlineBackgroundImages lanza (throw sync), preCache resuelve igual (56–65)', async () => {
// Lanzar sincrónico para entrar al try/catch de preCache
inlineBackgroundImages.mockImplementationOnce(() => { throw new Error('sync-boom') })
const el = document.createElement('section')
await expect(preCache(el, { embedFonts: false })).resolves.toBeUndefined()
// (Opcional) el resto del flujo no debe romperse
expect(inlineBackgroundImages).toHaveBeenCalledTimes(1)
})
})
================================================
FILE: __tests__/api.preCache.test.js
================================================
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { preCache } from '../src/api/preCache.js'
import { cache } from '../src/core/cache.js'
import { safeEncodeURI } from '../src/utils/helpers.js' // ajustá el path si difiere
beforeEach(() => {
vi.restoreAllMocks()
cache.image?.clear?.()
cache.background?.clear?.()
document.body.innerHTML = ''
})
describe('preCache – extra coverage', () => {
it('prefetches SVG background via proxy fallback and dedupes repeated URL', async () => {
const PROXY = 'https://proxy.example.com/?u='
const DIRECT = 'https://cdn.example.com/icon.svg'
const svg = '<svg xmlns="http://www.w3.org/2000/svg"><rect width="1" height="1"/></svg>'
globalThis.fetch = vi.fn((url) => {
const u = String(url)
if (u.startsWith(PROXY)) {
return Promise.resolve({
ok: true,
text: () => Promise.resolve(svg),
blob: () => Promise.resolve(new Blob([svg], { type: 'image/svg+xml' })),
})
}
// En el contrato nuevo, no se debería llegar acá si useProxy está seteado
return Promise.reject(new Error('network fail'))
})
const root = document.createElement('div')
const a = document.createElement('div')
const b = document.createElement('div')
a.style.backgroundImage = `url(${DIRECT})`
b.style.backgroundImage = `url(${DIRECT})`
root.appendChild(a)
root.appendChild(b)
document.body.appendChild(root)
await preCache(root, { useProxy: PROXY })
const calls = vi.mocked(globalThis.fetch).mock.calls.map(([u]) => String(u))
const proxyCalls = calls.filter(u => u.startsWith(PROXY))
const directCalls = calls.filter(u => u === DIRECT)
// (1) EXACTAMENTE una llamada proxied (in-flight + cache dedupe)
expect(proxyCalls.length).toBe(1)
// (2) Con proxy activo, NO hay intentos directos en el nuevo snapFetch
expect(directCalls.length).toBe(0)
// (3) Dedupe en cache.background: una sola entrada para ese URL
const key = safeEncodeURI(DIRECT)
expect(cache.background.has(key)).toBe(true)
expect([...cache.background.keys()].filter(k => k === key).length).toBe(1)
document.body.removeChild(root)
})
it('handles mixed background layers (gradient + url) and only processes the URL layer', async () => {
// No contamos fetch acá porque raster usa Image() y puede ser 0.
globalThis.fetch = vi.fn() // por si algo intenta fetch (no debería)
const URL = 'https://assets.test/a.svg' // usamos SVG para que sí pase por fetch en tu impl
// Si querés testear raster, cambiá asserts a cache.image; con SVG comprobamos background.
const svg = '<svg xmlns="http://www.w3.org/2000/svg"></svg>'
vi.mocked(globalThis.fetch).mockResolvedValue({
ok: true,
text: () => Promise.resolve(svg),
blob: () => Promise.resolve(new Blob([svg], { type: 'image/svg+xml' })),
})
const el = document.createElement('div')
el.style.backgroundImage = `linear-gradient(90deg, #000, #fff), url(${URL})`
document.body.appendChild(el)
await preCache(el)
// Verificamos que SOLO la capa url(...) fue procesada y quedó cacheada
const key = safeEncodeURI(URL)
expect(cache.background.has(key)).toBe(true)
// No exigimos conteo de fetch: puede ser 0 si fuese raster.
// Si mantenés SVG como arriba, opcionalmente:
// expect(globalThis.fetch).toHaveBeenCalledTimes(1);
document.body.removeChild(el)
})
it('walks the subtree and preloads child backgrounds', async () => {
const svg = '<svg xmlns="http://www.w3.org/2000/svg"></svg>'
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
text: () => Promise.resolve(svg),
blob: () => Promise.resolve(new Blob([svg], { type: 'image/svg+xml' })),
})
const root = document.createElement('div')
const child = document.createElement('span')
const CHILD_URL = 'https://x.test/nested.svg'
child.style.backgroundImage = `url(${CHILD_URL})`
root.appendChild(child)
document.body.appendChild(root)
await preCache(root)
// Comprobamos que el hijo fue visto y cacheado
const key = safeEncodeURI(CHILD_URL)
expect(cache.background.has(key)).toBe(true)
document.body.removeChild(root)
})
})
================================================
FILE: __tests__/api.snapdom.more.test.js
================================================
// __tests__/api.snapdom.more.test.js – snapdom.js extra coverage
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { snapdom } from '../src/index.js'
vi.mock('../src/utils/browser', { spy: true })
import * as browser from '../src/utils/browser'
beforeEach(() => {
document.body.innerHTML = ''
vi.restoreAllMocks()
vi.mocked(browser.isSafari).mockReturnValue(false)
})
afterEach(() => {
document.body.innerHTML = ''
})
describe('snapdom – error handling', () => {
it('throws when element is null', async () => {
await expect(snapdom(null)).rejects.toThrow(/cannot be null/)
})
it('throws when element is undefined', async () => {
await expect(snapdom(undefined)).rejects.toThrow(/cannot be null/)
})
})
describe('snapdom – result.to()', () => {
it('throws for unknown export type', async () => {
const el = document.createElement('div')
el.textContent = 'x'
document.body.appendChild(el)
const result = await snapdom(el)
await expect(result.to('unknownType')).rejects.toThrow(/Unknown export type/)
})
})
describe('snapdom – result helpers', () => {
it('result has all expected export methods', async () => {
const el = document.createElement('div')
el.style.width = '50px'
el.style.height = '50px'
el.textContent = 'x'
document.body.appendChild(el)
const result = await snapdom(el)
expect(typeof result.toPng).toBe('function')
expect(typeof result.toSvg).toBe('function')
expect(typeof result.toCanvas).toBe('function')
expect(typeof result.download).toBe('function')
})
})
================================================
FILE: __tests__/api.snapdom.test.js
================================================
import { describe, it, expect, vi } from 'vitest'
import { snapdom } from '../src/api/snapdom.js'
describe('snapdom API (direct)', () => {
it('throws on null element', async () => {
await expect(snapdom(null)).rejects.toThrow()
})
it('snapdom returns export methods', async () => {
const el = document.createElement('div')
el.style.width = '100px'
el.style.height = '50px'
document.body.appendChild(el)
const result = await snapdom(el)
expect(result).toHaveProperty('toRaw')
expect(result).toHaveProperty('toImg')
expect(result).toHaveProperty('download')
document.body.removeChild(el)
})
it('snapdom.toRaw, toImg, toCanvas, toBlob, toPng, toJpg, toWebp, download', async () => {
const el = document.createElement('div')
el.style.width = '100px'
el.style.height = '50px'
document.body.appendChild(el)
await snapdom.toRaw(el)
await snapdom.toImg(el)
await snapdom.toCanvas(el)
await snapdom.toBlob(el)
await snapdom.toPng(el)
await snapdom.toJpg(el)
await snapdom.toWebp(el)
await snapdom.download(el, { format: 'png', filename: 'test' })
document.body.removeChild(el)
})
it('cubre rama Safari en toImg', async () => {
vi.resetModules()
vi.mock('../utils', async () => {
const actual = await vi.importActual('../utils')
return { ...actual, isSafari: true }
})
const { snapdom } = await import('../src/api/snapdom.js')
const el = document.createElement('div')
el.style.width = '10px'
el.style.height = '10px'
document.body.appendChild(el)
// Forzar un SVG dataURL simple
const img = new Image()
img.width = 10
img.height = 10
img.decode = () => Promise.resolve()
globalThis.Image = function() { return img }
const res = await snapdom(el)
await res.toImg()
document.body.removeChild(el)
vi.resetModules()
})
it('cubre rama de download SVG', async () => {
const el = document.createElement('div')
el.style.width = '10px'
el.style.height = '10px'
document.body.appendChild(el)
// Mock a.click y URL.createObjectURL
const a = document.createElement('a')
document.body.appendChild(a)
const origCreate = URL.createObjectURL
URL.createObjectURL = () => 'blob:url'
const origClick = a.click
a.click = () => {}
HTMLAnchorElement.prototype.click = () => {}
const { snapdom } = await import('../src/api/snapdom.js')
await snapdom.download(el, { format: 'svg', filename: 'testsvg' })
URL.createObjectURL = origCreate
a.click = origClick
document.body.removeChild(a)
document.body.removeChild(el)
})
it('snapdom.toBlob supports type options ', async () => {
const el = document.createElement('div')
el.style.width = '50px'
el.style.height = '30px'
document.body.appendChild(el)
const result = await snapdom(el)
const pngBlob = await result.toBlob({ type: 'png' })
expect(pngBlob).toBeInstanceOf(Blob)
expect(pngBlob.type).toBe('image/png')
const jpgBlob = await result.toBlob({ type: 'jpeg', quality: 0.8 })
expect(jpgBlob).toBeInstanceOf(Blob)
expect(jpgBlob.type).toBe('image/jpeg')
const webpBlob = await result.toBlob({ type: 'webp', quality: 0.9 })
expect(webpBlob).toBeInstanceOf(Blob)
expect(webpBlob.type).toBe('image/webp')
// default fallback
const svgBlob = await result.toBlob()
expect(svgBlob).toBeInstanceOf(Blob)
expect(svgBlob.type).toBe('image/svg+xml')
document.body.removeChild(el)
})
it('toPng, toJpg, toWebp return HTMLImageElement with URLs', async () => {
const el = document.createElement('div')
el.style.width = '60px'
el.style.height = '40px'
document.body.appendChild(el)
const snap = await snapdom(el)
const pngImg = await snap.toPng()
expect(pngImg).toBeInstanceOf(HTMLImageElement)
expect(typeof pngImg.src).toBe('string')
expect(pngImg.src.startsWith('data:image/png')).toBe(true)
const jpgImg = await snap.toJpg()
expect(jpgImg).toBeInstanceOf(HTMLImageElement)
expect(typeof jpgImg.src).toBe('string')
expect(jpgImg.src.startsWith('data:image/jpeg')).toBe(true)
const webpImg = await snap.toWebp()
expect(webpImg).toBeInstanceOf(HTMLImageElement)
expect(typeof webpImg.src).toBe('string')
expect(webpImg.src.startsWith('data:image/webp')).toBe(true)
document.body.removeChild(el)
})
it('snapdom should support exclude option to filter out elements by CSS selectors', async () => {
const el = document.createElement('div')
el.innerHTML = `
<h1>Title</h1>
<div class="exclude-me">Should be excluded</div>
<div data-private="true">Private data</div>
<p>This should remain</p>
`
document.body.appendChild(el)
const result = await snapdom(el, { exclude: ['.exclude-me', '[data-private]'] })
const svg = result.toRaw()
const decoded = decodeURIComponent(svg.split(',')[1])
expect(decoded).not.toContain('Should be excluded')
expect(decoded).not.toContain('Private data')
expect(decoded).toContain('Title')
expect(decoded).toContain('This should remain')
})
it('snapdom should support filter option to exclude elements with custom logic', async () => {
const el = document.createElement('div')
el.innerHTML = `
<div class="level-1">Level 1
<div class="level-2">Level 2
<div class="level-3">Level 3</div>
</div>
</div>
`
document.body.appendChild(el)
const result = await snapdom(el, {
filter: (element) => !element.classList.contains('level-3')
})
const svg = result.toRaw()
const decoded = decodeURIComponent(svg.split(',')[1])
expect(decoded).toContain('Level 1')
expect(decoded).toContain('Level 2')
expect(decoded).not.toContain('Level 3')
})
it('snapdom should support combining exclude and filter options', async () => {
const el = document.createElement('div')
el.innerHTML = `
<div class="exclude-by-selector">Exclude by selector</div>
<div class="exclude-by-filter">Exclude by filter</div>
<div class="keep-me">Keep this content</div>
`
document.body.appendChild(el)
const result = await snapdom(el, {
exclude: ['.exclude-by-selector'],
filter: (element) => !element.classList.contains('exclude-by-filter')
})
const svg = result.toRaw()
const decoded = decodeURIComponent(svg.split(',')[1])
expect(decoded).not.toContain('Exclude by selector')
expect(decoded).not.toContain('Exclude by filter')
expect(decoded).toContain('Keep this content')
})
})
================================================
FILE: __tests__/core.cache.test.js
================================================
// __tests__/core.cache.test.js
import { describe, it, expect, beforeEach } from 'vitest'
import { cache, normalizeCachePolicy, applyCachePolicy } from '../src/core/cache.js'
/**
* Snapshot helpers to assert identity changes.
*/
function snapshotRefs() {
return {
image: cache.image,
background: cache.background,
resource: cache.resource,
defaultStyle: cache.defaultStyle,
baseStyle: cache.baseStyle,
computedStyle: cache.computedStyle,
font: cache.font,
session_styleMap: cache.session.styleMap,
session_styleCache: cache.session.styleCache,
session_nodeMap: cache.session.nodeMap,
}
}
function seedSomeData() {
cache.image.set('k', 1)
cache.background.set('b', 2)
cache.resource.set('r', 3)
cache.defaultStyle.set('d', 4)
cache.baseStyle.set('bs', 5)
cache.computedStyle.set({}, { c: 6 })
cache.font.add('Inter__400')
cache.session.styleMap.set('x', 'y')
cache.session.styleCache.set({}, { sc: 1 })
cache.session.nodeMap.set({}, document.createElement('div'))
}
describe('normalizeCachePolicy', () => {
it('maps booleans and known strings, defaults to "soft"', () => {
expect(normalizeCachePolicy(true)).toBe('soft')
expect(normalizeCachePolicy(false)).toBe('disabled')
expect(normalizeCachePolicy('auto')).toBe('auto')
expect(normalizeCachePolicy('full')).toBe('full')
expect(normalizeCachePolicy('soft')).toBe('soft')
expect(normalizeCachePolicy('disabled')).toBe('disabled')
// unknown → soft (default)
expect(normalizeCachePolicy('weird')).toBe('soft')
expect(normalizeCachePolicy(undefined)).toBe('soft')
expect(normalizeCachePolicy(123)).toBe('soft')
})
})
describe('applyCachePolicy', () => {
beforeEach(() => {
// Re-crear contenedores para que cada test sea independiente.
cache.image = new Map()
cache.background = new Map()
cache.resource = new Map()
cache.defaultStyle = new Map()
cache.baseStyle = new Map()
cache.computedStyle = new WeakMap()
cache.font = new Set()
cache.session.styleMap = new Map()
cache.session.styleCache = new WeakMap()
cache.session.nodeMap = new Map()
})
it('auto: resets only session.styleMap and session.nodeMap', () => {
seedSomeData()
const before = snapshotRefs()
applyCachePolicy('auto')
const after = snapshotRefs()
// Reemplaza solo estos dos
expect(after.session_styleMap).not.toBe(before.session_styleMap)
expect(after.session_nodeMap).not.toBe(before.session_nodeMap)
// Mantiene styleCache y resto
expect(after.session_styleCache).toBe(before.session_styleCache)
expect(after.image).toBe(before.image)
expect(after.background).toBe(before.background)
expect(after.resource).toBe(before.resource)
expect(after.defaultStyle).toBe(before.defaultStyle)
expect(after.baseStyle).toBe(before.baseStyle)
expect(after.computedStyle).toBe(before.computedStyle)
expect(after.font).toBe(before.font)
// Nuevos maps están vacíos
expect(cache.session.styleMap.size).toBe(0)
expect(cache.session.nodeMap.size).toBe(0)
})
it('soft: resets toda la sesión (styleMap, nodeMap, styleCache) y deja globales', () => {
seedSomeData()
const before = snapshotRefs()
applyCachePolicy('soft')
const after = snapshotRefs()
// Reemplaza los tres de sesión
expect(after.session_styleMap).not.toBe(before.session_styleMap)
expect(after.session_nodeMap).not.toBe(before.session_nodeMap)
expect(after.session_styleCache).not.toBe(before.session_styleCache)
// Globales se mantienen (misma identidad)
expect(after.image).toBe(before.image)
expect(after.background).toBe(before.background)
expect(after.resource).toBe(before.resource)
expect(after.defaultStyle).toBe(before.defaultStyle)
expect(after.baseStyle).toBe(before.baseStyle)
expect(after.computedStyle).toBe(before.computedStyle)
expect(after.font).toBe(before.font)
// Sesión está vacía
expect(cache.session.styleMap.size).toBe(0)
expect(cache.session.nodeMap.size).toBe(0)
})
it('full: no limpia nada (mantiene identidades y contenidos)', () => {
seedSomeData()
const before = snapshotRefs()
applyCachePolicy('full')
const after = snapshotRefs()
// Todo igual
expect(after.image).toBe(before.image)
expect(after.background).toBe(before.background)
expect(after.resource).toBe(before.resource)
expect(after.defaultStyle).toBe(before.defaultStyle)
expect(after.baseStyle).toBe(before.baseStyle)
expect(after.computedStyle).toBe(before.computedStyle)
expect(after.font).toBe(before.font)
expect(after.session_styleMap).toBe(before.session_styleMap)
expect(after.session_styleCache).toBe(before.session_styleCache)
expect(after.session_nodeMap).toBe(before.session_nodeMap)
// Y siguen con datos
expect(cache.image.size).toBeGreaterThan(0)
expect(cache.session.styleMap.size).toBeGreaterThan(0)
})
it('disabled: reinstancia TODO (global + sesión) y deja todo vacío', () => {
seedSomeData()
const before = snapshotRefs()
applyCachePolicy('disabled')
const after = snapshotRefs()
// Todo debe ser nuevo
expect(after.image).not.toBe(before.image)
expect(after.background).not.toBe(before.background)
expect(after.resource).not.toBe(before.resource)
expect(after.defaultStyle).not.toBe(before.defaultStyle)
expect(after.baseStyle).not.toBe(before.baseStyle)
expect(after.computedStyle).not.toBe(before.computedStyle)
expect(after.font).not.toBe(before.font)
expect(after.session_styleMap).not.toBe(before.session_styleMap)
expect(after.session_styleCache).not.toBe(before.session_styleCache)
expect(after.session_nodeMap).not.toBe(before.session_nodeMap)
// Vacíos
expect(cache.image.size).toBe(0)
expect(cache.background.size).toBe(0)
expect(cache.resource.size).toBe(0)
expect(cache.defaultStyle.size).toBe(0)
expect(cache.baseStyle.size).toBe(0)
expect(cache.font.size).toBe(0)
expect(cache.session.styleMap.size).toBe(0)
expect(cache.session.nodeMap.size).toBe(0)
})
it('default (input desconocido): cae en soft', () => {
seedSomeData()
const before = snapshotRefs()
// Política inexistente provoca rama default → soft
applyCachePolicy('unknown-policy')
const after = snapshotRefs()
// Reemplaza los de sesión
expect(after.session_styleMap).not.toBe(before.session_styleMap)
expect(after.session_nodeMap).not.toBe(before.session_nodeMap)
expect(after.session_styleCache).not.toBe(before.session_styleCache)
// Mantiene globales
expect(after.image).toBe(before.image)
expect(after.baseStyle).toBe(before.baseStyle)
expect(after.defaultStyle).toBe(before.defaultStyle)
expect(after.computedStyle).toBe(before.computedStyle)
expect(after.font).toBe(before.font)
})
})
================================================
FILE: __tests__/core.capture.more.test.js
================================================
import { describe, it, expect, vi, afterEach } from 'vitest'
/**
* Decode the SVG XML text from a data URL returned by captureDOM.
* @param {string} dataUrl
* @returns {string}
*/
function decodeSvg(dataUrl) {
const [, encoded] = dataUrl.split(',', 2)
return decodeURIComponent(encoded)
}
/**
* Creates a stable DOMRect for BCR stubs.
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
* @returns {DOMRect}
*/
function rect(x, y, w, h) {
return new DOMRect(x, y, w, h)
}
afterEach(() => {
vi.restoreAllMocks()
})
//
// ──────────────────────────────────────────────────────────────────────────────
// Edge cases (los que ya tenías, sin cambios)
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM edge cases', () => {
it('throws for unsupported element (unknown nodeType)', async () => {
const { captureDOM } = await import('../src/core/capture.js')
const fakeNode = { nodeType: 999 }
await expect(captureDOM(fakeNode)).rejects.toThrow()
})
it('throws if element is null', async () => {
const { captureDOM } = await import('../src/core/capture.js')
await expect(captureDOM(null)).rejects.toThrow()
})
it('throws error if getBoundingClientRect fails', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect')
.mockImplementation(() => { throw new Error('fail') })
const el = document.createElement('div')
await expect(captureDOM(el, { fast: true })).rejects.toThrow(/fail/)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// Functional & overflow rules
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM functional', () => {
it('returns a data:image/svg+xml and includes overflow visible rules', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(
rect(0, 0, 80, 40)
)
const el = document.createElement('div')
el.textContent = 'test'
const url = await captureDOM(el, { fast: true, embedFonts: false })
expect(url.startsWith('data:image/svg+xml')).toBe(true)
const svg = decodeSvg(url)
expect(svg).toMatch(/svg\{overflow:visible;?\}/)
expect(svg).toMatch(/foreignObject\{overflow:visible;?\}/)
})
it('supports scale and width/height options (wrapper sizing behavior)', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(
rect(10, 20, 100, 50) // aspect = 2
)
const el = document.createElement('div')
// scale → mantiene tamaño natural en <svg>, usa viewBox y no agrega transform: scale(...)
const svg1 = decodeSvg(await captureDOM(el, { fast: true, scale: 2, embedFonts: false }))
expect(svg1).toContain('width="100"')
expect(svg1).toContain('height="50"')
expect(svg1).toContain('viewBox="0 0 100 50"')
expect(svg1).toMatch(/<div[^>]*style="[^"]*width:\s*100px/)
expect(svg1).toMatch(/<div[^>]*style="[^"]*height:\s*50px/)
expect(/transform:[^"]*scale\(/.test(svg1)).toBe(false)
// width only → el <svg> adopta 200x100; el wrapper interno permanece 100x50 (natural)
const svg2 = decodeSvg(await captureDOM(el, { fast: true, width: 200, embedFonts: false }))
expect(svg2).toContain('width="200"')
expect(svg2).toContain('height="100"')
expect(svg2).toContain('viewBox="0 0 100 50"')
expect(svg2).toMatch(/<div[^>]*style="[^"]*width:\s*100px/)
expect(svg2).toMatch(/<div[^>]*style="[^"]*height:\s*50px/)
// height only → el <svg> adopta 200x100; el wrapper permanece 100x50 (natural)
const svg3 = decodeSvg(await captureDOM(el, { fast: true, height: 100, embedFonts: false }))
expect(svg3).toContain('width="200"')
expect(svg3).toContain('height="100"')
expect(svg3).toContain('viewBox="0 0 100 50"')
expect(svg3).toMatch(/<div[^>]*style="[^"]*width:\s*100px/)
expect(svg3).toMatch(/<div[^>]*style="[^"]*height:\s*50px/)
})
it('supports fast=false (idle scheduling path)', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 10, 10))
const el = document.createElement('div')
const url = await captureDOM(el, { fast: false, embedFonts: false })
expect(url.startsWith('data:image/svg+xml')).toBe(true)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// BaseCSS presence (sin espiar ESM): solo verificamos reglas base
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – baseCSS presence (no ESM spies)', () => {
it('includes base overflow rules on repeated calls', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 120, 60))
const el1 = document.createElement('div')
const el2 = document.createElement('div')
const s1 = decodeSvg(await captureDOM(el1, { fast: true, embedFonts: false }))
const s2 = decodeSvg(await captureDOM(el2, { fast: true, embedFonts: false }))
expect(s1).toMatch(/svg\{overflow:visible;?\}/)
expect(s1).toMatch(/foreignObject\{overflow:visible;?\}/)
expect(s2).toMatch(/svg\{overflow:visible;?\}/)
expect(s2).toMatch(/foreignObject\{overflow:visible;?\}/)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// Width/Height/Scale branches (precise, alineado a tu implementación actual)
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – width/height/scale branches (precise)', () => {
it('natural rect used when no width/height/scale given', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 100, 50))
const el = document.createElement('div')
const svg = decodeSvg(await captureDOM(el, { fast: true, embedFonts: false }))
expect(svg).toContain('width="100"')
expect(svg).toContain('height="50"')
})
it('width only → SVG 200x100; wrapper queda 100x50; viewBox natural', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 100, 50))
const el = document.createElement('div')
const svg = decodeSvg(await captureDOM(el, { fast: true, width: 200, embedFonts: false }))
expect(svg).toContain('width="200"')
expect(svg).toContain('height="100"')
expect(svg).toContain('viewBox="0 0 100 50"')
expect(svg).toMatch(/<div[^>]*style="[^"]*width:\s*100px/)
expect(svg).toMatch(/<div[^>]*style="[^"]*height:\s*50px/)
})
it('height only → SVG 200x100; wrapper queda 100x50; viewBox natural', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 100, 50))
const el = document.createElement('div')
const svg = decodeSvg(await captureDOM(el, { fast: true, height: 100, embedFonts: false }))
expect(svg).toContain('width="200"')
expect(svg).toContain('height="100"')
expect(svg).toContain('viewBox="0 0 100 50"')
expect(svg).toMatch(/<div[^>]*style="[^"]*width:\s*100px/)
expect(svg).toMatch(/<div[^>]*style="[^"]*height:\s*50px/)
})
it('scale only → keeps natural width/height; uses viewBox; no transform', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 100, 50))
const el = document.createElement('div')
const svg = decodeSvg(await captureDOM(el, { fast: true, scale: 2, embedFonts: false }))
expect(svg).toContain('width="100"')
expect(svg).toContain('height="50"')
expect(svg).toContain('viewBox="0 0 100 50"')
expect(/transform:[^"]*scale\(/.test(svg)).toBe(false)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// Viewport path (sin tocar utils): solo afirmamos x/y presentes (0 o valores)
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – viewport path sanity', () => {
it('foreignObject has x/y attributes (tx/ty), even if 0', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(5, 7, 80, 30))
const el = document.createElement('div')
const svg = decodeSvg(await captureDOM(el, { fast: true, embedFonts: false }))
// No imponemos un cálculo exacto; validamos la presencia de x="" y y="" numéricos.
expect(svg).toMatch(/<foreignObject[^>]*\sx="[-\d]+"/)
expect(svg).toMatch(/<foreignObject[^>]*\sy="[-\d]+"/)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// #348: CSS vars excluded from snapshot – fidelity preserved (var() resolved)
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – #348 CSS vars fidelity', () => {
it('color: var(--x) resolves to computed value in output', async () => {
const { captureDOM } = await import('../src/core/capture.js')
const wrap = document.createElement('div')
wrap.innerHTML = `
<style>:root { --snapdom-test-color: rgb(255, 0, 0); } .t348 { color: var(--snapdom-test-color); }</style>
<div class="t348">red text</div>
`
document.body.appendChild(wrap)
const el = wrap.querySelector('.t348')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 80, 20))
const url = await captureDOM(el, { fast: true, embedFonts: false })
document.body.removeChild(wrap)
const svg = decodeSvg(url)
expect(svg).toMatch(/rgb\(255,\s*0,\s*0\)|#[fF]{2}0000/)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// #372: iframe CSS isolation – wrapper div must not inherit iframe cascade
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – #372 iframe CSS isolation', () => {
it('wrapper div has all:initial to block iframe cascade (e.g. div { border: 10px solid red })', async () => {
const { captureDOM } = await import('../src/core/capture.js')
const iframe = document.createElement('iframe')
iframe.srcdoc = `
<!DOCTYPE html>
<html><head><style>div { border: 10px solid red; }</style></head>
<body><div>content</div></body></html>
`
iframe.style.width = '100px'
iframe.style.height = '80px'
document.body.appendChild(iframe)
await new Promise((resolve) => { iframe.onload = resolve })
const doc = iframe.contentDocument
const root = doc.documentElement
const url = await captureDOM(root, { fast: true, embedFonts: false })
document.body.removeChild(iframe)
const svg = decodeSvg(url)
// Wrapper div (container) inside foreignObject must be isolated from iframe CSS (#372).
// Browser expands all:initial to individual props (border: initial, position: initial, etc.)
expect(svg).toContain('box-sizing: border-box')
expect(svg).toMatch(/border:\s*initial|position:\s*initial/)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// #362: Tailwind * { border: 0 solid } – normalize to border: none in capture
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – #362 canvas Tailwind border', () => {
it('elements with border-width 0 get border:none in output (not border: 0 solid)', async () => {
const { captureDOM } = await import('../src/core/capture.js')
const wrap = document.createElement('div')
wrap.innerHTML = `
<style>* { border: 0 solid; }</style>
<canvas id="c362" width="80" height="40"></canvas>
`
document.body.appendChild(wrap)
const canvas = wrap.querySelector('#c362')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 80, 40)
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(
new DOMRect(0, 0, 80, 40)
)
const url = await captureDOM(wrap, { fast: true, embedFonts: false })
document.body.removeChild(wrap)
const svg = decodeSvg(url)
// Canvas becomes img; snapshot should normalize border: 0 solid → border: none
expect(svg).toMatch(/\bborder:\s*none\b/)
})
})
// ──────────────────────────────────────────────────────────────────────────────
// Transform handling (lenient, effect-only)
// ──────────────────────────────────────────────────────────────────────────────
describe('captureDOM – transform handling (lenient heuristic)', () => {
it('when element has a rotate transform, output stays valid and exposes transform-related styles', async () => {
const { captureDOM } = await import('../src/core/capture.js')
const el = document.createElement('div')
el.style.transform = 'rotate(30deg)'
vi.spyOn(Element.prototype, 'getBoundingClientRect')
.mockReturnValue(new DOMRect(0, 0, 120, 60))
const svg = decodeSvg(await captureDOM(el, { fast: true, embedFonts: false }))
// 1) SVG válido
expect(svg.startsWith('<svg')).toBe(true)
// 2) El wrapper suele incluir transform-origin aunque no tenga shorthand transform
expect(svg).toMatch(/transform-origin:\s*[\d.]+px\s+[\d.]+px/)
// 3) Aceptamos props individuales inline O resets en el CSS base (rotate/scale/translate:none)
const hasInlineProps =
/style="[^"]*(?:rotate:\s*[^;"]+|scale:\s*[^;"]+|translate:\s*[^;"]+)[^"]*"/.test(svg)
const hasBaseResets = /\b(rotate|scale|translate):\s*none\b/.test(svg)
expect(hasInlineProps || hasBaseResets).toBe(true)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// Base transform + individual rotate/scale/translate (sin forzar valores exactos)
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – baseTransform & individual props on clone (lenient)', () => {
it('inline style in output includes individual transform properties', async () => {
const { captureDOM } = await import('../src/core/capture.js')
const el = document.createElement('div')
el.style.transform = 'rotate(10deg)'
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 100, 100))
const svg = decodeSvg(await captureDOM(el, { fast: true, embedFonts: false }))
// Aceptamos presencia inline o, si la implementación normaliza, resets en CSS base.
const hasInlineAny = /style="[^"]*(?:rotate|scale|translate):/.test(svg)
const hasBaseResets = /\b(rotate|scale|translate):\s*none\b/.test(svg)
expect(hasInlineAny || hasBaseResets).toBe(true)
// transform-origin suele estar presente
expect(svg).toMatch(/transform-origin:\s*[\d.]+px\s+[\d.]+px/)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// embedFonts branch: no espiamos ESM; solo verificamos que no rompa y que
// potencialmente inserte CSS de fuentes si el pipeline interno lo decide.
// (Sin red real, aceptamos ambas salidas.)
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – embedFonts=true (no spies, effect-only)', () => {
it('does not throw and may inject fonts CSS', async () => {
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 50, 20))
const el = document.createElement('div')
el.textContent = 'Hello'
const svg = decodeSvg(await captureDOM(el, { fast: true, embedFonts: true }))
// No afirmamos siempre la presencia de CSS de fuentes (depende de IO/hints),
// pero sí que sea SVG válido.
expect(svg.startsWith('<svg')).toBe(true)
})
})
//
// ──────────────────────────────────────────────────────────────────────────────
// Sandbox cleanup
// ──────────────────────────────────────────────────────────────────────────────
//
describe('captureDOM – removes #snapdom-sandbox when absolute', () => {
it('cleans up the offscreen sandbox', async () => {
const sandbox = document.createElement('div')
sandbox.id = 'snapdom-sandbox'
sandbox.style.position = 'absolute'
document.body.appendChild(sandbox)
const { captureDOM } = await import('../src/core/capture.js')
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 10, 10))
const el = document.createElement('div')
const url = await captureDOM(el, { fast: true })
expect(url.startsWith('data:image/svg+xml')).toBe(true)
expect(document.getElementById('snapdom-sandbox')).toBeNull()
})
})
// ──────────────────────────────────────────────────────────────────────────────
// Width & Height together -> container may use non-uniform scale OR set wrapper size
// ──────────────────────────────────────────────────────────────────────────────
describe('captureDOM – width & height together apply size (scale or wrapper size)', () => {
it('adopts requested SVG width/height; implementation may use non-uniform scale, wrapper sizing, or viewBox-only', async () => {
const { captureDOM } = await import('../src/core/capture.js')
// Natural rect = 100x50 (aspect 2)
vi.spyOn(Element.prototype, 'getBoundingClientRect')
.mockReturnValue(new DOMRect(0, 0, 100, 50))
const el = document.createElement('div')
// Ask for 150x120
const svg = decodeSvg(await captureDOM(el, {
fast: true,
width: 150,
height: 120,
embedFonts: false,
}))
// SVG header adopta el tamaño pedido
expect(svg).toContain('width="150"')
expect(svg).toContain('height="120"')
// Implementación puede elegir:
// A) non-uniform scale en container
const hasScale = /transform:[^"]*scale\(\s*1\.5[0-9]*\s*,\s*2\.4[0-9]*\s*\)/.test(svg)
// B) explicit wrapper sizing via style width/height
const hasWrapperSize =
/<div[^>]*style="[^"]*width:\s*150px[^"]*height:\s*120px/.test(svg) ||
/<div[^>]*style="[^"]*height:\s*120px[^"]*width:\s*150px/.test(svg)
// C) solo viewBox natural con wrapper natural (lo que estás emitiendo)
const usesViewBoxOnly =
svg.includes('viewBox="0 0 100 50"') &&
/<div[^>]*style="[^"]*width:\s*100px/.test(svg) &&
/<div[^>]*style="[^"]*height:\s*50px/.test(svg)
expect(hasScale || hasWrapperSize || usesViewBoxOnly).toBe(true)
})
})
// ──────────────────────────────────────────────
gitextract_5bp_18n4/ ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── labels.yml │ ├── scripts/ │ │ ├── classify-issues.js │ │ └── update-contributors.js │ └── workflows/ │ ├── issue-triage.yml │ ├── label-sync.yml │ └── update-contributors.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_CN.md ├── __tests__/ │ ├── api.preCache.more.test.js │ ├── api.preCache.test.js │ ├── api.snapdom.more.test.js │ ├── api.snapdom.test.js │ ├── core.cache.test.js │ ├── core.capture.more.test.js │ ├── core.capture.test.js │ ├── core.clone.more.test.js │ ├── core.clone.test.js │ ├── core.context.test.js │ ├── core.exporters.test.js │ ├── core.prepare.test.js │ ├── cssTools.utils.test.js │ ├── exporter.download.test.js │ ├── exporter.toCanvas.more.test.js │ ├── exporter.toCanvas.test.js │ ├── exporter.toImg.test.js │ ├── exporters.jpg-png-svg-webp.test.js │ ├── index.browser.test.js │ ├── module.background.test.js │ ├── module.changeCSS.test.js │ ├── module.counter.test.js │ ├── module.fonts.katex.test.js │ ├── module.fonts.more.more.test.js │ ├── module.fonts.more.test.js │ ├── module.fonts.test.js │ ├── module.iconFonts.more.test.js │ ├── module.iconFonts.test.js │ ├── module.lineClamp.test.js │ ├── module.pseudo.test.js │ ├── module.snapFetch.test.js │ ├── module.styles.test.js │ ├── module.svg.test.js │ ├── modules.images.test.js │ ├── snapdom.attributes.test.js │ ├── snapdom.backgroundColor.test.js │ ├── snapdom.benchmark.js │ ├── snapdom.complex.benchmark.js │ ├── snapdom.delete.test.js │ ├── snapdom.precache.perf.test.js │ ├── snapdom.test.js │ ├── snapdom.vs.htm2canvas.outputfilesize.test.js │ ├── snapdom.vs.modernscreenshot.outputfilesize.test.js │ ├── three-shake.test.js │ ├── utils.browser.more.test.js │ ├── utils.browser.test.js │ ├── utils.capture.helpers.test.js │ ├── utils.css.test.js │ ├── utils.helpers.test.js │ ├── utils.image.test.js │ └── utils.transforms.helpers.test.js ├── docs/ │ ├── CNAME │ ├── assets/ │ │ └── favicon/ │ │ └── site.webmanifest │ ├── index.html │ └── labs.html ├── esbuild.config.mjs ├── eslint.config.cjs ├── package.json ├── plugins/ │ └── html-in-canvas.js ├── src/ │ ├── api/ │ │ ├── preCache.js │ │ └── snapdom.js │ ├── core/ │ │ ├── cache.js │ │ ├── capture.js │ │ ├── clone.js │ │ ├── context.js │ │ ├── exporters.js │ │ ├── plugins.js │ │ └── prepare.js │ ├── exporters/ │ │ ├── download.js │ │ ├── toBlob.js │ │ ├── toCanvas.js │ │ ├── toImg.js │ │ ├── toJpg.js │ │ ├── toPng.js │ │ ├── toSvg.js │ │ └── toWebp.js │ ├── index.browser.js │ ├── index.js │ ├── modules/ │ │ ├── CSSVar.js │ │ ├── background.js │ │ ├── changeCSS.js │ │ ├── counter.js │ │ ├── fonts.js │ │ ├── iconFonts.js │ │ ├── images.js │ │ ├── lineClamp.js │ │ ├── pseudo.js │ │ ├── rasterize.js │ │ ├── snapFetch.js │ │ ├── styles.js │ │ └── svgDefs.js │ └── utils/ │ ├── browser.js │ ├── capture.helpers.js │ ├── clone.helpers.js │ ├── css.js │ ├── debug.js │ ├── helpers.js │ ├── image.js │ ├── index.js │ ├── prepare.helpers.js │ └── transforms.helpers.js ├── types/ │ └── snapdom.d.ts └── vitest.config.js
SYMBOL INDEX (359 symbols across 64 files)
FILE: .github/scripts/classify-issues.js
constant CRITICAL_KEYWORDS (line 21) | const CRITICAL_KEYWORDS = [
constant HIGH_PRIORITY_KEYWORDS (line 27) | const HIGH_PRIORITY_KEYWORDS = [
constant LOW_KEYWORDS (line 44) | const LOW_KEYWORDS = [
constant SAFARI_KEYWORDS (line 54) | const SAFARI_KEYWORDS = ['safari', 'ios', 'webkit', 'iphone', 'ipad', 'a...
constant FIREFOX_KEYWORDS (line 55) | const FIREFOX_KEYWORDS = ['firefox', 'mozilla', 'gecko'];
function classifyPriority (line 61) | function classifyPriority(issue) {
function classifyExtra (line 84) | function classifyExtra(issue) {
function request (line 98) | function request(method, path, body) {
function fetchOpenIssues (line 129) | async function fetchOpenIssues() {
function addLabels (line 143) | async function addLabels(issueNumber, labels) {
FILE: .github/scripts/update-contributors.js
function fetchContributors (line 8) | function fetchContributors() {
function buildHTML (line 32) | function buildHTML(contributors) {
function updateReadmes (line 45) | function updateReadmes(contributorHTML) {
FILE: __tests__/core.cache.test.js
function snapshotRefs (line 8) | function snapshotRefs() {
function seedSomeData (line 23) | function seedSomeData() {
FILE: __tests__/core.capture.more.test.js
function decodeSvg (line 8) | function decodeSvg(dataUrl) {
function rect (line 21) | function rect(x, y, w, h) {
method get (line 513) | get(prop) {
FILE: __tests__/core.clone.test.js
function runClone (line 13) | async function runClone(node) {
FILE: __tests__/exporter.download.test.js
constant DATA_PNG (line 8) | const DATA_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...
constant DATA_SVG (line 9) | const DATA_SVG = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponen...
FILE: __tests__/exporter.toCanvas.more.test.js
constant ONE_BY_ONE_PNG (line 8) | const ONE_BY_ONE_PNG =
FILE: __tests__/exporter.toCanvas.test.js
constant ONE_BY_ONE_PNG (line 11) | const ONE_BY_ONE_PNG =
FILE: __tests__/exporter.toImg.test.js
constant DATA_PNG (line 11) | const DATA_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...
constant DATA_SVG (line 12) | const DATA_SVG = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponen...
FILE: __tests__/exporters.jpg-png-svg-webp.test.js
constant DATA_PNG (line 4) | const DATA_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...
FILE: __tests__/module.fonts.katex.test.js
function addLink (line 56) | function addLink(href) {
FILE: __tests__/module.fonts.more.more.test.js
method blob (line 15) | async blob () { return new Blob([new Uint8Array([0x77, 0x6F, 0x32])], { ...
method text (line 16) | async text () { return '' }
function addStyle (line 51) | function addStyle (css) {
function cleanInjectedStuff (line 58) | function cleanInjectedStuff () {
function setDocumentFonts (line 69) | function setDocumentFonts (fontsArray = []) {
FILE: __tests__/module.fonts.more.test.js
function addLink (line 44) | function addLink(href) {
function addStyle (line 51) | function addStyle(css) {
method add (line 391) | add(ff) { items.push(ff) }
FILE: __tests__/module.fonts.test.js
function cleanFontEnvironment (line 6) | function cleanFontEnvironment() {
function addStyleTag (line 12) | function addStyleTag(css) {
function makeRequired (line 21) | function makeRequired(family, weight='400', style='normal', stretchPct=1...
function makeUsedCodepoints (line 26) | function makeUsedCodepoints(text='A') {
function setDocumentFonts (line 36) | function setDocumentFonts(fontsArray = []) {
FILE: __tests__/module.snapFetch.test.js
function importFresh (line 10) | async function importFresh() {
constant ORIGIN (line 16) | const ORIGIN = globalThis.location?.origin || 'http://localhost'
constant SAME (line 17) | const SAME = `${ORIGIN}/assets/a.css`
constant CROSS (line 18) | const CROSS = 'https://cdn.example/x.png'
function mockFetchOnce (line 21) | function mockFetchOnce(status = 200, body = 'ok', headers = {}) {
function mockFetchNetworkError (line 26) | function mockFetchNetworkError() {
function mockFetchTimeoutAware (line 33) | function mockFetchTimeoutAware() {
function deferred (line 45) | function deferred() {
FILE: __tests__/module.styles.test.js
function loadInlineAllStylesFresh (line 4) | async function loadInlineAllStylesFresh() {
class MOStub (line 11) | class MOStub {
method constructor (line 13) | constructor(/* cb */) {
method observe (line 16) | observe() {}
method disconnect (line 17) | disconnect() {}
method takeRecords (line 18) | takeRecords() { return [] }
function freshSession (line 21) | function freshSession() {
FILE: __tests__/module.svg.test.js
constant SVG_NS (line 4) | const SVG_NS = 'http://www.w3.org/2000/svg'
function createGlobalSvg (line 7) | function createGlobalSvg() {
function createRootContainer (line 13) | function createRootContainer() {
function createLocalUse (line 19) | function createLocalUse(hrefValue) {
FILE: __tests__/snapdom.benchmark.js
function loadHtml2Canvas (line 10) | async function loadHtml2Canvas() {
function setupContainer (line 36) | async function setupContainer() {
FILE: __tests__/snapdom.complex.benchmark.js
function loadHtml2Canvas (line 9) | async function loadHtml2Canvas() {
function setupContainer (line 35) | async function setupContainer() {
FILE: __tests__/snapdom.precache.perf.test.js
function createContainer (line 11) | function createContainer(size) {
function waitForNextFrame (line 66) | function waitForNextFrame() {
FILE: __tests__/snapdom.vs.htm2canvas.outputfilesize.test.js
function dataUrlBytes (line 6) | function dataUrlBytes(dataUrl) {
FILE: __tests__/three-shake.test.js
method defineExports (line 84) | async defineExports(ctx) {
FILE: __tests__/utils.image.test.js
function clearCaches (line 21) | function clearCaches() {
method constructor (line 77) | constructor() { setTimeout(() => this.onload && this.onload(), 0) }
method src (line 78) | set src(_) {}
method decode (line 79) | decode() { return Promise.resolve() }
method naturalWidth (line 80) | get naturalWidth() { return 2 }
method naturalHeight (line 81) | get naturalHeight() { return 2 }
method width (line 82) | get width() { return 2 }
method height (line 83) | get height() { return 2 }
method crossOrigin (line 84) | set crossOrigin(_) {}
method onload (line 85) | set onload(_) {}
method onerror (line 86) | set onerror(_) {}
method constructor (line 96) | constructor() { setTimeout(() => this.onerror && this.onerror(), 0) }
method src (line 97) | set src(_) {}
method crossOrigin (line 98) | set crossOrigin(_) {}
method onload (line 99) | set onload(_) {}
method onerror (line 100) | set onerror(_) {}
function expectOkDataURL (line 119) | function expectOkDataURL(r) {
function expectOkText (line 128) | function expectOkText(r) {
method constructor (line 139) | constructor() { setTimeout(() => this.onload && this.onload(), 0) }
method src (line 140) | set src(_) {}
method decode (line 141) | decode() { return Promise.resolve() }
method naturalWidth (line 142) | get naturalWidth() { return 3 }
method naturalHeight (line 143) | get naturalHeight() { return 4 }
method crossOrigin (line 144) | set crossOrigin(_) {}
method onload (line 145) | set onload(_) {}
method onerror (line 146) | set onerror(_) {}
FILE: esbuild.config.mjs
function buildLegacy (line 27) | async function buildLegacy() {
function buildESM (line 44) | async function buildESM() {
function buildSubpaths (line 60) | async function buildSubpaths() {
function main (line 76) | async function main() {
FILE: plugins/html-in-canvas.js
constant PLUGIN_NAME (line 10) | const PLUGIN_NAME = 'html-in-canvas'
function isDrawElementImageAvailable (line 12) | function isDrawElementImageAvailable() {
function htmlInCanvasPlugin (line 25) | function htmlInCanvasPlugin() {
FILE: src/api/preCache.js
function preCache (line 20) | async function preCache(root = document, options = {}) {
FILE: src/api/snapdom.js
function plugins (line 12) | function plugins(...defs) { registerPlugins(...defs); return snapdom }
constant INTERNAL_TOKEN (line 16) | const INTERNAL_TOKEN = Symbol('snapdom.internal')
constant INTERNAL_EXPORT_TOKEN (line 18) | const INTERNAL_EXPORT_TOKEN = Symbol('snapdom.internal.silent')
function main (line 40) | async function main(element, userOptions) {
function normalizeExportOptions (line 159) | function normalizeExportOptions(type, opts) {
function runExport (line 171) | async function runExport(type, opts) {
function safariWarmup (line 294) | async function safariWarmup(element, baseOptions) {
function hasBackgroundOrMask (line 368) | function hasBackgroundOrMask(el) {
FILE: src/core/cache.js
constant MAX_IMAGE (line 2) | const MAX_IMAGE = 100
constant MAX_BACKGROUND (line 3) | const MAX_BACKGROUND = 100
constant MAX_RESOURCE (line 4) | const MAX_RESOURCE = 150
constant MAX_BASE_STYLE (line 5) | const MAX_BASE_STYLE = 50
constant MAX_DEFAULT_STYLE (line 6) | const MAX_DEFAULT_STYLE = 30
class EvictingMap (line 12) | class EvictingMap extends Map {
method constructor (line 13) | constructor(maxSize = 100, ...args) {
method set (line 17) | set(key, value) {
function normalizeCachePolicy (line 56) | function normalizeCachePolicy(v) {
function applyCachePolicy (line 72) | function applyCachePolicy(policy = 'soft') {
FILE: src/core/capture.js
function captureDOM (line 49) | async function captureDOM(element, options) {
function hasTFBBox (line 363) | function hasTFBBox(el) {
FILE: src/core/clone.js
function deepClone (line 28) | async function deepClone(node, sessionCache, options) {
FILE: src/core/context.js
function createContext (line 38) | function createContext(options = {}) {
FILE: src/core/exporters.js
function normalizeExporter (line 18) | function normalizeExporter(spec) {
function registerExporters (line 37) | function registerExporters(...defs) {
function getExporter (line 56) | function getExporter(format) {
function _exportersMap (line 63) | function _exportersMap() { return new Map(__exporters) }
function _clearExporters (line 64) | function _clearExporters() { __exporters.clear() }
function runExportHooks (line 86) | async function runExportHooks(ctx, work) {
FILE: src/core/plugins.js
function normalizePlugin (line 28) | function normalizePlugin(spec) {
function registerPlugins (line 46) | function registerPlugins(...defs) {
function getContextPlugins (line 65) | function getContextPlugins(context) {
function runHook (line 77) | async function runHook(name, context, payload) {
function runAll (line 97) | async function runAll(name, context, payload) {
function pluginsList (line 113) | function pluginsList() { return __plugins.slice() }
function clearPlugins (line 116) | function clearPlugins() { __plugins.length = 0 }
function mergePlugins (line 130) | function mergePlugins(localDefs) {
function attachSessionPlugins (line 163) | function attachSessionPlugins(context, localDefs, force = false) {
function getGlobalPlugins (line 173) | function getGlobalPlugins() {
FILE: src/core/prepare.js
function prepareClone (line 26) | async function prepareClone(element, options = {}) {
FILE: src/exporters/download.js
function shareFile (line 12) | async function shareFile(blob, filename) {
function download (line 33) | async function download(url, options) {
FILE: src/exporters/toBlob.js
function toBlob (line 13) | async function toBlob(url, options) {
FILE: src/exporters/toCanvas.js
function isSvgDataURL (line 13) | function isSvgDataURL(u) {
function decodeSvgFromDataURL (line 16) | function decodeSvgFromDataURL(u) {
function encodeSvgToDataURL (line 20) | function encodeSvgToDataURL(svgText) {
function splitDecls (line 23) | function splitDecls(s) {
function boxShadowToDropShadow (line 34) | function boxShadowToDropShadow(value) {
function rewriteDeclList (line 61) | function rewriteDeclList(list) {
function rewriteCssBlock (line 89) | function rewriteCssBlock(css) {
function rewriteSvgBoxShadowToDropShadow (line 92) | function rewriteSvgBoxShadowToDropShadow(svgText) {
function maybeConvertBoxShadowForSafari (line 103) | function maybeConvertBoxShadowForSafari(url) {
function toCanvas (line 128) | async function toCanvas(url, options) {
FILE: src/exporters/toImg.js
function toImg (line 11) | async function toImg(url, options) {
FILE: src/exporters/toJpg.js
function toJpg (line 4) | async function toJpg(elOrUrl, opts = {}) {
FILE: src/exporters/toPng.js
function toPng (line 10) | async function toPng(elOrUrl, opts = {}) {
FILE: src/exporters/toWebp.js
function toWebp (line 4) | async function toWebp(elOrUrl, opts = {}) {
FILE: src/modules/CSSVar.js
constant KEY_PROPS (line 4) | const KEY_PROPS = ['fill', 'stroke', 'color', 'background-color', 'stop-...
function getBaselineComputed (line 10) | function getBaselineComputed(tagName, ns) {
function resolveCSSVars (line 43) | function resolveCSSVars(sourceEl, cloneEl) {
FILE: src/modules/background.js
function inlineBackgroundImages (line 31) | async function inlineBackgroundImages(source, clone, styleCache, options...
FILE: src/modules/changeCSS.js
function freezeSticky (line 14) | function freezeSticky(originalRoot, cloneRoot) {
function _toPx (line 98) | function _toPx(v) {
function _pathOf (line 104) | function _pathOf(el, root) {
function _findByPathIgnoringPlaceholders (line 125) | function _findByPathIgnoringPlaceholders(root, path, phAttr) {
function _childrenWithoutPlaceholders (line 140) | function _childrenWithoutPlaceholders(el, phAttr) {
FILE: src/modules/counter.js
function hasCounters (line 14) | function hasCounters(input) {
function unquoteDoubleStrings (line 19) | function unquoteDoubleStrings(s) {
function alpha (line 29) | function alpha(n, upper = false) {
function roman (line 41) | function roman(n, upper = true) {
function formatCounter (line 55) | function formatCounter(value, style) {
function buildCounterContext (line 81) | function buildCounterContext(root) {
function resolveCountersInContent (line 224) | function resolveCountersInContent(raw, node, ctx) {
function deriveCounterCtxForPseudo (line 260) | function deriveCounterCtxForPseudo(node, pseudoStyle, baseCtx) {
function resolvePseudoContent (line 330) | function resolvePseudoContent(node, pseudo, baseCtx) {
FILE: src/modules/fonts.js
function iconToImage (line 22) | async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize...
constant GENERIC_FAMILIES (line 68) | const GENERIC_FAMILIES = new Set([
constant FONT_LIBRARIES (line 74) | const FONT_LIBRARIES = ['katex', 'mathjax', 'mathml']
function pickPrimaryFamily (line 82) | function pickPrimaryFamily(familyList) {
function normWeight (line 96) | function normWeight(w) {
function normStyle (line 109) | function normStyle(s) {
function normStretchPct (line 121) | function normStretchPct(st) {
function parseWeightSpec (line 126) | function parseWeightSpec(spec) {
function parseStyleSpec (line 137) | function parseStyleSpec(spec) {
function parseStretchSpec (line 144) | function parseStretchSpec(spec) {
function baseFamilyToken (line 168) | function baseFamilyToken(family) {
function isLikelyFontStylesheet (line 177) | function isLikelyFontStylesheet(href, requiredFamilies, allowedDomains =...
function familiesFromRequired (line 217) | function familiesFromRequired(required) {
function rewriteRelativeUrls (line 231) | function rewriteRelativeUrls(cssText, baseHref) {
constant IMPORT_ANY_RE (line 246) | const IMPORT_ANY_RE = /@import\s+(?:url\(\s*(['"]?)([^)"']+)\1\s*\)|(['"...
constant MAX_IMPORT_DEPTH (line 248) | const MAX_IMPORT_DEPTH = 4
function inlineImportsAndRewrite (line 258) | async function inlineImportsAndRewrite(cssText, ownerHref, useProxy) {
constant URL_RE (line 316) | const URL_RE = /url\((["']?)([^"')]+)\1\)/g
constant FACE_RE (line 317) | const FACE_RE = /@font-face[^{}]*\{[^}]*\}/g
function parseUnicodeRange (line 320) | function parseUnicodeRange(ur) {
function unicodeIntersects (line 349) | function unicodeIntersects(used, ranges) {
function extractSrcUrls (line 359) | function extractSrcUrls(srcValue, baseHref) {
function inlineUrlsInCssBlock (line 374) | async function inlineUrlsInCssBlock(cssBlock, baseHref, useProxy = '') {
function subsetFromRanges (line 408) | function subsetFromRanges(ranges) {
function buildSimpleExcluder (line 424) | function buildSimpleExcluder(ex = {}) {
function dedupeFontFaces (line 443) | function dedupeFontFaces(cssText) {
function buildFontsCacheKey (line 485) | function buildFontsCacheKey(required, exclude, localFonts, useProxy, fon...
function collectFacesFromSheet (line 520) | async function collectFacesFromSheet(sheet, baseHref, emitFace, ctx) {
function embedCustomFonts (line 601) | async function embedCustomFonts({
function collectUsedFontVariants (line 924) | function collectUsedFontVariants(root) {
function collectUsedCodepoints (line 956) | function collectUsedCodepoints(root) {
function ensureFontsReady (line 999) | async function ensureFontsReady(families, warmupRepetitions = 2) {
FILE: src/modules/iconFonts.js
constant ICON_FONT_URLS (line 21) | const ICON_FONT_URLS = Object.assign({
function extendIconFonts (line 30) | function extendIconFonts(fonts) {
function isIconFont (line 39) | function isIconFont(input) {
function isMaterialFamily (line 52) | function isMaterialFamily(family = '') {
function parseAxes (line 59) | function parseAxes(variation = '') {
function ensureLigatureCanvasFont (line 82) | async function ensureLigatureCanvasFont(cssFamily, className, axes) {
function ensureMaterialFontsReady (line 140) | async function ensureMaterialFontsReady(family = 'Material Icons', px = ...
function resolvePaintColor (line 149) | function resolvePaintColor(cs) {
function materialIconToImage (line 157) | async function materialIconToImage(
function ligatureIconToImage (line 223) | async function ligatureIconToImage(cloneRoot, sourceRoot) {
FILE: src/modules/images.js
constant XLINK_NS (line 9) | const XLINK_NS = 'http://www.w3.org/1999/xlink'
function getSvgImageHref (line 11) | function getSvgImageHref(el) {
function extractImageDimensions (line 21) | function extractImageDimensions(img) {
function inlineImages (line 46) | async function inlineImages(clone, options = {}) {
FILE: src/modules/lineClamp.js
function lineClampTree (line 10) | function lineClampTree(el) {
function lineClamp (line 31) | function lineClamp(el) {
function getClamp (line 75) | function getClamp(el) {
function usedLineHeightPx (line 83) | function usedLineHeightPx(cs) {
function vpad (line 93) | function vpad(cs) {
function isPlainTextContainer (line 98) | function isPlainTextContainer(el) {
FILE: src/modules/pseudo.js
constant CSS_RULE_SCAN_BUDGET (line 30) | const CSS_RULE_SCAN_BUDGET = 300
function preflightWithFp (line 39) | function preflightWithFp(doc, sessionCache) {
function safeRules (line 54) | function safeRules(sheet) {
function styleFingerprint (line 71) | function styleFingerprint(doc) {
function sheetHasNeedles (line 108) | function sheetHasNeedles(sheet, needles, state) {
function shouldProcessPseudos (line 155) | function shouldProcessPseudos(doc = document) {
function unquoteDoubleStrings (line 228) | function unquoteDoubleStrings(s) {
function collapseCssContent (line 237) | function collapseCssContent(raw) {
function withSiblingOverrides (line 253) | function withSiblingOverrides(node, base) {
function deriveCounterCtxForPseudo (line 285) | function deriveCounterCtxForPseudo(node, pseudoStyle, baseCtx) {
function resolvePseudoContentAndIncs (line 351) | function resolvePseudoContentAndIncs(node, pseudo, baseCtx) {
function inlinePseudoElements (line 382) | async function inlinePseudoElements(source, clone, sessionCache, options) {
FILE: src/modules/rasterize.js
function rasterize (line 10) | async function rasterize(url, options) {
FILE: src/modules/snapFetch.js
function createSnapLogger (line 38) | function createSnapLogger(prefix = '[snapDOM]', { ttlMs = 5 * 60_000, ma...
function isSpecialURL (line 74) | function isSpecialURL(url) {
function isAlreadyProxied (line 79) | function isAlreadyProxied(url, useProxy) {
function shouldProxy (line 97) | function shouldProxy(url, useProxy) {
function applyProxy (line 119) | function applyProxy(url, useProxy) {
function blobToDataURL (line 148) | function blobToDataURL(blob) {
function makeKey (line 157) | function makeKey(url, o) {
function snapFetch (line 177) | async function snapFetch(url, options = {}) {
FILE: src/modules/styles.js
function bumpEpoch (line 7) | function bumpEpoch() { __epoch++ }
function notifyStyleEpoch (line 9) | function notifyStyleEpoch() { bumpEpoch() }
function setupInvalidationOnce (line 12) | function setupInvalidationOnce(root = document.documentElement) {
function snapshotComputedStyleFull (line 32) | function snapshotComputedStyleFull(style, options = {}) {
function styleSignature (line 127) | function styleSignature(snap) {
function getSnapshot (line 135) | function getSnapshot(el, preStyle = null, options = {}) {
function _resolveCtx (line 145) | function _resolveCtx(sessionOrCtx, opts) {
function normalizeInlineStyleToComputed (line 186) | function normalizeInlineStyleToComputed(source, clone, computed) {
function inlineAllStyles (line 195) | async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
function isReplaced (line 237) | function isReplaced(el) {
function hasBox (line 251) | function hasBox(cs) {
function isFlexOrGridItem (line 266) | function isFlexOrGridItem(el) {
function hasFlowFast (line 281) | function hasFlowFast(el, cs) {
function stripHeightForWrappers (line 303) | function stripHeightForWrappers(el, cs, snap) {
FILE: src/modules/svgDefs.js
function inlineExternalDefsAndSymbols (line 13) | function inlineExternalDefsAndSymbols(element, lookupRoot) {
FILE: src/utils/browser.js
function idle (line 7) | function idle(fn, { fast = false } = {}) {
function isIOS (line 16) | function isIOS() {
function isSafari (line 30) | function isSafari() {
function isFirefox (line 67) | function isFirefox() {
FILE: src/utils/capture.helpers.js
function stripRootShadows (line 15) | function stripRootShadows(originalEl, cloneRoot, opts = {}) {
function removeAllComments (line 33) | function removeAllComments(root) {
function sanitizeAttributesForXHTML (line 45) | function sanitizeAttributesForXHTML(root, opts = {}) {
function sanitizeCloneForXHTML (line 84) | function sanitizeCloneForXHTML(root, opts = {}) {
function authorHasExplicitSize (line 95) | function authorHasExplicitSize(el) {
function isReplacedElement (line 107) | function isReplacedElement(el) {
function shouldShrinkBox (line 123) | function shouldShrinkBox(srcEl, cs) {
function shrinkAutoSizeBoxes (line 152) | function shrinkAutoSizeBoxes(sourceRoot, cloneRoot, styleCache = new Map...
function contributesToParentHeight (line 204) | function contributesToParentHeight(el) {
function willBeExcluded (line 217) | function willBeExcluded(el, options) {
function estimateKeptHeight (line 239) | function estimateKeptHeight(container, options) {
constant SCROLLBAR_PSEUDO (line 278) | const SCROLLBAR_PSEUDO = /::-webkit-scrollbar(-[a-z]+)?\b/i
function collectScrollbarRulesFromRules (line 287) | function collectScrollbarRulesFromRules(rules, seen = new Set()) {
function collectScrollbarCSS (line 325) | function collectScrollbarCSS(doc) {
FILE: src/utils/clone.helpers.js
function idleCallback (line 19) | function idleCallback(childList, callback, fast) {
function addNotSlottedRightmost (line 45) | function addNotSlottedRightmost(sel) {
function wrapWithScope (line 57) | function wrapWithScope(selectorList, scopeSelector, excludeSlotted = tru...
function rewriteShadowCSS (line 84) | function rewriteShadowCSS(cssText, scopeSelector) {
function nextShadowScopeId (line 118) | function nextShadowScopeId(sessionCache) {
function extractShadowCSS (line 128) | function extractShadowCSS(sr) {
function injectScopedStyle (line 151) | function injectScopedStyle(hostClone, cssText, scopeId) {
function freezeImgSrcset (line 168) | function freezeImgSrcset(original, cloned) {
function collectCustomPropsFromCSS (line 186) | function collectCustomPropsFromCSS(cssText) {
function resolveCustomProp (line 202) | function resolveCustomProp(el, name) {
function buildSeedCustomPropsRule (line 224) | function buildSeedCustomPropsRule(hostEl, names, scopeSelector) {
function markSlottedSubtree (line 238) | function markSlottedSubtree(root) {
function getAccessibleIframeDocument (line 255) | async function getAccessibleIframeDocument(iframe, attempts = 3) {
function measureContentBox (line 274) | function measureContentBox(el) {
function getUnscaledDimensions (line 304) | function getUnscaledDimensions(el) {
function pinIframeViewport (line 360) | function pinIframeViewport(doc, w, h) {
function rasterizeIframe (line 379) | async function rasterizeIframe(iframe, sessionCache, options) {
function createCheckboxRadioReplacement (line 436) | function createCheckboxRadioReplacement(node) {
function blobUrlToDataUrl (line 545) | async function blobUrlToDataUrl(blobUrl) {
function replaceBlobUrlsInCssText (line 577) | async function replaceBlobUrlsInCssText(cssText) {
function isBlobUrl (line 591) | function isBlobUrl(u) {
function parseSrcset (line 595) | function parseSrcset(srcset) {
function stringifySrcset (line 607) | function stringifySrcset(parts) {
function resolveBlobUrlsInTree (line 611) | async function resolveBlobUrlsInTree(root, sessionCache = null) {
FILE: src/utils/css.js
constant NO_CAPTURE_TAGS (line 6) | const NO_CAPTURE_TAGS = new Set([
constant NO_DEFAULTS_TAGS (line 11) | const NO_DEFAULTS_TAGS = new Set([
function precacheCommonTags (line 29) | function precacheCommonTags() {
function getDefaultStyleForTag (line 46) | function getDefaultStyleForTag(tagName) {
constant NO_PAINT_TOKEN (line 93) | const NO_PAINT_TOKEN = /(?:^|-)(animation|transition)(?:-|$)/i
constant NO_PAINT_PREFIX (line 96) | const NO_PAINT_PREFIX = /^(--.+|view-timeline|scroll-timeline|animation-...
constant NO_PAINT_EXACT (line 99) | const NO_PAINT_EXACT = new Set([
function shouldIgnoreProp (line 126) | function shouldIgnoreProp(prop /*, tag */) {
function getStyleKey (line 142) | function getStyleKey(snapshot, tagName) {
function collectUsedTagNames (line 172) | function collectUsedTagNames(root) {
function generateDedupedBaseCSS (line 195) | function generateDedupedBaseCSS(usedTagNames) {
function generateCSSClasses (line 234) | function generateCSSClasses(styleMap) {
function getWindowForElement (line 252) | function getWindowForElement(el) {
function getStyle (line 278) | function getStyle(el, pseudo = null) {
function parseContent (line 346) | function parseContent(content) {
function snapshotComputedStyle (line 363) | function snapshotComputedStyle(style) {
function splitBackgroundImage (line 376) | function splitBackgroundImage(bg) {
FILE: src/utils/debug.js
function debugWarn (line 13) | function debugWarn(ctx, msg, err) {
FILE: src/utils/helpers.js
function extractURL (line 8) | function extractURL(value) {
function isIconFont (line 23) | function isIconFont(familyOrUrl) {
function stripTranslate (line 39) | function stripTranslate(transform) {
function safeEncodeURI (line 63) | function safeEncodeURI(uri) {
function resolveURL (line 74) | function resolveURL(url, base) {
FILE: src/utils/image.js
function inlineSingleBackgroundEntry (line 15) | async function inlineSingleBackgroundEntry(entry, options = {}) {
FILE: src/utils/prepare.helpers.js
function stabilizeLayout (line 10) | function stabilizeLayout(element) {
FILE: src/utils/transforms.helpers.js
function parseBoxShadow (line 13) | function parseBoxShadow(cs) {
function parseFilterBlur (line 37) | function parseFilterBlur(cs) {
function parseOutline (line 48) | function parseOutline(cs) {
function parseFilterDropShadows (line 59) | function parseFilterDropShadows(cs) {
function normalizeRootTransforms (line 98) | function normalizeRootTransforms(originalEl, cloneRoot) {
function bboxWithOriginFull (line 173) | function bboxWithOriginFull(w2, h2, M, ox2, oy2) {
function parseTransformOriginPx (line 200) | function parseTransformOriginPx(cs, w, h) {
function readIndividualTransforms (line 229) | function readIndividualTransforms(el) {
function getMeasureHost (line 304) | function getMeasureHost() {
function readTotalTransformMatrix (line 330) | function readTotalTransformMatrix(t) {
function hasBBoxAffectingTransform (line 348) | function hasBBoxAffectingTransform(el) {
function matrixFromComputed (line 372) | function matrixFromComputed(el) {
FILE: types/snapdom.d.ts
type RasterMime (line 15) | type RasterMime = "png" | "jpg" | "jpeg" | "webp";
type BlobType (line 16) | type BlobType = "svg" | RasterMime;
type IconFontMatcher (line 18) | type IconFontMatcher = string | RegExp;
type CachePolicy (line 19) | type CachePolicy = "disabled" | "full" | "auto" | "soft";
type LocalFont (line 25) | interface LocalFont {
type ExcludeFonts (line 32) | interface ExcludeFonts {
type SnapdomOptions (line 45) | interface SnapdomOptions {
type CaptureContext (line 120) | interface CaptureContext extends SnapdomOptions {
type Exporter (line 152) | type Exporter = (ctx: CaptureContext, opts?: any) => Promise<any>;
type ExportMap (line 155) | type ExportMap = Record<string, Exporter>;
type SnapdomPlugin (line 161) | interface SnapdomPlugin {
type PluginFactory (line 190) | type PluginFactory = (options?: any) => SnapdomPlugin;
type PluginUse (line 192) | type PluginUse =
type DownloadOptions (line 202) | interface DownloadOptions {
type BlobOptions (line 213) | interface BlobOptions {
type CaptureResult (line 220) | interface CaptureResult {
type PreCacheOptions (line 332) | interface PreCacheOptions {
Condensed preview — 121 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (884K chars).
[
{
"path": ".github/CODE_OF_CONDUCT.md",
"chars": 5222,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 1973,
"preview": "## 🤝 Contributing Guide\n\nThank you for your interest in contributing to **Snapdom**! Your help makes this project better"
},
{
"path": ".github/FUNDING.yml",
"chars": 852,
"preview": "# These are supported funding model platforms\n\ngithub: tinchox5\npatreon: # Replace with a single Patreon username\nopen_c"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 652,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: 'bug'\nassignees: ''\n\n---\n\n### Describe "
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 28,
"preview": "blank_issues_enabled: false\n"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 609,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: 'enhancement'\nassignees: ''\n\n---\n\n**"
},
{
"path": ".github/labels.yml",
"chars": 1784,
"preview": "# Priority labels\n- name: \"priority: critical\"\n color: \"B60205\"\n description: \"Core functionality broken, causes crash"
},
{
"path": ".github/scripts/classify-issues.js",
"chars": 5905,
"preview": "// .github/scripts/classify-issues.js\n// Classifies all open issues by importance and applies priority labels.\n// Usage:"
},
{
"path": ".github/scripts/update-contributors.js",
"chars": 1985,
"preview": "// .github/scripts/update-contributors.js\nimport { writeFileSync, readFileSync } from 'fs';\nimport https from 'https';\n\n"
},
{
"path": ".github/workflows/issue-triage.yml",
"chars": 3566,
"preview": "name: Auto-label Issues\n\non:\n issues:\n types: [opened, edited]\n\njobs:\n triage:\n runs-on: ubuntu-latest\n permi"
},
{
"path": ".github/workflows/label-sync.yml",
"chars": 519,
"preview": "name: Sync Labels\n\non:\n push:\n branches:\n - main\n paths:\n - '.github/labels.yml'\n workflow_dispatch:\n\n"
},
{
"path": ".github/workflows/update-contributors.yml",
"chars": 855,
"preview": "name: Update Contributors in READMES\n\non:\n push:\n branches:\n - main\n schedule:\n - cron: '0 0 * * 0'\n workf"
},
{
"path": ".gitignore",
"chars": 2205,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports"
},
{
"path": ".vscode/settings.json",
"chars": 40,
"preview": "{\n \"liveServer.settings.port\": 5501\n}"
},
{
"path": "CHANGELOG.md",
"chars": 57323,
"preview": "### Changelog\n\nAll notable changes to this project will be documented in this file. \n\n#### [v2.5.0](https://github.com/z"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2025 ZumerLab\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 38494,
"preview": "<p align=\"center\">\n <a href=\"http://zumerlab.github.io/snapdom\">\n <img src=\"https://raw.githubusercontent.com/zumerl"
},
{
"path": "README_CN.md",
"chars": 30611,
"preview": "<p align=\"center\">\n <a href=\"http://zumerlab.github.io/snapdom\">\n <img src=\"https://raw.githubusercontent.com/zumerl"
},
{
"path": "__tests__/api.preCache.more.test.js",
"chars": 4823,
"preview": "// __tests__/api.preCache.more.test.js\nimport { describe, it, expect, vi, beforeEach } from 'vitest'\n\nvi.mock('../src/ut"
},
{
"path": "__tests__/api.preCache.test.js",
"chars": 4220,
"preview": "import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport { preCache } from '../src/api/preCache.js'\nimport {"
},
{
"path": "__tests__/api.snapdom.more.test.js",
"chars": 1603,
"preview": "// __tests__/api.snapdom.more.test.js – snapdom.js extra coverage\nimport { describe, it, expect, beforeEach, afterEach, "
},
{
"path": "__tests__/api.snapdom.test.js",
"chars": 6469,
"preview": "import { describe, it, expect, vi } from 'vitest'\nimport { snapdom } from '../src/api/snapdom.js'\n\ndescribe('snapdom API"
},
{
"path": "__tests__/core.cache.test.js",
"chars": 6923,
"preview": "// __tests__/core.cache.test.js\nimport { describe, it, expect, beforeEach } from 'vitest'\nimport { cache, normalizeCache"
},
{
"path": "__tests__/core.capture.more.test.js",
"chars": 26451,
"preview": "import { describe, it, expect, vi, afterEach } from 'vitest'\n\n/**\n * Decode the SVG XML text from a data URL returned by"
},
{
"path": "__tests__/core.capture.test.js",
"chars": 1944,
"preview": "import { describe, it, expect, vi, afterEach } from 'vitest'\nimport { captureDOM } from '../src/core/capture.js'\n\nafterE"
},
{
"path": "__tests__/core.clone.more.test.js",
"chars": 24767,
"preview": "// __tests__/core.clone.more.test.js\nimport { describe, it, expect, vi, beforeEach } from 'vitest'\nimport { deepClone } "
},
{
"path": "__tests__/core.clone.test.js",
"chars": 3439,
"preview": "import { describe, it, expect } from 'vitest'\nimport { deepClone } from '../src/core/clone.js'\nimport { createContext } "
},
{
"path": "__tests__/core.context.test.js",
"chars": 5245,
"preview": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { createContext } from '../src/core/context."
},
{
"path": "__tests__/core.exporters.test.js",
"chars": 4710,
"preview": "// __tests__/core.exporters.test.js\nimport { describe, it, expect, beforeEach, vi } from 'vitest'\nimport {\n normalizeEx"
},
{
"path": "__tests__/core.prepare.test.js",
"chars": 18148,
"preview": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'\nimport { prepareClone } from '../src/core/prepa"
},
{
"path": "__tests__/cssTools.utils.test.js",
"chars": 1026,
"preview": "import { describe, it, expect } from 'vitest'\nimport { getStyleKey, collectUsedTagNames, getDefaultStyleForTag } from '."
},
{
"path": "__tests__/exporter.download.test.js",
"chars": 3429,
"preview": "// __tests__/exporter.download.test.js – download.js coverage\nimport { describe, it, expect, vi, beforeEach, afterEach }"
},
{
"path": "__tests__/exporter.toCanvas.more.test.js",
"chars": 2171,
"preview": "// __tests__/exporter.toCanvas.more.test.js – extra toCanvas coverage\nimport { describe, it, expect, vi, beforeEach, aft"
},
{
"path": "__tests__/exporter.toCanvas.test.js",
"chars": 2667,
"preview": "// __tests__/exporters.toCanvas.more.test.js\nimport { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'\n\n/"
},
{
"path": "__tests__/exporter.toImg.test.js",
"chars": 2268,
"preview": "// __tests__/exporter.toImg.test.js – toImg.js coverage\nimport { describe, it, expect, vi, beforeEach, afterEach } from "
},
{
"path": "__tests__/exporters.jpg-png-svg-webp.test.js",
"chars": 1670,
"preview": "// __tests__/exporters.jpg-png-svg-webp.test.js – direct exporter calls (0% → covered)\nimport { describe, it, expect } f"
},
{
"path": "__tests__/index.browser.test.js",
"chars": 185,
"preview": "import { it, expect } from 'vitest'\nimport * as snapdom from '../src/index.browser.js'\n\nit('should import the browser bu"
},
{
"path": "__tests__/module.background.test.js",
"chars": 1017,
"preview": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { inlineBackgroundImages } from '../src/modu"
},
{
"path": "__tests__/module.changeCSS.test.js",
"chars": 2049,
"preview": "// __tests__/module.changeCSS.test.js – freezeSticky coverage\nimport { describe, it, expect, beforeEach, afterEach } fro"
},
{
"path": "__tests__/module.counter.test.js",
"chars": 5070,
"preview": "// __tests__/module.counter.test.js – counter.js coverage\nimport { describe, it, expect, beforeEach, afterEach } from 'v"
},
{
"path": "__tests__/module.fonts.katex.test.js",
"chars": 5351,
"preview": "// __tests__/module.fonts.katex.test.js\nimport { describe, it, expect, vi, beforeEach } from 'vitest'\n\n/**\n * Test for i"
},
{
"path": "__tests__/module.fonts.more.more.test.js",
"chars": 8141,
"preview": "// __tests__/module.fonts.more.more.test.js\nimport { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'\n\n//"
},
{
"path": "__tests__/module.fonts.more.test.js",
"chars": 13397,
"preview": "// __tests__/module.fonts.more.test.js\nimport { describe, it, expect, vi, beforeEach } from 'vitest'\n\n/**\n * Mocks\n * - "
},
{
"path": "__tests__/module.fonts.test.js",
"chars": 7794,
"preview": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'\nimport { iconToImage, embedCustomFonts } from '"
},
{
"path": "__tests__/module.iconFonts.more.test.js",
"chars": 1677,
"preview": "// __tests__/module.iconFonts.more.test.js – extend iconFonts coverage (34% → higher)\nimport { describe, it, expect, bef"
},
{
"path": "__tests__/module.iconFonts.test.js",
"chars": 2040,
"preview": "// __tests__/module.iconFonts.test.js\nimport { describe, it, expect, beforeEach, vi } from 'vitest'\n\nlet mod // se setea"
},
{
"path": "__tests__/module.lineClamp.test.js",
"chars": 2996,
"preview": "// __tests__/module.lineClamp.test.js – lineClamp coverage\nimport { describe, it, expect, beforeEach, afterEach } from '"
},
{
"path": "__tests__/module.pseudo.test.js",
"chars": 14274,
"preview": "// __tests__/modules.pseudo.test.js\nimport { describe, it, expect, vi, beforeEach } from 'vitest'\nimport { inlinePseudoE"
},
{
"path": "__tests__/module.snapFetch.test.js",
"chars": 8886,
"preview": "// __tests__/module.snapFetch.test.js\n\nimport { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'\n\n/**\n * "
},
{
"path": "__tests__/module.styles.test.js",
"chars": 6796,
"preview": "import { describe, it, expect, vi, beforeEach } from 'vitest'\n\n// Carga fresca del módulo para que __wired se reinicie e"
},
{
"path": "__tests__/module.svg.test.js",
"chars": 6748,
"preview": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { inlineExternalDefsAndSymbols } from '../sr"
},
{
"path": "__tests__/modules.images.test.js",
"chars": 8674,
"preview": "// __tests__/modules.images.more.test.js\nimport { describe, it, expect, beforeEach, vi, afterEach} from 'vitest'\nimport "
},
{
"path": "__tests__/snapdom.attributes.test.js",
"chars": 1480,
"preview": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { snapdom } from '../src/index'\n\ndescribe('"
},
{
"path": "__tests__/snapdom.backgroundColor.test.js",
"chars": 1525,
"preview": "import { describe, it, expect, beforeEach } from 'vitest'\nimport { snapdom } from '../src/index'\n\ndescribe('snapdom.toJ"
},
{
"path": "__tests__/snapdom.benchmark.js",
"chars": 5102,
"preview": "import { bench, describe, afterEach } from 'vitest'\nimport { domToDataUrl } from 'https://unpkg.com/modern-screenshot'\ni"
},
{
"path": "__tests__/snapdom.complex.benchmark.js",
"chars": 4278,
"preview": "import { bench, describe, afterEach } from 'vitest'\nimport { domToDataUrl } from 'https://unpkg.com/modern-screenshot'\ni"
},
{
"path": "__tests__/snapdom.delete.test.js",
"chars": 1860,
"preview": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { snapdom } from '../src/index'\n\ndescribe('"
},
{
"path": "__tests__/snapdom.precache.perf.test.js",
"chars": 3953,
"preview": "import { describe, test, expect, afterEach, afterAll, beforeEach } from 'vitest'\nimport { snapdom, preCache } from '../s"
},
{
"path": "__tests__/snapdom.test.js",
"chars": 2550,
"preview": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { snapdom } from '../src/index'\n\ndescribe('s"
},
{
"path": "__tests__/snapdom.vs.htm2canvas.outputfilesize.test.js",
"chars": 1967,
"preview": "import { describe, it, beforeEach, afterEach, afterAll, expect } from 'vitest'\n// ✅ variante ESM en jsDelivr (también po"
},
{
"path": "__tests__/snapdom.vs.modernscreenshot.outputfilesize.test.js",
"chars": 1514,
"preview": "import { describe, it, beforeEach, afterEach, afterAll } from 'vitest'\nimport { domToDataUrl} from 'https://unpkg.com/mo"
},
{
"path": "__tests__/three-shake.test.js",
"chars": 3505,
"preview": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { snapdom } from '../src/index'\n\ndescribe('s"
},
{
"path": "__tests__/utils.browser.more.test.js",
"chars": 1602,
"preview": "// __tests__/utils.browser.more.test.js – browser.js extra coverage\nimport { describe, it, expect, vi, beforeEach, after"
},
{
"path": "__tests__/utils.browser.test.js",
"chars": 1034,
"preview": "import { describe, it, expect} from 'vitest'\nimport { isSafari, idle } from '../src/utils'\n\ndescribe('isSafari', () => {"
},
{
"path": "__tests__/utils.capture.helpers.test.js",
"chars": 6488,
"preview": "// __tests__/utils.capture.helpers.test.js – 51% → higher coverage\nimport { describe, it, expect, beforeEach, afterEach "
},
{
"path": "__tests__/utils.css.test.js",
"chars": 3034,
"preview": "import { describe, it, expect } from 'vitest'\nimport { getStyle, parseContent, snapshotComputedStyle, stripTranslate, sh"
},
{
"path": "__tests__/utils.helpers.test.js",
"chars": 2141,
"preview": "import { describe, it, expect } from 'vitest'\nimport { extractURL, isIconFont, stripTranslate, safeEncodeURI, resolveURL"
},
{
"path": "__tests__/utils.image.test.js",
"chars": 8418,
"preview": "// __tests__/utils.image.more.test.js\nimport { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'\nimport { "
},
{
"path": "__tests__/utils.transforms.helpers.test.js",
"chars": 6271,
"preview": "// __tests__/utils.transforms.helpers.test.js – transforms.helpers.js coverage\nimport { describe, it, expect, beforeEach"
},
{
"path": "docs/CNAME",
"chars": 11,
"preview": "snapdom.dev"
},
{
"path": "docs/assets/favicon/site.webmanifest",
"chars": 436,
"preview": "{\n \"name\": \"MyWebSite\",\n \"short_name\": \"MySite\",\n \"icons\": [\n {\n \"src\": \"/web-app-manifest-192x192.png\",\n "
},
{
"path": "docs/index.html",
"chars": 81381,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <!-- === Meta básicos === -->\n <meta charset=\"UTF-8\" />\n <meta name=\"viewpor"
},
{
"path": "docs/labs.html",
"chars": 28630,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta name=\"viewport\" content=\"width=device-width, i"
},
{
"path": "esbuild.config.mjs",
"chars": 1755,
"preview": "import { build } from 'esbuild'\nimport { readFileSync, rmSync } from 'node:fs'\n\n/** @type {import('esbuild').BuildOption"
},
{
"path": "eslint.config.cjs",
"chars": 812,
"preview": "const js = require(\"@eslint/js\")\nconst globals = require(\"globals\")\n\nmodule.exports = [\n js.configs.recommended,\n {\n "
},
{
"path": "package.json",
"chars": 2269,
"preview": "{\n \"name\": \"@zumer/snapdom\",\n \"version\": \"2.5.0\",\n \"description\": \"snapDOM captures HTML elements to images with exce"
},
{
"path": "plugins/html-in-canvas.js",
"chars": 4011,
"preview": "/**\n * Experimental plugin for WICG html-in-canvas (issue #172).\n * Uses drawElementImage() to render the snapdom clone "
},
{
"path": "src/api/preCache.js",
"chars": 5390,
"preview": "// src/api/preCache.js\nimport { getStyle, inlineSingleBackgroundEntry, precacheCommonTags, isSafari } from '../utils'\nim"
},
{
"path": "src/api/snapdom.js",
"chars": 14748,
"preview": "// src/api/snapdom.js\nimport { captureDOM } from '../core/capture.js'\nimport { extendIconFonts } from '../modules/iconFo"
},
{
"path": "src/core/cache.js",
"chars": 3090,
"preview": "/** Max entries before evicting oldest (FIFO). Keeps lib lightweight, avoids memory leaks. */\nconst MAX_IMAGE = 100\ncons"
},
{
"path": "src/core/capture.js",
"chars": 14898,
"preview": "/**\n * Core logic for capturing DOM elements as SVG data URLs.\n * @module capture\n */\n\nimport { prepareClone } from './p"
},
{
"path": "src/core/clone.js",
"chars": 13636,
"preview": "/**\n * Deep cloning utilities for DOM elements, including styles and shadow DOM.\n * @module clone\n */\n\nimport { inlineAl"
},
{
"path": "src/core/context.js",
"chars": 3805,
"preview": "/**\n * @typedef {\"disabled\"|\"full\"|\"auto\"|\"soft\"} CachePolicy\n */\n\nimport { normalizeCachePolicy } from './cache.js'\n\n/*"
},
{
"path": "src/core/exporters.js",
"chars": 2938,
"preview": "/**\n * Exporters registry (by format).\n * An exporter declares supported formats and an export() method:\n *\n * interface"
},
{
"path": "src/core/plugins.js",
"chars": 5335,
"preview": "/**\n * Plugin core for SnapDOM (minimalistic, local-first compatible).\n *\n * Public hooks:\n * - beforeSnap(context)\n * "
},
{
"path": "src/core/prepare.js",
"chars": 5857,
"preview": "/**\n * Prepares a deep clone of an element, inlining pseudo-elements and generating CSS classes.\n * @module prepare\n */\n"
},
{
"path": "src/exporters/download.js",
"chars": 2553,
"preview": "// src/exporters/download.js\nimport { toBlob } from './toBlob.js'\nimport { toCanvas } from './toCanvas.js'\nimport { isIO"
},
{
"path": "src/exporters/toBlob.js",
"chars": 914,
"preview": "// src/exporters/toBlob.js\nimport { toCanvas } from './toCanvas.js'\n\n/**\n * Converts the rendered output to a Blob.\n * @"
},
{
"path": "src/exporters/toCanvas.js",
"chars": 5833,
"preview": "// src/exporters/toCanvas.js\nimport { isSafari } from '../utils/browser'\n\n/**\n * Converts a data URL to a Canvas element"
},
{
"path": "src/exporters/toImg.js",
"chars": 2356,
"preview": "// src/exporters/toImg.js\nimport { isSafari, debugWarn } from '../utils'\nimport { rasterize } from '../modules/rasterize"
},
{
"path": "src/exporters/toJpg.js",
"chars": 472,
"preview": "import { rasterize } from '../modules/rasterize.js'\nimport { captureDOM } from '../core/capture.js'\n\nexport async functi"
},
{
"path": "src/exporters/toPng.js",
"chars": 518,
"preview": "// PNG via rasterize; acepta Element o dataURL (SVG)\nimport { rasterize } from '../modules/rasterize.js'\nimport { captur"
},
{
"path": "src/exporters/toSvg.js",
"chars": 110,
"preview": "// Reexpone el toSvg que ya definís en toImg.js para habilitar el sub-path\nexport { toSvg } from './toImg.js'\n"
},
{
"path": "src/exporters/toWebp.js",
"chars": 293,
"preview": "import { rasterize } from '../modules/rasterize.js'\nimport { captureDOM } from '../core/capture.js'\n\nexport async functi"
},
{
"path": "src/index.browser.js",
"chars": 266,
"preview": "/**\n * Entry point for snapDOM library exports.\n *\n * @file index.browser.js\n */\n\nimport { snapdom } from './api/snapdom"
},
{
"path": "src/index.js",
"chars": 162,
"preview": "/**\n * Entry point for snapDOM library exports.\n *\n * @file index.js\n */\n\nexport { snapdom } from './api/snapdom.js'\nexp"
},
{
"path": "src/modules/CSSVar.js",
"chars": 4187,
"preview": "// src/utils/resolveCSSVars.js\n\n/** Props donde típicamente aparece var() y conviene “materializar” si difieren del base"
},
{
"path": "src/modules/background.js",
"chars": 6094,
"preview": "/**\n * Utilities for inlining background images as data URLs.\n * @module background\n */\n\nimport { getStyle, inlineSingle"
},
{
"path": "src/modules/changeCSS.js",
"chars": 4845,
"preview": "/**\n * Freeze sticky elements by converting them to absolutely-positioned overlays.\n * Also creates an invisible absolut"
},
{
"path": "src/modules/counter.js",
"chars": 11280,
"preview": "import { cache } from '../core/cache'\n\n/**\n * Lightweight CSS counter resolver for SnapDOM.\n * - Supports counter(name[,"
},
{
"path": "src/modules/fonts.js",
"chars": 37234,
"preview": "/**\n * Utilities for handling and embedding web fonts and icon fonts.\n * @module fonts\n */\n\nimport { extractURL } from '"
},
{
"path": "src/modules/iconFonts.js",
"chars": 9985,
"preview": "// iconFonts.js\n\n// ---------------------------------------------------------------------------\n// Detection / configura"
},
{
"path": "src/modules/images.js",
"chars": 5047,
"preview": "/**\n * Utilities for inlining <img> and SVG <image> elements as data URLs or placeholders.\n * Fixes #341: SVG <image hre"
},
{
"path": "src/modules/lineClamp.js",
"chars": 3192,
"preview": "// src/core/lineClamp.js\n\n/**\n * Apply line-clamp to element AND all descendants that have -webkit-line-clamp.\n * Fixes "
},
{
"path": "src/modules/pseudo.js",
"chars": 22395,
"preview": "/**\n * Utilities for inlining ::before and ::after pseudo-elements.\n * @module pseudo\n */\n\nimport {\n getStyle,\n snapsh"
},
{
"path": "src/modules/rasterize.js",
"chars": 696,
"preview": "// src/exporters/rasterize.js\nimport { toCanvas } from '../exporters/toCanvas.js'\n\n/**\n * Converts to an HTMLImageElemen"
},
{
"path": "src/modules/snapFetch.js",
"chars": 12917,
"preview": "// src/modules/snapFetch.js\nimport { safeEncodeURI } from '../utils/helpers.js'\n\n/**\n * snapFetch — unified fetch for Sn"
},
{
"path": "src/modules/styles.js",
"chars": 12511,
"preview": "import { getStyleKey, shouldIgnoreProp } from '../utils/index.js'\nimport { cache } from '../core/cache.js'\n\nconst snapsh"
},
{
"path": "src/modules/svgDefs.js",
"chars": 7378,
"preview": "/**\n * Inline external <defs> and <symbol> dependencies needed by an SVG subtree (or multiple SVGs),\n * so that serializ"
},
{
"path": "src/utils/browser.js",
"chars": 2425,
"preview": "/**\n * Creates a promise that resolves after the specified delay\n * @param {number} [ms=0] - Milliseconds to delay\n * @r"
},
{
"path": "src/utils/capture.helpers.js",
"chars": 11596,
"preview": "/**\n * Helper utilities for DOM capture operations\n * @module utils/capture.helpers\n */\n\nimport { debugWarn } from './in"
},
{
"path": "src/utils/clone.helpers.js",
"chars": 24507,
"preview": "/**\n * Helper utilities for DOM cloning operations\n * @module utils/clone.helpers\n */\n\nimport { idle, debugWarn } from '"
},
{
"path": "src/utils/css.js",
"chars": 12870,
"preview": "// -----------------------------------------------------------------------------\n// Central single-source-of-truth sets\n"
},
{
"path": "src/utils/debug.js",
"chars": 681,
"preview": "/**\n * Debug logging when options.debug is true.\n * Keeps production quiet; opt-in for troubleshooting.\n * @module utils"
},
{
"path": "src/utils/helpers.js",
"chars": 2318,
"preview": "/**\n * Extracts a URL from a CSS value like background-image.\n *\n * @param {string} value - The CSS value\n * @returns {s"
},
{
"path": "src/utils/image.js",
"chars": 2231,
"preview": "import { cache } from '../core/cache'\nimport { extractURL, safeEncodeURI, resolveURL } from './helpers'\nimport { snapFet"
},
{
"path": "src/utils/index.js",
"chars": 515,
"preview": "export { inlineSingleBackgroundEntry } from './image.js'\nexport { precacheCommonTags, getDefaultStyleForTag, getStyleKey"
},
{
"path": "src/utils/prepare.helpers.js",
"chars": 731,
"preview": "/**\n * Helper utilities for preparing DOM clones\n * @module utils/prepare.helpers\n */\n\n/**\n * Stabilize layout by adding"
},
{
"path": "src/utils/transforms.helpers.js",
"chars": 12881,
"preview": "/**\n * Helper utilities for transform and geometry calculations\n * @module utils/transforms.helpers\n */\n\nimport { limitD"
},
{
"path": "types/snapdom.d.ts",
"chars": 11305,
"preview": "/**\n * snapDOM – ultra-fast DOM-to-image capture\n * TypeScript definitions (v1.9.14)\n *\n * Notes:\n * - Style compression"
},
{
"path": "vitest.config.js",
"chars": 431,
"preview": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n test: {\n browser: {\n enabled: true"
}
]
About this extraction
This page contains the full source code of the zumerlab/snapdom GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 121 files (830.3 KB), approximately 237.5k tokens, and a symbol index with 359 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.