gutterpress 0.11.23-beta.1 → 0.11.23-beta.3

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.
Files changed (65) hide show
  1. package/README.md +13 -3
  2. package/dist/api/index.d.ts +5 -5
  3. package/dist/api/index.js +19 -7
  4. package/dist/{build-dg8a566j.js → build-fq3mexhy.js} +9 -9
  5. package/dist/checks/asset/approved-fonts.d.ts +8 -0
  6. package/dist/checks/asset/extensions.d.ts +1 -1
  7. package/dist/checks/asset/index.d.ts +1 -0
  8. package/dist/{cli-e31jshz6.js → cli-5zbdb6mt.js} +296 -65
  9. package/dist/{cli-ttj95aex.js → cli-7mt0dftw.js} +47 -18
  10. package/dist/{cli-989eg8jz.js → cli-ah93yyzb.js} +163 -55
  11. package/dist/{cli-31anr0r9.js → cli-eymdsefg.js} +69 -29
  12. package/dist/{cli-1fy1pm9d.js → cli-fh685nqs.js} +1 -1
  13. package/dist/{cli-k087gj0p.js → cli-fsjre61x.js} +123 -12
  14. package/dist/{cli-s1zt779w.js → cli-q7t04gjt.js} +1 -1
  15. package/dist/{cli-vhav8g82.js → cli-rybz2dfs.js} +1 -1
  16. package/dist/cli.js +12 -12
  17. package/dist/{doctor-g4a42tcz.js → doctor-tg9ht1n3.js} +3 -3
  18. package/dist/engine/compiler/postprocess.d.ts +3 -0
  19. package/dist/engine/shared/pdf-inspect.d.ts +33 -0
  20. package/dist/{engine-jvkcahr1.js → engine-17v87vq5.js} +1 -1
  21. package/dist/{engine-wgkaqjsc.js → engine-y9fnarfa.js} +2 -2
  22. package/dist/{ext-w9d5ftcg.js → ext-esz03jz4.js} +27 -10
  23. package/dist/{extension-restore-5x4mj79d.js → extension-restore-91gy1e28.js} +3 -3
  24. package/dist/{extension-restore-as0g38p1.js → extension-restore-q70wqah5.js} +5 -5
  25. package/dist/{gutterpress-agent-d29ry3ak.js → gutterpress-agent-wyryqehx.js} +13 -4
  26. package/dist/{gutterpress-viewer-ze691rrd.js → gutterpress-viewer-ywkf49q5.js} +38 -153
  27. package/dist/{index-a4j3xcfp.js → index-67g8f7r4.js} +47 -18
  28. package/dist/{index-798jt0qc.js → index-d9w3r89w.js} +3 -3
  29. package/dist/{index-rj5vrhz6.js → index-fntpr44x.js} +321 -66
  30. package/dist/{index-5x42amh8.js → index-g3te5ey1.js} +2 -1
  31. package/dist/{index-cw03e9p4.js → index-kw4tbthz.js} +69 -29
  32. package/dist/{index-fk2kpf2t.js → index-santcs4q.js} +121 -10
  33. package/dist/{index-c6y0p7jy.js → index-z6vzj3s5.js} +243 -73
  34. package/dist/index.js +35 -8
  35. package/dist/lib/check-options.d.ts +70 -0
  36. package/dist/lib/extension-manager.d.ts +16 -1
  37. package/dist/lib/extension-scaffold.d.ts +15 -2
  38. package/dist/lib/extension-specifier.d.ts +8 -1
  39. package/dist/lib/manifest-config.d.ts +3 -1
  40. package/dist/lib/manifest-old-spellings.d.ts +32 -9
  41. package/dist/lib/markdown/gutterpress-css.d.ts +1 -1
  42. package/dist/lib/markdown/math.d.ts +1 -1
  43. package/dist/lib/markdown/typography.d.ts +1 -0
  44. package/dist/lib/page-size.d.ts +12 -4
  45. package/dist/lib/plugin-vendor.d.ts +6 -0
  46. package/dist/lib/presets.d.ts +2 -2
  47. package/dist/lib/root-tokens.d.ts +2 -1
  48. package/dist/lib/source-provider.d.ts +27 -0
  49. package/dist/lib/style-resolver.d.ts +2 -1
  50. package/dist/lib/version-changes.d.ts +22 -6
  51. package/dist/{manifest.schema-0jgj0jt1.json → manifest.schema-5cm13vr3.json} +215 -5
  52. package/dist/{new-jtk028rf.js → new-rmfyb7va.js} +22 -15
  53. package/dist/{plugins-bcbe7xs8.js → plugins-gs21xz5s.js} +2 -2
  54. package/dist/{plugins-zsm6wv79.js → plugins-tqv1s16f.js} +4 -4
  55. package/dist/{preflight-cs6mj1d6.js → preflight-bjyeeq0d.js} +9 -9
  56. package/dist/{preview-d1j1g4qf.js → preview-ke03fk5d.js} +9 -9
  57. package/dist/{publish-7yw4jyrt.js → publish-wpnz0qng.js} +9 -9
  58. package/dist/render.js +55 -13
  59. package/dist/schema/manifest.types.d.ts +45 -7
  60. package/dist/{source-provider-n7vehh6t.js → source-provider-3g3yrn1f.js} +6 -2
  61. package/dist/{source-provider-tgzet3j5.js → source-provider-azg9ywhb.js} +6 -2
  62. package/dist/test-helpers/vocabulary.d.ts +12 -0
  63. package/dist/{validate-rkcdnzhw.js → validate-zcwykb43.js} +9 -9
  64. package/package.json +1 -1
  65. /package/dist/{cli-y28j019d.js → cli-1dha1nxk.js} +0 -0
@@ -2,7 +2,7 @@ import {
2
2
  EXTENSIONS_DIR,
3
3
  LEGACY_PLUGINS_DIR,
4
4
  VENDORED_NPM_DIR
5
- } from "./index-5x42amh8.js";
5
+ } from "./index-g3te5ey1.js";
6
6
  import {
7
7
  capabilitiesFor,
8
8
  detectProjectSource,
@@ -423,6 +423,40 @@ class LocalFolderSourceProvider {
423
423
  return Promise.reject(new Error(NO_VERSION_HISTORY));
424
424
  }
425
425
  }
426
+ var lastHistoryWalk = null;
427
+ async function startHistoryWalk(dir, cache, heads) {
428
+ const shallow = await gitFs.promises.readFile(path2.join(gitDirFor(dir), "shallow"), "utf8").catch(() => "");
429
+ const tips = await Promise.all(heads.map((oid) => git.readCommit({ fs: gitFs, dir, cache, oid })));
430
+ tips.sort((a, b) => a.commit.committer.timestamp - b.commit.committer.timestamp);
431
+ return {
432
+ tips,
433
+ queued: new Set(heads),
434
+ shallow: new Set(shallow.split(`
435
+ `)),
436
+ readAhead: new Map
437
+ };
438
+ }
439
+ async function nextInHistory(dir, walk, cache, needed) {
440
+ const commit = walk.tips.pop();
441
+ if (!commit || walk.shallow.has(commit.oid))
442
+ return commit;
443
+ for (const oid of commit.commit.parent) {
444
+ if (walk.queued.has(oid))
445
+ continue;
446
+ const oneLine = walk.tips.length === 0 && commit.commit.parent.length === 1;
447
+ if (needed > 1 && oneLine && !walk.readAhead.has(oid)) {
448
+ for (const c of await git.log({ fs: gitFs, dir, cache, ref: oid, depth: needed })) {
449
+ if (!walk.queued.has(c.oid))
450
+ walk.readAhead.set(c.oid, c);
451
+ }
452
+ }
453
+ walk.queued.add(oid);
454
+ walk.tips.push(walk.readAhead.get(oid) ?? await git.readCommit({ fs: gitFs, dir, cache, oid }));
455
+ walk.readAhead.delete(oid);
456
+ }
457
+ walk.tips.sort((a, b) => a.commit.committer.timestamp - b.commit.committer.timestamp);
458
+ return commit;
459
+ }
426
460
 
427
461
  class LocalGitSourceProvider {
428
462
  source;
@@ -460,40 +494,46 @@ class LocalGitSourceProvider {
460
494
  const dir = this.dir;
461
495
  const limit = Math.min(500, Math.max(1, options.limit ?? HISTORY_PAGE_LIMIT));
462
496
  const before = options.before;
463
- let commits;
497
+ const cache = {};
464
498
  try {
465
- commits = await git.log({
466
- fs: gitFs,
467
- dir,
468
- cache: {},
469
- depth: limit + 2,
470
- ...before ? { ref: before } : {}
471
- });
499
+ const head = await git.resolveRef({ fs: gitFs, dir, ref: "HEAD" });
500
+ let walk;
501
+ const saved = lastHistoryWalk;
502
+ if (before && saved?.dir === dir && saved.head === head && saved.last === before) {
503
+ walk = saved.walk;
504
+ lastHistoryWalk = null;
505
+ } else {
506
+ walk = await startHistoryWalk(dir, cache, [head]);
507
+ if (before) {
508
+ let c;
509
+ do {
510
+ c = await nextInHistory(dir, walk, cache, limit);
511
+ } while (c && c.oid !== before);
512
+ if (!c)
513
+ return { entries: [], hasMore: false };
514
+ }
515
+ }
516
+ const entries = [];
517
+ while (entries.length < limit) {
518
+ const c = await nextInHistory(dir, walk, cache, limit - entries.length);
519
+ if (!c)
520
+ break;
521
+ entries.push({
522
+ id: c.oid,
523
+ message: c.commit.message.trim(),
524
+ timestamp: c.commit.author.timestamp * 1000,
525
+ author: c.commit.author.name
526
+ });
527
+ }
528
+ const hasMore = walk.tips.length > 0;
529
+ lastHistoryWalk = hasMore ? { dir, head, last: entries.at(-1).id, walk } : null;
530
+ return { entries, hasMore };
472
531
  } catch (e) {
473
532
  if (before && e?.code === "NotFoundError") {
474
533
  return { entries: [], hasMore: false };
475
534
  }
476
535
  throw e;
477
536
  }
478
- const seen = new Set;
479
- let filtered = commits.filter((c) => {
480
- if (c.oid === before || seen.has(c.oid))
481
- return false;
482
- seen.add(c.oid);
483
- return true;
484
- });
485
- const hasMore = filtered.length > limit;
486
- if (hasMore)
487
- filtered = filtered.slice(0, limit);
488
- return {
489
- entries: filtered.map((c) => ({
490
- id: c.oid,
491
- message: c.commit.message.trim(),
492
- timestamp: c.commit.author.timestamp * 1000,
493
- author: c.commit.author.name
494
- })),
495
- hasMore
496
- };
497
537
  }
498
538
  restore(options) {
499
539
  return withRepoLock(this.dir, () => this.restoreUnlocked(options));
@@ -793,4 +833,4 @@ async function switchBranch(options) {
793
833
  });
794
834
  }
795
835
 
796
- export { gitFs, errorLogData, resolveLogger, gitScopeFor, HISTORY_PAGE_LIMIT, NO_VERSION_HISTORY, withRepoLock, whenGitIdle, __repoLockQueueSizeForTests, resolveGitAuthor, listWorkdirChanges, stageChanges, hasPendingChanges, hasUncommittedChanges, snapshotWorkingTreeUnlocked, isNoChangesError, countUnversionedChanges, providerFor, gitDirFor, snapshotStagingMarkerPath, RESTORE_BACKUP_MESSAGE, AUTO_SNAPSHOT_MESSAGE, restoreVersionWithBackup, SWITCH_BRANCH_SNAPSHOT_MESSAGE, readRevisionRefs, isContained, listRevisions, revisionNameFrom, assertFreeRevisionName, createRevision, switchBranch };
836
+ export { gitFs, errorLogData, resolveLogger, gitScopeFor, HISTORY_PAGE_LIMIT, NO_VERSION_HISTORY, withRepoLock, whenGitIdle, __repoLockQueueSizeForTests, resolveGitAuthor, listWorkdirChanges, stageChanges, hasPendingChanges, hasUncommittedChanges, startHistoryWalk, nextInHistory, snapshotWorkingTreeUnlocked, isNoChangesError, countUnversionedChanges, providerFor, gitDirFor, snapshotStagingMarkerPath, RESTORE_BACKUP_MESSAGE, AUTO_SNAPSHOT_MESSAGE, restoreVersionWithBackup, SWITCH_BRANCH_SNAPSHOT_MESSAGE, readRevisionRefs, isContained, listRevisions, revisionNameFrom, assertFreeRevisionName, createRevision, switchBranch };
@@ -19,7 +19,7 @@ import {
19
19
  stringValueAt,
20
20
  toFolioPage,
21
21
  wantsRecto
22
- } from "./index-798jt0qc.js";
22
+ } from "./index-d9w3r89w.js";
23
23
  import {
24
24
  round3
25
25
  } from "./index-tayzfr9c.js";
@@ -312,12 +312,14 @@ function needsMeasurement(value) {
312
312
 
313
313
  // src/engine/shared/pdf-inspect.ts
314
314
  import {
315
+ decodePDFRawStream,
315
316
  PDFArray,
316
317
  PDFDict,
317
318
  PDFDocument,
318
319
  PDFHexString,
319
320
  PDFName,
320
321
  PDFNumber,
322
+ PDFRawStream,
321
323
  PDFRef,
322
324
  PDFString
323
325
  } from "pdf-lib";
@@ -438,6 +440,52 @@ async function inspectPdf(bytes) {
438
440
  walkOutline(outlines);
439
441
  return { pageCount: pages.length, boxes, linkTargets, namedDests, outline };
440
442
  }
443
+ var PROLOGUE = /^\s*(\d*\.?\d+) 0 0 -\d*\.?\d+ -?\d*\.?\d+ -?\d*\.?\d+ cm/;
444
+ var CLIP = /(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+) re\s+W\*? n\b/g;
445
+ var UNIFORM = /\bq\s+(\d*\.?\d+) 0 0 \1 (-?\d*\.?\d+) -?\d*\.?\d+ cm/g;
446
+ var UNSCALED_EPSILON = 0.000001;
447
+ function printScale(doc) {
448
+ const readings = new Map;
449
+ for (const page of doc.getPages()) {
450
+ const contents = page.node.Contents();
451
+ const streams = contents instanceof PDFArray ? contents.asArray().map((ref) => doc.context.lookup(ref)) : [contents];
452
+ let text = "";
453
+ for (const stream of streams) {
454
+ if (!(stream instanceof PDFRawStream))
455
+ continue;
456
+ try {
457
+ text += Buffer.from(decodePDFRawStream(stream).decode()).toString("latin1") + `
458
+ `;
459
+ } catch {}
460
+ }
461
+ const prologue = PROLOGUE.exec(text);
462
+ if (!prologue)
463
+ continue;
464
+ let area;
465
+ for (const m of text.matchAll(CLIP)) {
466
+ const size = Number(m[3]) * Number(m[4]);
467
+ if (!area || size > area.size)
468
+ area = { x: Number(m[1]), size, end: m.index + m[0].length };
469
+ }
470
+ if (!area)
471
+ continue;
472
+ UNIFORM.lastIndex = area.end;
473
+ for (let m;m = UNIFORM.exec(text); ) {
474
+ if (Math.abs(Number(m[2]) - area.x) > 0.5)
475
+ continue;
476
+ const scale = Number(m[1]) * Number(prologue[1]) * 96 / 72;
477
+ readings.set(scale, (readings.get(scale) ?? 0) + 1);
478
+ break;
479
+ }
480
+ if ([...readings.values()].some((n) => n >= 3))
481
+ break;
482
+ }
483
+ let best = null;
484
+ for (const [scale, n] of readings)
485
+ if (best === null || n > readings.get(best))
486
+ best = scale;
487
+ return best === null ? null : Math.abs(1 - best) < UNSCALED_EPSILON ? 1 : best;
488
+ }
441
489
 
442
490
  // src/engine/compiler/tier2.ts
443
491
  var DEFAULT_SLUG_PT = 18;
@@ -584,6 +632,7 @@ function round(n) {
584
632
  import { PDFDocument as PDFDocument2, PDFName as PDFName2, PDFNumber as PDFNumber2, rgb } from "pdf-lib";
585
633
  async function postprocess(input, opts) {
586
634
  const doc = await PDFDocument2.load(input, { updateMetadata: false });
635
+ const scale = printScale(doc);
587
636
  const g = opts.geometry;
588
637
  const inset = g.bleed + g.slug;
589
638
  const media = [0, 0, g.media.width, g.media.height];
@@ -630,7 +679,8 @@ async function postprocess(input, opts) {
630
679
  bytes,
631
680
  pageCount: doc.getPageCount(),
632
681
  padded,
633
- boxes: { media, bleed: bleedBox, trim: trimBox }
682
+ boxes: { media, bleed: bleedBox, trim: trimBox },
683
+ scale
634
684
  };
635
685
  }
636
686
  function drawCropMarks(page, g) {
@@ -934,8 +984,6 @@ ${pinCss}`);
934
984
  const msg = `${headline}:
935
985
  ${describe(widthOffenders.boxes)}`;
936
986
  if (opts.allowShrink) {
937
- if (scale !== null)
938
- diagnose("engine.width.overflow", `${headline}.`);
939
987
  for (const o of widthOffenders.boxes)
940
988
  diagnose("engine.width.overflow", `${o.desc} is ${Math.round(o.px)}px wide, past the ${Math.round(widthOffenders.limitPx)}px page content box. ${o.left < -1 ? "Keep it inside the page content box." : "Give it an explicit width that fits."}`);
941
989
  log(`WARNING: ${msg}`);
@@ -951,6 +999,54 @@ ${describe(widthOffenders.intrinsics)}`;
951
999
  diagnose("engine.width.intrinsic", `${o.desc} has no width set and is naturally wider than the page — if the printed output comes out smaller than your CSS says, give it an explicit width (e.g. width: 100%).`);
952
1000
  log(`WARNING: ${msg}`);
953
1001
  }
1002
+ let unclipped = null;
1003
+ if (!widthOffenders.boxes.length) {
1004
+ const g = tier2.geometry;
1005
+ const ext = g.bleed + g.slug;
1006
+ const contentPx = (name, pseudos) => {
1007
+ const { width, margin } = resolvePage(model, { name: name || undefined, pseudos }).geometry;
1008
+ return (width + 2 * ext - bleedMargin(margin.left, g) - bleedMargin(margin.right, g)) * 96 / 72;
1009
+ };
1010
+ const pages = await page.evaluate(`(() => {
1011
+ const body = document.body;
1012
+ const named = (el) => { const p = getComputedStyle(el).page; return p && p !== "auto" ? p : ""; };
1013
+ if (!body) return { first: "", used: [], reason: "there is no body" };
1014
+ const bs = getComputedStyle(body);
1015
+ const r = body.getBoundingClientRect();
1016
+ if (
1017
+ Math.abs(r.left + parseFloat(bs.borderLeftWidth)) > 0.01 ||
1018
+ Math.abs(r.right - parseFloat(bs.borderRightWidth) - window.innerWidth) > 0.01
1019
+ ) return { first: "", used: [], reason: "the body does not span the page content box" };
1020
+ // The first page's name: the root's, overridden down the chain of
1021
+ // first in-flow children (CSS Paged Media's start page value).
1022
+ let first = named(document.documentElement) || named(body);
1023
+ for (let el = body; el; ) {
1024
+ el = [...el.children].find((c) => {
1025
+ const cs = getComputedStyle(c);
1026
+ return cs.display !== "none" && cs.position !== "absolute" && cs.position !== "fixed";
1027
+ });
1028
+ if (el && named(el)) first = named(el);
1029
+ }
1030
+ const used = new Set([""]);
1031
+ for (const el of document.querySelectorAll("*")) used.add(named(el));
1032
+ return { first, used: [...used], reason: "" };
1033
+ })()`);
1034
+ const firstPx = contentPx(pages.first, ["first", "right", "recto"]);
1035
+ const wider = pages.used.find((name) => pseudoContexts.some((pseudos) => contentPx(name, pseudos) > firstPx + 0.01));
1036
+ unclipped = pages.reason || (wider !== undefined ? `${wider ? `the page "${wider}"` : "the unnamed page"} is wider than the first page` : await page.evaluate(`(() => {
1037
+ const hs = getComputedStyle(document.documentElement);
1038
+ const bs = getComputedStyle(document.body);
1039
+ let css;
1040
+ if (hs.overflowX === "visible" && hs.overflowY === "visible")
1041
+ css = "html { overflow-x: clip !important; } " +
1042
+ "body { overflow-x: clip !important; overflow-y: visible !important; }";
1043
+ else if (bs.overflowX === "visible") css = "body { overflow-x: clip !important; }";
1044
+ else return "the body clips its own overflow";
1045
+ window.__gp.addCss("gp-root-clip", css);
1046
+ return "";
1047
+ })()`)) || null;
1048
+ log(`body overflow ${unclipped ? `left as authored: ${unclipped}` : "held to the page content box"}`);
1049
+ }
954
1050
  const rectoDecls = model.breaks.filter(isRectoVersoBreak);
955
1051
  const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0 || furnitureRoots.length > 0 || sideRoots.length > 0;
956
1052
  let tier = tier2.geometry.bleed > 0 || tier2.geometry.slug > 0 ? 2 : 1;
@@ -1472,6 +1568,20 @@ ${mapCss}`;
1472
1568
  author: opts.author
1473
1569
  });
1474
1570
  log(`postprocess: ${post.pageCount} pages` + (post.padded ? ` (+${post.padded} blank${opts.lastPage ? `; last page: ${opts.lastPage}` : ""})` : ""));
1571
+ {
1572
+ const scale = post.scale;
1573
+ if (scale !== null && scale < 1) {
1574
+ const pct = (scale * 100).toPrecision(6);
1575
+ const overPx = widthOffenders.defaultLimitPx * (1 / scale - 1);
1576
+ diagnose("engine.width.overflow", `This book printed at ${pct}% of its declared size: content reaches about ` + `${overPx < 0.1 ? overPx.toFixed(3) : overPx.toFixed(2)}px past the ` + `${Math.round(widthOffenders.defaultLimitPx)}px page content box, and Chromium shrinks ` + `the WHOLE print to fit it. The preview is not shrunk, so its lines and page ` + `breaks can differ from the PDF's.` + (unclipped ? ` Gutterpress could not hold it at the page content box: ${unclipped}.` : ""));
1577
+ if (!widthOffenders.boxes.length) {
1578
+ const named = await findWidthOffenders(page, model, 2 * (tier2.geometry.bleed + tier2.geometry.slug), sheetViewport, 0);
1579
+ for (const o of named.boxes)
1580
+ diagnose("engine.width.overflow", `${o.desc} reaches ${Math.max(o.px + Math.min(0, o.left) - (o.limit ?? named.limitPx), -o.left).toFixed(2)}px ` + `past the ${Math.round(o.limit ?? named.limitPx)}px page content box. Keep it ` + `inside the page content box.`);
1581
+ }
1582
+ log(`audit: printed at ${pct}% (shrink-to-fit)`);
1583
+ }
1584
+ }
1475
1585
  return {
1476
1586
  bytes: post.bytes,
1477
1587
  tier,
@@ -1506,7 +1616,7 @@ function shrinkScale(boxes, limitPx) {
1506
1616
  return null;
1507
1617
  return Math.max(1 / MAX_SHRINK, limitPx / maxRight);
1508
1618
  }
1509
- async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport) {
1619
+ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport, tolerancePx = 1) {
1510
1620
  const contexts = [
1511
1621
  resolvePage(model),
1512
1622
  ...model.pageNames.filter((n) => !n.startsWith("gp-") || n.startsWith("gp--pin-")).map((n) => resolvePage(model, { name: n }))
@@ -1532,7 +1642,8 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
1532
1642
  mobile: false
1533
1643
  });
1534
1644
  boxes.push(...JSON.parse(await page.evaluate(`(() => {
1535
- const LIMIT = ${widthPx} + 1;
1645
+ const TOL = ${tolerancePx};
1646
+ const LIMIT = Math.max(${widthPx} + TOL, ${Math.ceil(widthPx)});
1536
1647
  const NAMED = ${JSON.stringify(namedLimits)};
1537
1648
  const DEFAULT_LIMIT = ${defaultLimitPx};
1538
1649
  // The page an element lands on: the nearest ancestor with a non-auto
@@ -1572,10 +1683,10 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
1572
1683
  // pulls) AND left-edge protrusion (negative left with an ordinary
1573
1684
  // width) — both trigger the same whole-document shrink-to-fit,
1574
1685
  // measured: a left bleed alone cost 16%.
1575
- if (right <= LIMIT && r.width <= LIMIT && r.left >= -1) continue;
1686
+ if (right <= LIMIT && r.width <= LIMIT && r.left >= -TOL) continue;
1576
1687
  // ...but only where it actually escapes its clipping ancestors.
1577
1688
  const clip = clipEdges(el).edges;
1578
- if (Math.min(right, clip.right) <= LIMIT && Math.max(r.left, clip.left) >= -1) continue;
1689
+ if (Math.min(right, clip.right) <= LIMIT && Math.max(r.left, clip.left) >= -TOL) continue;
1579
1690
  let deepest = true;
1580
1691
  for (const c of el.children) {
1581
1692
  if (c.getBoundingClientRect().width >= r.width - 1) { deepest = false; break; }
@@ -1612,7 +1723,7 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
1612
1723
  const over = el.scrollWidth - el.clientWidth;
1613
1724
  // clientWidth is 0 on inline/replaced boxes, where scrollWidth
1614
1725
  // carries no overflow information -- skip rather than guess.
1615
- if (el.clientWidth <= 0 || over <= 1) continue;
1726
+ if (el.clientWidth <= 0 || over <= TOL) continue;
1616
1727
  // A descendant assigned to a DIFFERENT page is not overflowing this
1617
1728
  // element's page -- it is laid out against its own, wider one. The
1618
1729
  // full-bleed art plate is the standard case: it is legitimately
@@ -1666,7 +1777,7 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
1666
1777
  }
1667
1778
  return JSON.stringify(out);
1668
1779
  })()`));
1669
- return { limitPx, boxes, intrinsics: replaced };
1780
+ return { limitPx, defaultLimitPx, boxes, intrinsics: replaced };
1670
1781
  } finally {
1671
1782
  await page.send("Emulation.setDeviceMetricsOverride", restoreViewport);
1672
1783
  }