mnfst 0.5.78 → 0.5.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/manifest.export.js +96 -76
- package/package.json +1 -1
package/lib/manifest.export.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* Supported formats: pdf (default), png, jpeg, webp, csv, json.
|
|
28
28
|
*
|
|
29
|
-
* Library dependencies (
|
|
29
|
+
* Library dependencies (html2canvas-pro, jsPDF) are loaded lazily on first
|
|
30
30
|
* use from jsDelivr; pages that never export pay nothing.
|
|
31
31
|
*
|
|
32
32
|
* Elements with `data-no-export` are excluded from visual snapshots.
|
|
@@ -52,7 +52,7 @@ function initializeExportPlugin() {
|
|
|
52
52
|
urlTriggerFired = true;
|
|
53
53
|
const fmt = isKnownFormat(paramValue) ? paramValue : format;
|
|
54
54
|
setTimeout(() => {
|
|
55
|
-
runExport(fmt, opts, opts
|
|
55
|
+
runExport(fmt, opts, resolveFilename(el, opts, fmt))
|
|
56
56
|
.catch((err) => emitError(fmt, err));
|
|
57
57
|
}, Number(opts.delay) > 0 ? Number(opts.delay) : 0);
|
|
58
58
|
}
|
|
@@ -87,7 +87,7 @@ function initializeExportPlugin() {
|
|
|
87
87
|
if (isAnchor && href && href.startsWith('#')) {
|
|
88
88
|
const onClick = async (e) => {
|
|
89
89
|
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
|
90
|
-
const filename = opts
|
|
90
|
+
const filename = resolveFilename(el, opts, format);
|
|
91
91
|
try {
|
|
92
92
|
await runExport(format, { ...opts, target: href }, filename);
|
|
93
93
|
} catch (err) {
|
|
@@ -102,7 +102,7 @@ function initializeExportPlugin() {
|
|
|
102
102
|
// ----- Default: click anywhere else triggers export -----
|
|
103
103
|
const onClick = async (e) => {
|
|
104
104
|
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
|
105
|
-
const filename = opts
|
|
105
|
+
const filename = resolveFilename(el, opts, format);
|
|
106
106
|
try {
|
|
107
107
|
await runExport(format, opts, filename);
|
|
108
108
|
} catch (err) {
|
|
@@ -150,6 +150,24 @@ function initializeExportPlugin() {
|
|
|
150
150
|
return `export-${ts}.${ext}`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Filename resolution precedence (highest first):
|
|
154
|
+
// 1. opts.filename from the directive's object expression
|
|
155
|
+
// 2. the standard HTML `download` attribute on an anchor host
|
|
156
|
+
// 3. a `data-filename` attribute on any host element
|
|
157
|
+
// 4. a timestamped default based on the format
|
|
158
|
+
function resolveFilename(el, opts, format) {
|
|
159
|
+
if (opts && opts.filename) return String(opts.filename);
|
|
160
|
+
if (el && el.tagName === 'A') {
|
|
161
|
+
const dl = el.getAttribute('download');
|
|
162
|
+
if (dl) return dl;
|
|
163
|
+
}
|
|
164
|
+
if (el && typeof el.getAttribute === 'function') {
|
|
165
|
+
const df = el.getAttribute('data-filename');
|
|
166
|
+
if (df) return df;
|
|
167
|
+
}
|
|
168
|
+
return defaultFilename(format);
|
|
169
|
+
}
|
|
170
|
+
|
|
153
171
|
// Transparent 1×1 PNG, used as a fallback when an inline image fails to fetch.
|
|
154
172
|
// Without it, a single CORS or 404 image would reject the entire snapshot.
|
|
155
173
|
const TRANSPARENT_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=';
|
|
@@ -208,21 +226,28 @@ function initializeExportPlugin() {
|
|
|
208
226
|
}
|
|
209
227
|
|
|
210
228
|
function snapshotOptions(opts) {
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return
|
|
229
|
+
// ignoreElements callback for html2canvas-pro — exclude opt-out elements.
|
|
230
|
+
// (Inverse of modern-screenshot's `filter`: return true to SKIP this node.)
|
|
231
|
+
const ignoreElements = (node) => {
|
|
232
|
+
return !!(node && node.nodeType === 1
|
|
233
|
+
&& node.hasAttribute && node.hasAttribute('data-no-export'));
|
|
215
234
|
};
|
|
216
235
|
const out = {
|
|
217
236
|
scale: Number(opts.resolution) > 0 ? Number(opts.resolution) : 2,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
ignoreElements,
|
|
238
|
+
useCORS: true,
|
|
239
|
+
allowTaint: false,
|
|
240
|
+
// Bound how long we wait per cross-origin image. Failed fetches
|
|
241
|
+
// produce a missing image rather than rejecting the whole snapshot.
|
|
242
|
+
imageTimeout: 5000,
|
|
243
|
+
logging: false,
|
|
244
|
+
// Pin the capture origin to (0, 0) so the snapshot starts at the
|
|
245
|
+
// document top regardless of the user's current scroll position.
|
|
246
|
+
// Without this, html2canvas defaults scrollY to window.pageYOffset,
|
|
247
|
+
// which clips the snapshot to "below the current scroll" — anything
|
|
248
|
+
// above the fold renders as blank white space.
|
|
249
|
+
scrollX: 0,
|
|
250
|
+
scrollY: 0,
|
|
226
251
|
};
|
|
227
252
|
if (opts.backgroundColor) out.backgroundColor = opts.backgroundColor;
|
|
228
253
|
if (opts.width) out.width = Number(opts.width);
|
|
@@ -230,61 +255,54 @@ function initializeExportPlugin() {
|
|
|
230
255
|
return out;
|
|
231
256
|
}
|
|
232
257
|
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
async function
|
|
258
|
+
// Await all <img> loads inside the target so freshly-mounted images don't
|
|
259
|
+
// appear blank in the snapshot. Bounded by a timeout — a single slow image
|
|
260
|
+
// shouldn't stall the export indefinitely.
|
|
261
|
+
async function waitForImages(target, timeoutMs = 5000) {
|
|
262
|
+
if (!target || typeof target.querySelectorAll !== 'function') return;
|
|
263
|
+
const imgs = Array.from(target.querySelectorAll('img'));
|
|
264
|
+
const pending = imgs.filter((img) => !img.complete || img.naturalWidth === 0);
|
|
265
|
+
if (pending.length === 0) return;
|
|
266
|
+
await Promise.race([
|
|
267
|
+
Promise.all(pending.map((img) => new Promise((resolve) => {
|
|
268
|
+
img.addEventListener('load', resolve, { once: true });
|
|
269
|
+
img.addEventListener('error', resolve, { once: true });
|
|
270
|
+
}))),
|
|
271
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// html2canvas-pro paints directly to canvas via computed styles, so the
|
|
276
|
+
// SVG-foreignObject failure modes (oversized SVGs, @layer/oklch parsing
|
|
277
|
+
// issues) don't apply. The fallback path is retained for the rare case
|
|
278
|
+
// where the library throws (e.g. tainted canvas on cross-origin images).
|
|
279
|
+
async function snapshotToCanvas(lib, target, so) {
|
|
237
280
|
try {
|
|
238
|
-
return await lib
|
|
281
|
+
return await lib(target, so);
|
|
239
282
|
} catch (err) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
font: false,
|
|
245
|
-
};
|
|
246
|
-
return await lib[fn](target, safer);
|
|
283
|
+
// Retry once with allowTaint enabled and a lower scale — preserves
|
|
284
|
+
// the snapshot even when an image fails cross-origin policy.
|
|
285
|
+
const safer = { ...so, allowTaint: true, useCORS: false, scale: Math.min(so.scale || 2, 1) };
|
|
286
|
+
return await lib(target, safer);
|
|
247
287
|
}
|
|
248
288
|
}
|
|
249
289
|
|
|
250
|
-
// Chromium and Safari reject SVG-image decoding past roughly 16,000px on
|
|
251
|
-
// either axis. A long article at pixelRatio: 2 easily exceeds that. When
|
|
252
|
-
// the target is the whole body we cap dimensions so the assembled SVG
|
|
253
|
-
// stays decodable; explicit targets keep their natural size.
|
|
254
|
-
const SAFE_MAX_DIMENSION = 14000;
|
|
255
|
-
|
|
256
|
-
function applySafeDimensions(target, so) {
|
|
257
|
-
const isWholeBody = target === document.body || target === document.documentElement;
|
|
258
|
-
if (!isWholeBody) return so;
|
|
259
|
-
const rect = target.getBoundingClientRect();
|
|
260
|
-
const width = Math.max(rect.width, target.scrollWidth || 0);
|
|
261
|
-
const height = Math.max(rect.height, target.scrollHeight || 0);
|
|
262
|
-
const ratio = so.pixelRatio || 2;
|
|
263
|
-
const limit = SAFE_MAX_DIMENSION / ratio;
|
|
264
|
-
const out = { ...so };
|
|
265
|
-
if (width > limit) out.canvasWidth = Math.floor(limit);
|
|
266
|
-
if (height > limit) out.canvasHeight = Math.floor(limit);
|
|
267
|
-
return out;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
290
|
async function exportImage(opts, filename, ext) {
|
|
271
291
|
const lib = await loadSnapshotLib();
|
|
272
292
|
const target = resolveTarget(opts);
|
|
273
|
-
const so =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
else if (ext === 'jpeg') {
|
|
277
|
-
if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
|
|
278
|
-
so.quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
|
|
279
|
-
? Number(opts.quality)
|
|
280
|
-
: 0.95;
|
|
281
|
-
dataUrl = await snapshotWithFallback(lib, 'domToJpeg', target, so);
|
|
282
|
-
} else if (ext === 'webp') {
|
|
283
|
-
so.quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
|
|
284
|
-
? Number(opts.quality)
|
|
285
|
-
: 0.95;
|
|
286
|
-
dataUrl = await snapshotWithFallback(lib, 'domToWebp', target, so);
|
|
293
|
+
const so = snapshotOptions(opts);
|
|
294
|
+
if ((ext === 'jpeg' || ext === 'webp') && !so.backgroundColor) {
|
|
295
|
+
so.backgroundColor = effectivePageBackground();
|
|
287
296
|
}
|
|
297
|
+
await waitForImages(target);
|
|
298
|
+
const canvas = await snapshotToCanvas(lib, target, so);
|
|
299
|
+
const quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
|
|
300
|
+
? Number(opts.quality)
|
|
301
|
+
: 0.95;
|
|
302
|
+
let dataUrl;
|
|
303
|
+
if (ext === 'png') dataUrl = canvas.toDataURL('image/png');
|
|
304
|
+
else if (ext === 'jpeg') dataUrl = canvas.toDataURL('image/jpeg', quality);
|
|
305
|
+
else if (ext === 'webp') dataUrl = canvas.toDataURL('image/webp', quality);
|
|
288
306
|
triggerDownload(dataUrl, filename);
|
|
289
307
|
}
|
|
290
308
|
|
|
@@ -293,17 +311,18 @@ function initializeExportPlugin() {
|
|
|
293
311
|
// own @media print CSS — far more reliable than rasterizing a long page
|
|
294
312
|
// and embedding it as a single image. The user sees the print dialog and
|
|
295
313
|
// picks "Save as PDF" (or any installed PDF printer). Element-scoped PDFs
|
|
296
|
-
// continue to use
|
|
297
|
-
// target sizes.
|
|
314
|
+
// continue to use html2canvas-pro + jsPDF.
|
|
298
315
|
async function exportPdf(opts, filename) {
|
|
299
316
|
if (!opts.target) {
|
|
300
317
|
return printToPdf(filename);
|
|
301
318
|
}
|
|
302
319
|
const [imgLib, jsPDFCtor] = await Promise.all([loadSnapshotLib(), loadJsPDF()]);
|
|
303
320
|
const target = resolveTarget(opts);
|
|
304
|
-
const so =
|
|
321
|
+
const so = snapshotOptions(opts);
|
|
305
322
|
if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
|
|
306
|
-
|
|
323
|
+
await waitForImages(target);
|
|
324
|
+
const canvas = await snapshotToCanvas(imgLib, target, so);
|
|
325
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
307
326
|
const img = await loadImage(dataUrl);
|
|
308
327
|
const orientation = img.width > img.height ? 'landscape' : 'portrait';
|
|
309
328
|
const pdf = new jsPDFCtor({ orientation, unit: 'pt', format: opts.pageSize || 'a4' });
|
|
@@ -429,21 +448,22 @@ function initializeExportPlugin() {
|
|
|
429
448
|
|
|
430
449
|
// Lazy library loaders — cached promises.
|
|
431
450
|
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
451
|
+
// html2canvas-pro paints directly to a canvas by walking computed styles,
|
|
452
|
+
// rather than cloning the DOM into an SVG foreignObject. That means it has
|
|
453
|
+
// no failure modes around @layer rules, oklch() colors, url(data:...) in
|
|
454
|
+
// cross-origin stylesheets, or oversized SVG decoding — the cases that
|
|
455
|
+
// sank both html-to-image and modern-screenshot on real-world pages.
|
|
456
|
+
// ESM-only, so we load via dynamic import() from jsDelivr.
|
|
438
457
|
let snapshotLibPromise = null;
|
|
439
458
|
function loadSnapshotLib() {
|
|
440
459
|
if (snapshotLibPromise) return snapshotLibPromise;
|
|
441
|
-
snapshotLibPromise = import('https://cdn.jsdelivr.net/npm/
|
|
460
|
+
snapshotLibPromise = import('https://cdn.jsdelivr.net/npm/html2canvas-pro@2/dist/html2canvas-pro.esm.js')
|
|
442
461
|
.then((mod) => {
|
|
443
|
-
|
|
444
|
-
|
|
462
|
+
const fn = mod.default || mod.html2canvas;
|
|
463
|
+
if (typeof fn !== 'function') {
|
|
464
|
+
throw new Error('html2canvas-pro failed to load (missing default export)');
|
|
445
465
|
}
|
|
446
|
-
return
|
|
466
|
+
return fn;
|
|
447
467
|
})
|
|
448
468
|
.catch((err) => {
|
|
449
469
|
snapshotLibPromise = null; // allow retry on next call
|