mnfst 0.5.78 → 0.5.79

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.
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * Supported formats: pdf (default), png, jpeg, webp, csv, json.
28
28
  *
29
- * Library dependencies (modern-screenshot, jsPDF) are loaded lazily on first
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.filename || defaultFilename(fmt))
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.filename || defaultFilename(format);
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.filename || defaultFilename(format);
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,21 @@ function initializeExportPlugin() {
208
226
  }
209
227
 
210
228
  function snapshotOptions(opts) {
211
- // Filter callback for modern-screenshot — exclude opt-out elements.
212
- const filter = (node) => {
213
- if (!node || node.nodeType !== 1) return true;
214
- return !(node.hasAttribute && node.hasAttribute('data-no-export'));
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
- filter,
219
- fetch: {
220
- // Cross-origin assets without CORS headers fall back to this
221
- // transparent pixel instead of rejecting the entire snapshot.
222
- placeholderImage: TRANSPARENT_PIXEL,
223
- // Always bypass the cache so we get fresh, CORS-correct copies.
224
- bypassingCache: true,
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,
226
244
  };
227
245
  if (opts.backgroundColor) out.backgroundColor = opts.backgroundColor;
228
246
  if (opts.width) out.width = Number(opts.width);
@@ -230,61 +248,36 @@ function initializeExportPlugin() {
230
248
  return out;
231
249
  }
232
250
 
233
- // Run a modern-screenshot call once. If it rejects with a raw Event (the
234
- // assembled SVG failed to decode), retry once with safer options before
235
- // propagating. Most real-world failures recover on the second attempt.
236
- async function snapshotWithFallback(lib, fn, target, so) {
251
+ // html2canvas-pro paints directly to canvas via computed styles, so the
252
+ // SVG-foreignObject failure modes (oversized SVGs, @layer/oklch parsing
253
+ // issues) don't apply. The fallback path is retained for the rare case
254
+ // where the library throws (e.g. tainted canvas on cross-origin images).
255
+ async function snapshotToCanvas(lib, target, so) {
237
256
  try {
238
- return await lib[fn](target, so);
257
+ return await lib(target, so);
239
258
  } catch (err) {
240
- if (err && err.message) throw err; // a real Errordon't mask it
241
- const safer = {
242
- ...so,
243
- scale: Math.min(so.scale || 2, 1),
244
- font: false,
245
- };
246
- return await lib[fn](target, safer);
259
+ // Retry once with allowTaint enabled and a lower scalepreserves
260
+ // the snapshot even when an image fails cross-origin policy.
261
+ const safer = { ...so, allowTaint: true, useCORS: false, scale: Math.min(so.scale || 2, 1) };
262
+ return await lib(target, safer);
247
263
  }
248
264
  }
249
265
 
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
266
  async function exportImage(opts, filename, ext) {
271
267
  const lib = await loadSnapshotLib();
272
268
  const target = resolveTarget(opts);
273
- const so = applySafeDimensions(target, snapshotOptions(opts));
274
- let dataUrl;
275
- if (ext === 'png') dataUrl = await snapshotWithFallback(lib, 'domToPng', target, so);
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);
269
+ const so = snapshotOptions(opts);
270
+ if ((ext === 'jpeg' || ext === 'webp') && !so.backgroundColor) {
271
+ so.backgroundColor = effectivePageBackground();
287
272
  }
273
+ const canvas = await snapshotToCanvas(lib, target, so);
274
+ const quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
275
+ ? Number(opts.quality)
276
+ : 0.95;
277
+ let dataUrl;
278
+ if (ext === 'png') dataUrl = canvas.toDataURL('image/png');
279
+ else if (ext === 'jpeg') dataUrl = canvas.toDataURL('image/jpeg', quality);
280
+ else if (ext === 'webp') dataUrl = canvas.toDataURL('image/webp', quality);
288
281
  triggerDownload(dataUrl, filename);
289
282
  }
290
283
 
@@ -293,17 +286,17 @@ function initializeExportPlugin() {
293
286
  // own @media print CSS — far more reliable than rasterizing a long page
294
287
  // and embedding it as a single image. The user sees the print dialog and
295
288
  // picks "Save as PDF" (or any installed PDF printer). Element-scoped PDFs
296
- // continue to use modern-screenshot + jsPDF, which is well-behaved at small
297
- // target sizes.
289
+ // continue to use html2canvas-pro + jsPDF.
298
290
  async function exportPdf(opts, filename) {
299
291
  if (!opts.target) {
300
292
  return printToPdf(filename);
301
293
  }
302
294
  const [imgLib, jsPDFCtor] = await Promise.all([loadSnapshotLib(), loadJsPDF()]);
303
295
  const target = resolveTarget(opts);
304
- const so = applySafeDimensions(target, snapshotOptions(opts));
296
+ const so = snapshotOptions(opts);
305
297
  if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
306
- const dataUrl = await snapshotWithFallback(imgLib, 'domToPng', target, so);
298
+ const canvas = await snapshotToCanvas(imgLib, target, so);
299
+ const dataUrl = canvas.toDataURL('image/png');
307
300
  const img = await loadImage(dataUrl);
308
301
  const orientation = img.width > img.height ? 'landscape' : 'portrait';
309
302
  const pdf = new jsPDFCtor({ orientation, unit: 'pt', format: opts.pageSize || 'a4' });
@@ -429,21 +422,22 @@ function initializeExportPlugin() {
429
422
 
430
423
  // Lazy library loaders — cached promises.
431
424
 
432
- // modern-screenshot is an actively maintained TypeScript rewrite of
433
- // html-to-image with proper CSS AST parsing. It handles @layer rules,
434
- // CSS custom properties, modern color functions, and url(data:...) values
435
- // inside cross-origin stylesheets all of which html-to-image mishandles.
436
- // It ships ESM-only, so we load via dynamic import() rather than a script
437
- // tag. jsDelivr serves the ESM bundle directly.
425
+ // html2canvas-pro paints directly to a canvas by walking computed styles,
426
+ // rather than cloning the DOM into an SVG foreignObject. That means it has
427
+ // no failure modes around @layer rules, oklch() colors, url(data:...) in
428
+ // cross-origin stylesheets, or oversized SVG decoding the cases that
429
+ // sank both html-to-image and modern-screenshot on real-world pages.
430
+ // ESM-only, so we load via dynamic import() from jsDelivr.
438
431
  let snapshotLibPromise = null;
439
432
  function loadSnapshotLib() {
440
433
  if (snapshotLibPromise) return snapshotLibPromise;
441
- snapshotLibPromise = import('https://cdn.jsdelivr.net/npm/modern-screenshot@4/dist/index.mjs')
434
+ snapshotLibPromise = import('https://cdn.jsdelivr.net/npm/html2canvas-pro@2/dist/html2canvas-pro.esm.js')
442
435
  .then((mod) => {
443
- if (!mod || typeof mod.domToPng !== 'function') {
444
- throw new Error('modern-screenshot failed to load (missing expected exports)');
436
+ const fn = mod.default || mod.html2canvas;
437
+ if (typeof fn !== 'function') {
438
+ throw new Error('html2canvas-pro failed to load (missing default export)');
445
439
  }
446
- return mod;
440
+ return fn;
447
441
  })
448
442
  .catch((err) => {
449
443
  snapshotLibPromise = null; // allow retry on next call
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.78",
3
+ "version": "0.5.79",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",