mnfst 0.5.112 → 0.5.113

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.
@@ -232,8 +232,17 @@ function initializeExportPlugin() {
232
232
  return !!(node && node.nodeType === 1
233
233
  && node.hasAttribute && node.hasAttribute('data-no-export'));
234
234
  };
235
+ // Default scale = device pixel ratio. On a retina display that's 2,
236
+ // so the export matches what the user actually sees on screen. On a
237
+ // standard 1x display it's 1, so the export is the literal CSS
238
+ // pixel size of the region — no surprise doubling. Callers can
239
+ // explicitly set `resolution: N` to render at N× independent of the
240
+ // host display (useful for "always-2x" archive renders or "1x" thumbs).
241
+ const defaultScale = (typeof window !== 'undefined' && window.devicePixelRatio > 0)
242
+ ? window.devicePixelRatio
243
+ : 1;
235
244
  const out = {
236
- scale: Number(opts.resolution) > 0 ? Number(opts.resolution) : 2,
245
+ scale: Number(opts.resolution) > 0 ? Number(opts.resolution) : defaultScale,
237
246
  ignoreElements,
238
247
  useCORS: true,
239
248
  allowTaint: false,
@@ -342,53 +351,87 @@ function initializeExportPlugin() {
342
351
  // - The page's own @media print rules
343
352
  // Whole-page is the default; passing a target adds a temporary
344
353
  // @media print stylesheet that hides everything outside that subtree.
345
- return printToPdf(filename, opts.target ? resolveTarget(opts) : null);
354
+ return printToPdf(filename, opts.target ? resolveTarget(opts) : null, opts.pageSize);
346
355
  }
347
356
 
348
357
  // Trigger the browser's "Save as PDF" dialog. When `target` is provided,
349
- // the print is scoped to that element via injected @media print CSS;
350
- // otherwise the whole page prints. Filename uses the document.title swap
351
- // trick — the print dialog seeds its suggested name from the title.
352
- function printToPdf(filename, target) {
358
+ // the print is scoped to just that element; otherwise the whole page
359
+ // prints. Filename uses the document.title swap trick — the print dialog
360
+ // seeds its suggested name from the title.
361
+ //
362
+ // Target-scoping strategy: clone the target into a fresh body-level
363
+ // container and `display: none` every other body child. Cloning (rather
364
+ // than relocating) leaves the live DOM intact — Alpine state, event
365
+ // listeners, and any in-flight reactivity stay where they were. The
366
+ // earlier visibility:hidden approach preserved layout space, so the body
367
+ // still occupied its full natural height during print → blank trailing
368
+ // pages. display:none actually removes that space, so the printed pages
369
+ // fit the target's content with no whitespace tail.
370
+ function printToPdf(filename, target, pageSize) {
353
371
  const original = document.title;
354
372
  const cleaned = String(filename || '').replace(/\.pdf$/i, '') || original;
355
373
  document.title = cleaned;
374
+ // Sanitize pageSize: only allow letters/digits/spaces so an
375
+ // attacker-controlled value can't break out of the @page rule.
376
+ const safePageSize = (typeof pageSize === 'string' && /^[a-zA-Z0-9 ]+$/.test(pageSize))
377
+ ? pageSize.trim()
378
+ : 'a4';
379
+
380
+ let printContainer = null;
381
+ // Always inject a @page rule so the chosen pageSize applies for both
382
+ // whole-page and targeted prints.
383
+ const pageRule = '@page { size: ' + safePageSize + '; margin: 1cm; }';
384
+ const style = document.createElement('style');
385
+ style.setAttribute('data-mnfst-print-scope', '');
356
386
 
357
- let style = null;
358
- let injectedId = null;
359
387
  if (target && target.nodeType === 1) {
360
- // The target needs an id we can reference from the stylesheet.
361
- // Give it a throwaway one if it doesn't already have one; we'll
362
- // remove it on cleanup so we don't leak DOM mutations.
363
- if (!target.id) {
364
- injectedId = 'mnfst-print-target-' + Math.random().toString(36).slice(2, 9);
365
- target.id = injectedId;
366
- }
367
- const id = target.id;
368
- style = document.createElement('style');
369
- style.setAttribute('data-mnfst-print-scope', '');
370
- // visibility:hidden preserves layout space (so absolute/fixed
371
- // positioning calculations remain sane), then we un-hide the
372
- // target subtree and pin it to the top-left of the print page.
388
+ printContainer = document.createElement('div');
389
+ printContainer.setAttribute('data-mnfst-print-target', '');
390
+ // Anchor off-screen so the clone never flashes visibly in normal
391
+ // (non-print) rendering. The print stylesheet below resets these
392
+ // for the print medium only.
393
+ printContainer.style.position = 'absolute';
394
+ printContainer.style.left = '-99999px';
395
+ printContainer.style.top = '0';
396
+ printContainer.appendChild(target.cloneNode(true));
397
+ document.body.appendChild(printContainer);
398
+
373
399
  style.textContent =
400
+ pageRule +
374
401
  '@media print {' +
375
- 'body * { visibility: hidden !important; }' +
376
- '#' + cssEscape(id) + ', #' + cssEscape(id) + ' * { visibility: visible !important; }' +
377
- '#' + cssEscape(id) + ' {' +
378
- 'position: absolute !important;' +
379
- 'left: 0 !important; top: 0 !important;' +
380
- 'width: 100% !important; max-width: none !important;' +
402
+ // Hide every original body child display:none takes them
403
+ // out of layout entirely, so body's height collapses to
404
+ // just the clone's height.
405
+ 'body > *:not([data-mnfst-print-target]) { display: none !important; }' +
406
+ // Restore the clone to normal flow at the top-left of
407
+ // the page, full width.
408
+ 'body > [data-mnfst-print-target] {' +
409
+ 'display: block !important;' +
410
+ 'position: static !important;' +
411
+ 'left: auto !important;' +
412
+ 'top: auto !important;' +
413
+ 'width: 100% !important;' +
414
+ 'max-width: none !important;' +
381
415
  'margin: 0 !important;' +
416
+ 'padding: 0 !important;' +
382
417
  '}' +
383
418
  '[data-no-export] { display: none !important; }' +
384
419
  '}';
385
- document.head.appendChild(style);
420
+ } else {
421
+ // Whole-page print: just honor pageSize and the data-no-export
422
+ // filter; let the page's own print CSS do everything else.
423
+ style.textContent =
424
+ pageRule +
425
+ '@media print {' +
426
+ '[data-no-export] { display: none !important; }' +
427
+ '}';
386
428
  }
429
+ document.head.appendChild(style);
387
430
 
388
431
  const cleanup = () => {
389
432
  document.title = original;
390
- if (style) style.remove();
391
- if (injectedId && target) target.removeAttribute('id');
433
+ style.remove();
434
+ if (printContainer) printContainer.remove();
392
435
  window.removeEventListener('afterprint', onAfter);
393
436
  };
394
437
  const onAfter = () => cleanup();
@@ -406,16 +449,6 @@ function initializeExportPlugin() {
406
449
  setTimeout(cleanup, 30000);
407
450
  }
408
451
 
409
- // CSS.escape polyfill — used to safely build #id selectors from arbitrary
410
- // ids. Real `CSS.escape` is widely available; fall back for very old
411
- // browsers to a conservative regex.
412
- function cssEscape(value) {
413
- if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
414
- return CSS.escape(String(value));
415
- }
416
- return String(value).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c);
417
- }
418
-
419
452
  function effectivePageBackground() {
420
453
  let el = document.body;
421
454
  while (el && el !== document.documentElement.parentElement) {
@@ -8,7 +8,7 @@
8
8
  "manifest.components.js": "sha384-3dCTD5EwCZTiX+1obYtDNM3WWwPh2JDQUQQsdRUUK3gs6FXjse1ShkKaT/2jsNaI",
9
9
  "manifest.data.js": "sha384-pgX6RJRWP7jmWO4ALb+GbS7Gm5JGOrCtxjEOnEcj1aJ8HoGbFjOniyjsntf8IA+B",
10
10
  "manifest.dropdowns.js": "sha384-WMrFoSpKfJuo81dyrwhVrDO8rq+rDwh2x8x4nH01BY5ZHkvjE+/SaT2gWCI0zOn+",
11
- "manifest.export.js": "sha384-kTZBHsXPtj6RuLSyR323lCjIm0VTtZ64IdpyvOULMMJHqw/kIXjtQDyph5Ak/Ryv",
11
+ "manifest.export.js": "sha384-IF2MJHlDpx9hprNtdplNIJJPnU5xE+9/S+LQL3MBG4ERICBtIwOKM5V7RRY/OWzM",
12
12
  "manifest.icons.js": "sha384-uOkboYrovjCpl22eey3Jaxpey+pOnot5NDnRRumcRxiR7IOVaRh1i20gYnWXR5dW",
13
13
  "manifest.localization.js": "sha384-M40EWrbWs2MoJvUiYVQaPxPiOzMYBn/ywuMR02rvoSXG77eIHT/aRoYub9r6+jC+",
14
14
  "manifest.markdown.js": "sha384-jJFVCg46bYcP/xglX6QguG2qsGi2I/0o9kXLpM2RMq7edbTYpgx6WQ9iNB7kKHmN",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.112",
3
+ "version": "0.5.113",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",