gutterpress 0.11.23-alpha.7 → 0.11.23-beta.2

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 (69) hide show
  1. package/README.md +3 -1
  2. package/dist/{README-syg2hn83.md → README-8rct3axy.md} +2 -2
  3. package/dist/api/index.d.ts +3 -3
  4. package/dist/api/index.js +8 -6
  5. package/dist/{build-w572wq10.js → build-3e18wnqj.js} +9 -8
  6. package/dist/{cli-mmgtqnmr.js → cli-0ep11b0q.js} +1 -1
  7. package/dist/cli-65gy8ask.js +82 -0
  8. package/dist/{cli-6pv84kf3.js → cli-88arpdke.js} +6 -84
  9. package/dist/{cli-eatdqx3z.js → cli-9n9kg58f.js} +1 -1
  10. package/dist/{cli-58fsk6d7.js → cli-pd2cje5b.js} +187 -44
  11. package/dist/{cli-3cvq3n1c.js → cli-pnrararp.js} +189 -85
  12. package/dist/{cli-wyg7qstd.js → cli-tgnyvra3.js} +15 -5
  13. package/dist/{cli-31anr0r9.js → cli-v0evdpdf.js} +68 -28
  14. package/dist/{cli-5ba6bm6z.js → cli-x7gcdg6w.js} +1 -1
  15. package/dist/cli.js +12 -12
  16. package/dist/{doctor-2f3zqmza.js → doctor-2f4s9dyn.js} +3 -3
  17. package/dist/engine/compiler/postprocess.d.ts +3 -0
  18. package/dist/engine/shared/page-vars.d.ts +32 -0
  19. package/dist/engine/shared/pdf-inspect.d.ts +33 -0
  20. package/dist/{engine-4rabsgwq.js → engine-c4pxz79g.js} +2 -1
  21. package/dist/{engine-wm4fggac.js → engine-cs5g2ez0.js} +3 -3
  22. package/dist/{ext-0cgky8n3.js → ext-70gv86v5.js} +9 -8
  23. package/dist/{extension-restore-vghc58vx.js → extension-restore-chj5nx3t.js} +3 -3
  24. package/dist/{extension-restore-e7je32pr.js → extension-restore-pd87p8r2.js} +5 -4
  25. package/dist/{gutterpress-agent-d29ry3ak.js → gutterpress-agent-wyryqehx.js} +13 -4
  26. package/dist/{gutterpress-viewer-mafsby5k.js → gutterpress-viewer-ywkf49q5.js} +90 -166
  27. package/dist/{index-nfysshcc.js → index-0k43evt0.js} +309 -155
  28. package/dist/{index-wm4x7xbf.js → index-2t1gdma7.js} +178 -35
  29. package/dist/{index-cw03e9p4.js → index-7j3hkwya.js} +68 -28
  30. package/dist/{index-fp7xwvpr.js → index-80vg3ed6.js} +15 -5
  31. package/dist/{index-2vxzxyyj.js → index-swfdgn2n.js} +10 -10
  32. package/dist/index-tayzfr9c.js +99 -0
  33. package/dist/{index-hphcn7th.js → index-ytjkvwy5.js} +5 -85
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +234 -7
  36. package/dist/lib/book-css.d.ts +56 -0
  37. package/dist/lib/markdown/assemble.d.ts +58 -0
  38. package/dist/lib/markdown/gutterpress-css.d.ts +1 -1
  39. package/dist/lib/publish/google-drive.d.ts +12 -4
  40. package/dist/lib/publish/manifest-publish.d.ts +6 -0
  41. package/dist/lib/publish/providers/gdrive-folder.d.ts +30 -0
  42. package/dist/lib/publish/run-publish.d.ts +6 -1
  43. package/dist/lib/publish/types.d.ts +52 -5
  44. package/dist/lib/remote-auth/converge-merge.d.ts +3 -3
  45. package/dist/lib/remote-auth/sync-messages.d.ts +2 -2
  46. package/dist/lib/root-tokens.d.ts +76 -0
  47. package/dist/lib/source-provider.d.ts +27 -0
  48. package/dist/lib/version-changes.d.ts +26 -16
  49. package/dist/{manifest-79zsgkpn.yaml → manifest-0vwaskj2.yaml} +1 -1
  50. package/dist/{manifest-2kc56wvw.yaml → manifest-aya31jwt.yaml} +1 -1
  51. package/dist/{manifest-41ddzm13.yaml → manifest-mffah3hr.yaml} +1 -1
  52. package/dist/{manifest-sqzm4681.yaml → manifest-wbcv8cbp.yaml} +2 -2
  53. package/dist/{manifest.schema-93sd7zje.json → manifest.schema-0jgj0jt1.json} +1 -1
  54. package/dist/{new-88rkrv71.js → new-d61n5cn0.js} +9 -8
  55. package/dist/{page-rules-ytj9p04v.css → page-rules-vnja6grt.css} +2 -2
  56. package/dist/{plugins-3aj0wc5h.js → plugins-0hhw6s17.js} +1 -1
  57. package/dist/{plugins-sprxsz5d.js → plugins-e0n46s6m.js} +3 -3
  58. package/dist/{preflight-11dg1qkk.js → preflight-fjayhjr5.js} +9 -8
  59. package/dist/{preview-nftp782j.js → preview-h815jrfb.js} +9 -8
  60. package/dist/{publish-6tgxfck9.js → publish-ft39zgma.js} +25 -9
  61. package/dist/render.js +77 -26
  62. package/dist/schema/manifest.types.d.ts +2 -1
  63. package/dist/{source-provider-n7vehh6t.js → source-provider-j1w7byeq.js} +5 -1
  64. package/dist/{source-provider-tgzet3j5.js → source-provider-tjaf303m.js} +5 -1
  65. package/dist/test-helpers/vocabulary.d.ts +12 -0
  66. package/dist/{tokens-n82t4k9h.css → tokens-a7grv940.css} +4 -5
  67. package/dist/{validate-pktqrye5.js → validate-b201dbrb.js} +9 -8
  68. package/package.json +1 -1
  69. package/dist/index-46ycxe6r.js +0 -18
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  launchChromium
3
3
  } from "./cli-eyyggect.js";
4
+ import {
5
+ round3
6
+ } from "./cli-65gy8ask.js";
4
7
 
5
8
  // src/engine/compiler/build.ts
6
9
  import { readFile as readFile2 } from "node:fs/promises";
@@ -630,6 +633,33 @@ function marginBoxesOnEdges(edges) {
630
633
  return Object.entries(owners).filter(([, own]) => own.some((e) => edges.includes(e))).map(([name]) => name);
631
634
  }
632
635
 
636
+ // src/engine/shared/page-vars.ts
637
+ var decls = (pairs) => pairs.map(([name, n]) => `--gp-page-${name}: ${round3(n)}pt;`).join(" ");
638
+ function pageDecls(model, name) {
639
+ const { width, height, margin } = resolvePage(model, { name, pseudos: ["right"] }).geometry;
640
+ return decls([
641
+ ["width", width],
642
+ ["height", height],
643
+ ["margin-top", margin.top],
644
+ ["margin-bottom", margin.bottom],
645
+ ["margin-inside", margin.left],
646
+ ["margin-outside", margin.right]
647
+ ]);
648
+ }
649
+ function pageGeometryVarsCss(model, area) {
650
+ const bookWide = decls([
651
+ ["bleed", area?.bleed ?? 0],
652
+ ["safe", area?.safe ?? 0],
653
+ ["gutter", area?.gutter ?? 0],
654
+ ["art-safe", area?.artSafe ?? 0]
655
+ ]);
656
+ return [
657
+ `:root { ${pageDecls(model)} ${bookWide} }`,
658
+ ...model.pageAssignments.map((a) => `${a.selector} { ${pageDecls(model, a.page)} }`)
659
+ ].join(`
660
+ `);
661
+ }
662
+
633
663
  // src/engine/shared/margin-box-support.ts
634
664
  var MARGIN_BOX_IGNORED_PROPERTIES = new Set([
635
665
  "transform",
@@ -1002,12 +1032,14 @@ function needsMeasurement(value) {
1002
1032
 
1003
1033
  // src/engine/shared/pdf-inspect.ts
1004
1034
  import {
1035
+ decodePDFRawStream,
1005
1036
  PDFArray,
1006
1037
  PDFDict,
1007
1038
  PDFDocument,
1008
1039
  PDFHexString,
1009
1040
  PDFName,
1010
1041
  PDFNumber,
1042
+ PDFRawStream,
1011
1043
  PDFRef,
1012
1044
  PDFString
1013
1045
  } from "pdf-lib";
@@ -1128,6 +1160,52 @@ async function inspectPdf(bytes) {
1128
1160
  walkOutline(outlines);
1129
1161
  return { pageCount: pages.length, boxes, linkTargets, namedDests, outline };
1130
1162
  }
1163
+ var PROLOGUE = /^\s*(\d*\.?\d+) 0 0 -\d*\.?\d+ -?\d*\.?\d+ -?\d*\.?\d+ cm/;
1164
+ var CLIP = /(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+) re\s+W\*? n\b/g;
1165
+ var UNIFORM = /\bq\s+(\d*\.?\d+) 0 0 \1 (-?\d*\.?\d+) -?\d*\.?\d+ cm/g;
1166
+ var UNSCALED_EPSILON = 0.000001;
1167
+ function printScale(doc) {
1168
+ const readings = new Map;
1169
+ for (const page of doc.getPages()) {
1170
+ const contents = page.node.Contents();
1171
+ const streams = contents instanceof PDFArray ? contents.asArray().map((ref) => doc.context.lookup(ref)) : [contents];
1172
+ let text = "";
1173
+ for (const stream of streams) {
1174
+ if (!(stream instanceof PDFRawStream))
1175
+ continue;
1176
+ try {
1177
+ text += Buffer.from(decodePDFRawStream(stream).decode()).toString("latin1") + `
1178
+ `;
1179
+ } catch {}
1180
+ }
1181
+ const prologue = PROLOGUE.exec(text);
1182
+ if (!prologue)
1183
+ continue;
1184
+ let area;
1185
+ for (const m of text.matchAll(CLIP)) {
1186
+ const size = Number(m[3]) * Number(m[4]);
1187
+ if (!area || size > area.size)
1188
+ area = { x: Number(m[1]), size, end: m.index + m[0].length };
1189
+ }
1190
+ if (!area)
1191
+ continue;
1192
+ UNIFORM.lastIndex = area.end;
1193
+ for (let m;m = UNIFORM.exec(text); ) {
1194
+ if (Math.abs(Number(m[2]) - area.x) > 0.5)
1195
+ continue;
1196
+ const scale = Number(m[1]) * Number(prologue[1]) * 96 / 72;
1197
+ readings.set(scale, (readings.get(scale) ?? 0) + 1);
1198
+ break;
1199
+ }
1200
+ if ([...readings.values()].some((n) => n >= 3))
1201
+ break;
1202
+ }
1203
+ let best = null;
1204
+ for (const [scale, n] of readings)
1205
+ if (best === null || n > readings.get(best))
1206
+ best = scale;
1207
+ return best === null ? null : Math.abs(1 - best) < UNSCALED_EPSILON ? 1 : best;
1208
+ }
1131
1209
 
1132
1210
  // src/lib/embedded-assets.ts
1133
1211
  import { existsSync } from "node:fs";
@@ -1140,7 +1218,7 @@ import { fileURLToPath } from "node:url";
1140
1218
  var favicon_default = "./favicon-272ga6y0.ico";
1141
1219
 
1142
1220
  // src/assets/manifest.schema.json
1143
- var manifest_schema_default = "./manifest.schema-93sd7zje.json";
1221
+ var manifest_schema_default = "./manifest.schema-0jgj0jt1.json";
1144
1222
 
1145
1223
  // src/assets/preview/scripts/preview-interface.js
1146
1224
  var preview_interface_default = "./preview-interface-0xdh0r5b.js";
@@ -1155,31 +1233,31 @@ var preview_shell_default = "./preview-shell-wk9k3gcd.js";
1155
1233
  var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
1156
1234
 
1157
1235
  // src/assets/engine/gutterpress-viewer.js
1158
- var gutterpress_viewer_default = "./gutterpress-viewer-mafsby5k.js";
1236
+ var gutterpress_viewer_default = "./gutterpress-viewer-ywkf49q5.js";
1159
1237
 
1160
1238
  // src/assets/engine/gutterpress-agent.js
1161
- var gutterpress_agent_default = "./gutterpress-agent-d29ry3ak.js";
1239
+ var gutterpress_agent_default = "./gutterpress-agent-wyryqehx.js";
1162
1240
 
1163
1241
  // src/assets/templates/book/manifest.yaml
1164
- var manifest_default = "./manifest-41ddzm13.yaml";
1242
+ var manifest_default = "./manifest-mffah3hr.yaml";
1165
1243
 
1166
1244
  // src/assets/templates/book/chapter-01.md
1167
1245
  var chapter_01_default = "./chapter-01-gsbjt584.md";
1168
1246
 
1169
1247
  // src/assets/templates/zine/manifest.yaml
1170
- var manifest_default2 = "./manifest-sqzm4681.yaml";
1248
+ var manifest_default2 = "./manifest-wbcv8cbp.yaml";
1171
1249
 
1172
1250
  // src/assets/templates/zine/chapter-01.md
1173
1251
  var chapter_01_default2 = "./chapter-01-wgjfz4w6.md";
1174
1252
 
1175
1253
  // src/assets/templates/technical/manifest.yaml
1176
- var manifest_default3 = "./manifest-2kc56wvw.yaml";
1254
+ var manifest_default3 = "./manifest-aya31jwt.yaml";
1177
1255
 
1178
1256
  // src/assets/templates/technical/chapter-01.md
1179
1257
  var chapter_01_default3 = "./chapter-01-6fn9pvd0.md";
1180
1258
 
1181
1259
  // src/assets/templates/paper/manifest.yaml
1182
- var manifest_default4 = "./manifest-79zsgkpn.yaml";
1260
+ var manifest_default4 = "./manifest-0vwaskj2.yaml";
1183
1261
 
1184
1262
  // src/assets/templates/paper/chapter-01.md
1185
1263
  var chapter_01_default4 = "./chapter-01-s71xrf5q.md";
@@ -1212,7 +1290,7 @@ var plugin_test_js_default = "./plugin.test.js-gxzqqfdh.tpl";
1212
1290
  var package_default2 = "./package-awwkt7sa.json";
1213
1291
 
1214
1292
  // src/assets/extension-templates/look/README.md
1215
- var README_default2 = "./README-syg2hn83.md";
1293
+ var README_default2 = "./README-8rct3axy.md";
1216
1294
 
1217
1295
  // src/assets/extension-templates/look/components.yaml
1218
1296
  var components_default = "./components-zzqdpt3m.yaml";
@@ -1221,7 +1299,7 @@ var components_default = "./components-zzqdpt3m.yaml";
1221
1299
  var callout_default = "./callout-hv56m16h.md";
1222
1300
 
1223
1301
  // src/assets/extension-templates/look/styles/tokens.css
1224
- var tokens_default = "./tokens-n82t4k9h.css";
1302
+ var tokens_default = "./tokens-a7grv940.css";
1225
1303
 
1226
1304
  // src/assets/extension-templates/look/styles/base.css
1227
1305
  var base_default = "./base-ypqh9n7f.css";
@@ -1233,7 +1311,7 @@ var components_default2 = "./components-aam0fe2z.css";
1233
1311
  var page_templates_default = "./page-templates-tx9ryt19.css";
1234
1312
 
1235
1313
  // src/assets/extension-templates/look/styles/page-rules.css
1236
- var page_rules_default = "./page-rules-ytj9p04v.css";
1314
+ var page_rules_default = "./page-rules-vnja6grt.css";
1237
1315
 
1238
1316
  // src/assets/extension-templates/look/styles/book.css
1239
1317
  var book_default = "./book-ted3tzrp.css";
@@ -1341,9 +1419,9 @@ function pseudoVariants(model) {
1341
1419
  function consumedStrings(model) {
1342
1420
  const names = new Set;
1343
1421
  for (const rule of model.pageRules)
1344
- for (const decls of Object.values(rule.marginBoxes))
1345
- if (decls.content) {
1346
- for (const part of parseContent(decls.content))
1422
+ for (const decls2 of Object.values(rule.marginBoxes))
1423
+ if (decls2.content) {
1424
+ for (const part of parseContent(decls2.content))
1347
1425
  if (part.type === "string")
1348
1426
  names.add(part.name);
1349
1427
  }
@@ -1453,6 +1531,7 @@ function round(n) {
1453
1531
  import { PDFDocument as PDFDocument2, PDFName as PDFName2, PDFNumber as PDFNumber2, rgb } from "pdf-lib";
1454
1532
  async function postprocess(input, opts) {
1455
1533
  const doc = await PDFDocument2.load(input, { updateMetadata: false });
1534
+ const scale = printScale(doc);
1456
1535
  const g = opts.geometry;
1457
1536
  const inset = g.bleed + g.slug;
1458
1537
  const media = [0, 0, g.media.width, g.media.height];
@@ -1499,7 +1578,8 @@ async function postprocess(input, opts) {
1499
1578
  bytes,
1500
1579
  pageCount: doc.getPageCount(),
1501
1580
  padded,
1502
- boxes: { media, bleed: bleedBox, trim: trimBox }
1581
+ boxes: { media, bleed: bleedBox, trim: trimBox },
1582
+ scale
1503
1583
  };
1504
1584
  }
1505
1585
  function drawCropMarks(page, g) {
@@ -1674,7 +1754,8 @@ async function build(opts) {
1674
1754
  key: root.key
1675
1755
  });
1676
1756
  }
1677
- const printArea = pinRoots.length ? await page.evaluate(`window.__gp.printArea()`) : null;
1757
+ const printArea = await page.evaluate(`window.__gp.printArea()`);
1758
+ const geometryVarsCss = pageGeometryVarsCss(model, printArea);
1678
1759
  const pinStyle = (group, pseudos) => pinRootStyle(resolvePage(model, { name: group.authorPage, pseudos }).geometry.margin, printArea, pseudos.includes("left") ? "verso" : "recto", group.edges, group.padding);
1679
1760
  if (pinGroups.size) {
1680
1761
  const lines = [];
@@ -1694,11 +1775,11 @@ async function build(opts) {
1694
1775
  const body = [];
1695
1776
  for (const [prop, value] of Object.entries(rule.decls))
1696
1777
  body.push(` ${prop}: ${value};`);
1697
- for (const [box, decls] of Object.entries(rule.marginBoxes)) {
1778
+ for (const [box, decls2] of Object.entries(rule.marginBoxes)) {
1698
1779
  if (relocated.has(box.slice(1)))
1699
1780
  continue;
1700
1781
  body.push(` ${box} {`);
1701
- for (const [prop, value] of Object.entries(decls))
1782
+ for (const [prop, value] of Object.entries(decls2))
1702
1783
  body.push(` ${prop}: ${value};`);
1703
1784
  body.push(` }`);
1704
1785
  }
@@ -1741,11 +1822,12 @@ ${pinCss}`);
1741
1822
  const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
1742
1823
  const pageVarsCss = [
1743
1824
  `:root { --gp-content-h: ${contentHeightPx}px; }`,
1744
- ...model.pageAssignments.map((a) => `${a.selector} { --gp-content-h: ${namedContentHeightsPx[a.page] ?? contentHeightPx}px; }`)
1825
+ ...model.pageAssignments.map((a) => `${a.selector} { --gp-content-h: ${namedContentHeightsPx[a.page] ?? contentHeightPx}px; }`),
1826
+ geometryVarsCss
1745
1827
  ].join(`
1746
1828
  `);
1747
1829
  await page.evaluate(`window.__gp.addCss("gp-page-vars", ${JSON.stringify(pageVarsCss)})`);
1748
- log(`page geometry published (--gp-content-h: ${Math.round(contentHeightPx)}px)`);
1830
+ log(`page geometry published (--gp-content-h: ${Math.round(contentHeightPx)}px, --gp-page-*)`);
1749
1831
  const relocationBoxes = (group, pseudos) => {
1750
1832
  const ctx = resolvePage(model, { name: group.authorPage, pseudos });
1751
1833
  return marginBoxesOnEdges(group.edges).map((name) => [name, ctx.marginBoxes[`@${name}`]]).filter((pair) => {
@@ -1788,8 +1870,6 @@ ${pinCss}`);
1788
1870
  const msg = `${headline}:
1789
1871
  ${describe(widthOffenders.boxes)}`;
1790
1872
  if (opts.allowShrink) {
1791
- if (scale !== null)
1792
- diagnose("engine.width.overflow", `${headline}.`);
1793
1873
  for (const o of widthOffenders.boxes)
1794
1874
  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."}`);
1795
1875
  log(`WARNING: ${msg}`);
@@ -1805,6 +1885,54 @@ ${describe(widthOffenders.intrinsics)}`;
1805
1885
  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%).`);
1806
1886
  log(`WARNING: ${msg}`);
1807
1887
  }
1888
+ let unclipped = null;
1889
+ if (!widthOffenders.boxes.length) {
1890
+ const g = tier2.geometry;
1891
+ const ext = g.bleed + g.slug;
1892
+ const contentPx = (name, pseudos) => {
1893
+ const { width, margin } = resolvePage(model, { name: name || undefined, pseudos }).geometry;
1894
+ return (width + 2 * ext - bleedMargin(margin.left, g) - bleedMargin(margin.right, g)) * 96 / 72;
1895
+ };
1896
+ const pages = await page.evaluate(`(() => {
1897
+ const body = document.body;
1898
+ const named = (el) => { const p = getComputedStyle(el).page; return p && p !== "auto" ? p : ""; };
1899
+ if (!body) return { first: "", used: [], reason: "there is no body" };
1900
+ const bs = getComputedStyle(body);
1901
+ const r = body.getBoundingClientRect();
1902
+ if (
1903
+ Math.abs(r.left + parseFloat(bs.borderLeftWidth)) > 0.01 ||
1904
+ Math.abs(r.right - parseFloat(bs.borderRightWidth) - window.innerWidth) > 0.01
1905
+ ) return { first: "", used: [], reason: "the body does not span the page content box" };
1906
+ // The first page's name: the root's, overridden down the chain of
1907
+ // first in-flow children (CSS Paged Media's start page value).
1908
+ let first = named(document.documentElement) || named(body);
1909
+ for (let el = body; el; ) {
1910
+ el = [...el.children].find((c) => {
1911
+ const cs = getComputedStyle(c);
1912
+ return cs.display !== "none" && cs.position !== "absolute" && cs.position !== "fixed";
1913
+ });
1914
+ if (el && named(el)) first = named(el);
1915
+ }
1916
+ const used = new Set([""]);
1917
+ for (const el of document.querySelectorAll("*")) used.add(named(el));
1918
+ return { first, used: [...used], reason: "" };
1919
+ })()`);
1920
+ const firstPx = contentPx(pages.first, ["first", "right", "recto"]);
1921
+ const wider = pages.used.find((name) => pseudoContexts.some((pseudos) => contentPx(name, pseudos) > firstPx + 0.01));
1922
+ unclipped = pages.reason || (wider !== undefined ? `${wider ? `the page "${wider}"` : "the unnamed page"} is wider than the first page` : await page.evaluate(`(() => {
1923
+ const hs = getComputedStyle(document.documentElement);
1924
+ const bs = getComputedStyle(document.body);
1925
+ let css;
1926
+ if (hs.overflowX === "visible" && hs.overflowY === "visible")
1927
+ css = "html { overflow-x: clip !important; } " +
1928
+ "body { overflow-x: clip !important; overflow-y: visible !important; }";
1929
+ else if (bs.overflowX === "visible") css = "body { overflow-x: clip !important; }";
1930
+ else return "the body clips its own overflow";
1931
+ window.__gp.addCss("gp-root-clip", css);
1932
+ return "";
1933
+ })()`)) || null;
1934
+ log(`body overflow ${unclipped ? `left as authored: ${unclipped}` : "held to the page content box"}`);
1935
+ }
1808
1936
  const rectoDecls = model.breaks.filter(isRectoVersoBreak);
1809
1937
  const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0 || furnitureRoots.length > 0 || sideRoots.length > 0;
1810
1938
  let tier = tier2.geometry.bleed > 0 || tier2.geometry.slug > 0 ? 2 : 1;
@@ -1949,8 +2077,8 @@ ${mapCss}`;
1949
2077
  const g = ctx.geometry;
1950
2078
  const eff = flushMargins(g.margin, group.edges);
1951
2079
  const boxes = [];
1952
- for (const [name, decls] of relocationBoxes(group, pseudos)) {
1953
- const text = evaluate(decls.content, {
2080
+ for (const [name, decls2] of relocationBoxes(group, pseudos)) {
2081
+ const text = evaluate(decls2.content, {
1954
2082
  page: toFolioPage(physical, pageValues),
1955
2083
  pages: pageCount,
1956
2084
  strings: (n, w) => stringValueAt(byName.get(n) ?? [], physical, parseWhich(w)),
@@ -1962,13 +2090,13 @@ ${mapCss}`;
1962
2090
  if (!text) {
1963
2091
  if (!relocationDiagnosed.has(name)) {
1964
2092
  relocationDiagnosed.add(name);
1965
- diagnose("engine.pin.margin-box", `@${name} sits on an edge a pinned image frees, and its content could not be ` + `re-homed into the page (unsupported content value: ${decls.content}). It will ` + `not print on that page — simplify the box's content, or pin the image to another edge.`);
2093
+ diagnose("engine.pin.margin-box", `@${name} sits on an edge a pinned image frees, and its content could not be ` + `re-homed into the page (unsupported content value: ${decls2.content}). It will ` + `not print on that page — simplify the box's content, or pin the image to another edge.`);
1966
2094
  }
1967
2095
  continue;
1968
2096
  }
1969
2097
  const r = marginBoxRectPt(name, g);
1970
2098
  const outDecls = {};
1971
- for (const [prop, value] of Object.entries(decls)) {
2099
+ for (const [prop, value] of Object.entries(decls2)) {
1972
2100
  if (prop.toLowerCase() === "content" || isIgnoredMarginBoxProperty(prop))
1973
2101
  continue;
1974
2102
  outDecls[prop] = value;
@@ -2326,6 +2454,20 @@ ${mapCss}`;
2326
2454
  author: opts.author
2327
2455
  });
2328
2456
  log(`postprocess: ${post.pageCount} pages` + (post.padded ? ` (+${post.padded} blank${opts.lastPage ? `; last page: ${opts.lastPage}` : ""})` : ""));
2457
+ {
2458
+ const scale = post.scale;
2459
+ if (scale !== null && scale < 1) {
2460
+ const pct = (scale * 100).toPrecision(6);
2461
+ const overPx = widthOffenders.defaultLimitPx * (1 / scale - 1);
2462
+ 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}.` : ""));
2463
+ if (!widthOffenders.boxes.length) {
2464
+ const named = await findWidthOffenders(page, model, 2 * (tier2.geometry.bleed + tier2.geometry.slug), sheetViewport, 0);
2465
+ for (const o of named.boxes)
2466
+ 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.`);
2467
+ }
2468
+ log(`audit: printed at ${pct}% (shrink-to-fit)`);
2469
+ }
2470
+ }
2329
2471
  return {
2330
2472
  bytes: post.bytes,
2331
2473
  tier,
@@ -2360,7 +2502,7 @@ function shrinkScale(boxes, limitPx) {
2360
2502
  return null;
2361
2503
  return Math.max(1 / MAX_SHRINK, limitPx / maxRight);
2362
2504
  }
2363
- async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport) {
2505
+ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport, tolerancePx = 1) {
2364
2506
  const contexts = [
2365
2507
  resolvePage(model),
2366
2508
  ...model.pageNames.filter((n) => !n.startsWith("gp-") || n.startsWith("gp--pin-")).map((n) => resolvePage(model, { name: n }))
@@ -2386,7 +2528,8 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
2386
2528
  mobile: false
2387
2529
  });
2388
2530
  boxes.push(...JSON.parse(await page.evaluate(`(() => {
2389
- const LIMIT = ${widthPx} + 1;
2531
+ const TOL = ${tolerancePx};
2532
+ const LIMIT = Math.max(${widthPx} + TOL, ${Math.ceil(widthPx)});
2390
2533
  const NAMED = ${JSON.stringify(namedLimits)};
2391
2534
  const DEFAULT_LIMIT = ${defaultLimitPx};
2392
2535
  // The page an element lands on: the nearest ancestor with a non-auto
@@ -2426,10 +2569,10 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
2426
2569
  // pulls) AND left-edge protrusion (negative left with an ordinary
2427
2570
  // width) — both trigger the same whole-document shrink-to-fit,
2428
2571
  // measured: a left bleed alone cost 16%.
2429
- if (right <= LIMIT && r.width <= LIMIT && r.left >= -1) continue;
2572
+ if (right <= LIMIT && r.width <= LIMIT && r.left >= -TOL) continue;
2430
2573
  // ...but only where it actually escapes its clipping ancestors.
2431
2574
  const clip = clipEdges(el).edges;
2432
- if (Math.min(right, clip.right) <= LIMIT && Math.max(r.left, clip.left) >= -1) continue;
2575
+ if (Math.min(right, clip.right) <= LIMIT && Math.max(r.left, clip.left) >= -TOL) continue;
2433
2576
  let deepest = true;
2434
2577
  for (const c of el.children) {
2435
2578
  if (c.getBoundingClientRect().width >= r.width - 1) { deepest = false; break; }
@@ -2466,7 +2609,7 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
2466
2609
  const over = el.scrollWidth - el.clientWidth;
2467
2610
  // clientWidth is 0 on inline/replaced boxes, where scrollWidth
2468
2611
  // carries no overflow information -- skip rather than guess.
2469
- if (el.clientWidth <= 0 || over <= 1) continue;
2612
+ if (el.clientWidth <= 0 || over <= TOL) continue;
2470
2613
  // A descendant assigned to a DIFFERENT page is not overflowing this
2471
2614
  // element's page -- it is laid out against its own, wider one. The
2472
2615
  // full-bleed art plate is the standard case: it is legitimately
@@ -2520,7 +2663,7 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
2520
2663
  }
2521
2664
  return JSON.stringify(out);
2522
2665
  })()`));
2523
- return { limitPx, boxes, intrinsics: replaced };
2666
+ return { limitPx, defaultLimitPx, boxes, intrinsics: replaced };
2524
2667
  } finally {
2525
2668
  await page.send("Emulation.setDeviceMetricsOverride", restoreViewport);
2526
2669
  }
@@ -2603,10 +2746,10 @@ function counterStyleCss(model, sources, pageMap, pageCount, resetSites = [], ha
2603
2746
  return "";
2604
2747
  const pairs = new Map;
2605
2748
  for (const rule of model.pageRules) {
2606
- for (const decls of Object.values(rule.marginBoxes)) {
2607
- if (!decls.content)
2749
+ for (const decls2 of Object.values(rule.marginBoxes)) {
2750
+ if (!decls2.content)
2608
2751
  continue;
2609
- for (const part of parseContent(decls.content)) {
2752
+ for (const part of parseContent(decls2.content)) {
2610
2753
  if (part.type !== "string" || !byName.has(part.name))
2611
2754
  continue;
2612
2755
  const which = parseWhich(part.which);
@@ -2645,13 +2788,13 @@ function counterStyleCss(model, sources, pageMap, pageCount, resetSites = [], ha
2645
2788
  for (const pseudos of variants) {
2646
2789
  const resolved = resolvePage(model, { name, pseudos });
2647
2790
  const lines = [];
2648
- for (const [box, decls] of Object.entries(resolved.marginBoxes)) {
2649
- if (!decls.content)
2791
+ for (const [box, decls2] of Object.entries(resolved.marginBoxes)) {
2792
+ if (!decls2.content)
2650
2793
  continue;
2651
- const hasString = /\bstring\s*\(/.test(decls.content);
2652
- const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls.content);
2794
+ const hasString = /\bstring\s*\(/.test(decls2.content);
2795
+ const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls2.content);
2653
2796
  const needsRewrite = hasString || hasPageCounter;
2654
- lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls.content) : decls.content}; }`);
2797
+ lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls2.content) : decls2.content}; }`);
2655
2798
  }
2656
2799
  if (!lines.length)
2657
2800
  continue;
@@ -2665,13 +2808,13 @@ ${lines.join(`
2665
2808
  if (hasBlank) {
2666
2809
  const blank = resolvePage(model, { pseudos: ["blank"] });
2667
2810
  const lines = [];
2668
- for (const [box, decls] of Object.entries(blank.marginBoxes)) {
2669
- if (!decls.content)
2811
+ for (const [box, decls2] of Object.entries(blank.marginBoxes)) {
2812
+ if (!decls2.content)
2670
2813
  continue;
2671
- const hasString = /\bstring\s*\(/.test(decls.content);
2672
- const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls.content);
2814
+ const hasString = /\bstring\s*\(/.test(decls2.content);
2815
+ const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls2.content);
2673
2816
  const needsRewrite = hasString || hasPageCounter;
2674
- lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls.content) : decls.content}; }`);
2817
+ lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls2.content) : decls2.content}; }`);
2675
2818
  }
2676
2819
  out.push(`@page ${BLANK_PAGE} {
2677
2820
  ${lines.length ? lines.join(`