mnfst 0.5.74 → 0.5.77

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 (html-to-image, jsPDF) are loaded lazily on first
29
+ * Library dependencies (modern-screenshot, 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.
@@ -151,12 +151,12 @@ function initializeExportPlugin() {
151
151
  }
152
152
 
153
153
  // Transparent 1×1 PNG, used as a fallback when an inline image fails to fetch.
154
- // html-to-image would otherwise reject the whole snapshot on a single CORS or 404.
154
+ // Without it, a single CORS or 404 image would reject the entire snapshot.
155
155
  const TRANSPARENT_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=';
156
156
 
157
- // html-to-image rejects with the raw `image.onerror` Event when the assembled
158
- // SVG fails to decode (oversized clones, malformed inline assets, etc.).
159
- // Translate those into something readable before logging.
157
+ // The snapshot library rejects with the raw `image.onerror` Event when the
158
+ // assembled SVG fails to decode (oversized clones, malformed inline assets,
159
+ // etc.). Translate those into something readable before logging.
160
160
  function describeExportError(err) {
161
161
  if (err && err.message) return err;
162
162
  if (err && typeof Event !== 'undefined' && err instanceof Event) {
@@ -208,66 +208,102 @@ function initializeExportPlugin() {
208
208
  }
209
209
 
210
210
  function snapshotOptions(opts) {
211
- // Filter callback for html-to-image — exclude opt-out elements.
211
+ // Filter callback for modern-screenshot — exclude opt-out elements.
212
212
  const filter = (node) => {
213
213
  if (!node || node.nodeType !== 1) return true;
214
214
  return !(node.hasAttribute && node.hasAttribute('data-no-export'));
215
215
  };
216
216
  const out = {
217
- pixelRatio: Number(opts.resolution) > 0 ? Number(opts.resolution) : 2,
218
- cacheBust: true,
219
- // A single cross-origin image without CORS headers would otherwise
220
- // reject the whole snapshot. Substitute a transparent pixel so the
221
- // export still produces a usable file.
222
- imagePlaceholder: TRANSPARENT_PIXEL,
223
- filter
217
+ 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
+ },
224
226
  };
225
227
  if (opts.backgroundColor) out.backgroundColor = opts.backgroundColor;
226
- if (opts.width) out.canvasWidth = Number(opts.width);
227
- if (opts.height) out.canvasHeight = Number(opts.height);
228
+ if (opts.width) out.width = Number(opts.width);
229
+ if (opts.height) out.height = Number(opts.height);
228
230
  return out;
229
231
  }
230
232
 
231
- // Run an html-to-image call once. If it rejects with a raw Event (the SVG
232
- // failed to decode usually a font or oversized clone issue), retry once
233
- // with safer options before propagating the error. Most real-world failures
234
- // recover on the second attempt.
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.
235
236
  async function snapshotWithFallback(lib, fn, target, so) {
236
237
  try {
237
238
  return await lib[fn](target, so);
238
239
  } catch (err) {
239
240
  if (err && err.message) throw err; // a real Error — don't mask it
240
- const safer = { ...so, skipFonts: true, pixelRatio: Math.min(so.pixelRatio || 2, 1) };
241
+ const safer = {
242
+ ...so,
243
+ scale: Math.min(so.scale || 2, 1),
244
+ font: { skip: true },
245
+ };
241
246
  return await lib[fn](target, safer);
242
247
  }
243
248
  }
244
249
 
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
+
245
270
  async function exportImage(opts, filename, ext) {
246
- const lib = await loadHtmlToImage();
271
+ const lib = await loadSnapshotLib();
247
272
  const target = resolveTarget(opts);
248
- const so = snapshotOptions(opts);
273
+ const so = applySafeDimensions(target, snapshotOptions(opts));
249
274
  let dataUrl;
250
- if (ext === 'png') dataUrl = await snapshotWithFallback(lib, 'toPng', target, so);
275
+ if (ext === 'png') dataUrl = await snapshotWithFallback(lib, 'domToPng', target, so);
251
276
  else if (ext === 'jpeg') {
252
277
  if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
253
278
  so.quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
254
279
  ? Number(opts.quality)
255
280
  : 0.95;
256
- dataUrl = await snapshotWithFallback(lib, 'toJpeg', target, so);
281
+ dataUrl = await snapshotWithFallback(lib, 'domToJpeg', target, so);
257
282
  } else if (ext === 'webp') {
258
- const webpOpts = { ...so, type: 'image/webp', quality: opts.quality || 0.95 };
259
- const blob = await snapshotWithFallback(lib, 'toBlob', target, webpOpts);
260
- dataUrl = URL.createObjectURL(blob);
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);
261
287
  }
262
288
  triggerDownload(dataUrl, filename);
263
289
  }
264
290
 
291
+ // Whole-page PDF: route through the browser's native print pipeline.
292
+ // It handles multi-page layout, page breaks, vector text, and the page's
293
+ // own @media print CSS — far more reliable than rasterizing a long page
294
+ // and embedding it as a single image. The user sees the print dialog and
295
+ // 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.
265
298
  async function exportPdf(opts, filename) {
266
- const [imgLib, jsPDFCtor] = await Promise.all([loadHtmlToImage(), loadJsPDF()]);
299
+ if (!opts.target) {
300
+ return printToPdf(filename);
301
+ }
302
+ const [imgLib, jsPDFCtor] = await Promise.all([loadSnapshotLib(), loadJsPDF()]);
267
303
  const target = resolveTarget(opts);
268
- const so = snapshotOptions(opts);
304
+ const so = applySafeDimensions(target, snapshotOptions(opts));
269
305
  if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
270
- const dataUrl = await snapshotWithFallback(imgLib, 'toPng', target, so);
306
+ const dataUrl = await snapshotWithFallback(imgLib, 'domToPng', target, so);
271
307
  const img = await loadImage(dataUrl);
272
308
  const orientation = img.width > img.height ? 'landscape' : 'portrait';
273
309
  const pdf = new jsPDFCtor({ orientation, unit: 'pt', format: opts.pageSize || 'a4' });
@@ -282,6 +318,20 @@ function initializeExportPlugin() {
282
318
  pdf.save(filename);
283
319
  }
284
320
 
321
+ function printToPdf(filename) {
322
+ // The browser's "Save as PDF" dialog seeds its default filename from
323
+ // document.title. Swap it briefly so the suggested name matches the
324
+ // user's intent, then restore after the dialog closes.
325
+ const original = document.title;
326
+ const cleaned = String(filename || '').replace(/\.pdf$/i, '') || original;
327
+ document.title = cleaned;
328
+ try { window.print(); }
329
+ finally {
330
+ // Wait one frame so the print dialog reads the swapped title first.
331
+ setTimeout(() => { document.title = original; }, 0);
332
+ }
333
+ }
334
+
285
335
  function effectivePageBackground() {
286
336
  let el = document.body;
287
337
  while (el && el !== document.documentElement.parentElement) {
@@ -379,15 +429,20 @@ function initializeExportPlugin() {
379
429
 
380
430
  // Lazy library loaders — cached promises.
381
431
 
382
- let htmlToImagePromise = null;
383
- function loadHtmlToImage() {
384
- if (htmlToImagePromise) return htmlToImagePromise;
385
- htmlToImagePromise = loadScript('https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js')
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
+ let snapshotLibPromise = null;
437
+ function loadSnapshotLib() {
438
+ if (snapshotLibPromise) return snapshotLibPromise;
439
+ snapshotLibPromise = loadScript('https://cdn.jsdelivr.net/npm/modern-screenshot/dist/index.iife.min.js')
386
440
  .then(() => {
387
- if (!window.htmlToImage) throw new Error('html-to-image failed to load');
388
- return window.htmlToImage;
441
+ const lib = window.modernScreenshot;
442
+ if (!lib) throw new Error('modern-screenshot failed to load');
443
+ return lib;
389
444
  });
390
- return htmlToImagePromise;
445
+ return snapshotLibPromise;
391
446
  }
392
447
 
393
448
  let jsPDFPromise = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.74",
3
+ "version": "0.5.77",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",