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
@@ -19,7 +19,10 @@ import {
19
19
  stringValueAt,
20
20
  toFolioPage,
21
21
  wantsRecto
22
- } from "./index-2vxzxyyj.js";
22
+ } from "./index-swfdgn2n.js";
23
+ import {
24
+ round3
25
+ } from "./index-tayzfr9c.js";
23
26
  import {
24
27
  launchChromium
25
28
  } from "./index-bt0qws5c.js";
@@ -86,6 +89,33 @@ function marginBoxesOnEdges(edges) {
86
89
  return Object.entries(owners).filter(([, own]) => own.some((e) => edges.includes(e))).map(([name]) => name);
87
90
  }
88
91
 
92
+ // src/engine/shared/page-vars.ts
93
+ var decls = (pairs) => pairs.map(([name, n]) => `--gp-page-${name}: ${round3(n)}pt;`).join(" ");
94
+ function pageDecls(model, name) {
95
+ const { width, height, margin } = resolvePage(model, { name, pseudos: ["right"] }).geometry;
96
+ return decls([
97
+ ["width", width],
98
+ ["height", height],
99
+ ["margin-top", margin.top],
100
+ ["margin-bottom", margin.bottom],
101
+ ["margin-inside", margin.left],
102
+ ["margin-outside", margin.right]
103
+ ]);
104
+ }
105
+ function pageGeometryVarsCss(model, area) {
106
+ const bookWide = decls([
107
+ ["bleed", area?.bleed ?? 0],
108
+ ["safe", area?.safe ?? 0],
109
+ ["gutter", area?.gutter ?? 0],
110
+ ["art-safe", area?.artSafe ?? 0]
111
+ ]);
112
+ return [
113
+ `:root { ${pageDecls(model)} ${bookWide} }`,
114
+ ...model.pageAssignments.map((a) => `${a.selector} { ${pageDecls(model, a.page)} }`)
115
+ ].join(`
116
+ `);
117
+ }
118
+
89
119
  // src/engine/shared/content-value.ts
90
120
  var FUNC = /^([a-z-]+)\(/i;
91
121
  function parseContent(value) {
@@ -282,12 +312,14 @@ function needsMeasurement(value) {
282
312
 
283
313
  // src/engine/shared/pdf-inspect.ts
284
314
  import {
315
+ decodePDFRawStream,
285
316
  PDFArray,
286
317
  PDFDict,
287
318
  PDFDocument,
288
319
  PDFHexString,
289
320
  PDFName,
290
321
  PDFNumber,
322
+ PDFRawStream,
291
323
  PDFRef,
292
324
  PDFString
293
325
  } from "pdf-lib";
@@ -408,6 +440,52 @@ async function inspectPdf(bytes) {
408
440
  walkOutline(outlines);
409
441
  return { pageCount: pages.length, boxes, linkTargets, namedDests, outline };
410
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
+ }
411
489
 
412
490
  // src/engine/compiler/tier2.ts
413
491
  var DEFAULT_SLUG_PT = 18;
@@ -442,9 +520,9 @@ function pseudoVariants(model) {
442
520
  function consumedStrings(model) {
443
521
  const names = new Set;
444
522
  for (const rule of model.pageRules)
445
- for (const decls of Object.values(rule.marginBoxes))
446
- if (decls.content) {
447
- for (const part of parseContent(decls.content))
523
+ for (const decls2 of Object.values(rule.marginBoxes))
524
+ if (decls2.content) {
525
+ for (const part of parseContent(decls2.content))
448
526
  if (part.type === "string")
449
527
  names.add(part.name);
450
528
  }
@@ -554,6 +632,7 @@ function round(n) {
554
632
  import { PDFDocument as PDFDocument2, PDFName as PDFName2, PDFNumber as PDFNumber2, rgb } from "pdf-lib";
555
633
  async function postprocess(input, opts) {
556
634
  const doc = await PDFDocument2.load(input, { updateMetadata: false });
635
+ const scale = printScale(doc);
557
636
  const g = opts.geometry;
558
637
  const inset = g.bleed + g.slug;
559
638
  const media = [0, 0, g.media.width, g.media.height];
@@ -600,7 +679,8 @@ async function postprocess(input, opts) {
600
679
  bytes,
601
680
  pageCount: doc.getPageCount(),
602
681
  padded,
603
- boxes: { media, bleed: bleedBox, trim: trimBox }
682
+ boxes: { media, bleed: bleedBox, trim: trimBox },
683
+ scale
604
684
  };
605
685
  }
606
686
  function drawCropMarks(page, g) {
@@ -788,7 +868,8 @@ async function build(opts) {
788
868
  key: root.key
789
869
  });
790
870
  }
791
- const printArea = pinRoots.length ? await page.evaluate(`window.__gp.printArea()`) : null;
871
+ const printArea = await page.evaluate(`window.__gp.printArea()`);
872
+ const geometryVarsCss = pageGeometryVarsCss(model, printArea);
792
873
  const pinStyle = (group, pseudos) => pinRootStyle(resolvePage(model, { name: group.authorPage, pseudos }).geometry.margin, printArea, pseudos.includes("left") ? "verso" : "recto", group.edges, group.padding);
793
874
  if (pinGroups.size) {
794
875
  const lines = [];
@@ -808,11 +889,11 @@ async function build(opts) {
808
889
  const body = [];
809
890
  for (const [prop, value] of Object.entries(rule.decls))
810
891
  body.push(` ${prop}: ${value};`);
811
- for (const [box, decls] of Object.entries(rule.marginBoxes)) {
892
+ for (const [box, decls2] of Object.entries(rule.marginBoxes)) {
812
893
  if (relocated.has(box.slice(1)))
813
894
  continue;
814
895
  body.push(` ${box} {`);
815
- for (const [prop, value] of Object.entries(decls))
896
+ for (const [prop, value] of Object.entries(decls2))
816
897
  body.push(` ${prop}: ${value};`);
817
898
  body.push(` }`);
818
899
  }
@@ -855,11 +936,12 @@ ${pinCss}`);
855
936
  const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
856
937
  const pageVarsCss = [
857
938
  `:root { --gp-content-h: ${contentHeightPx}px; }`,
858
- ...model.pageAssignments.map((a) => `${a.selector} { --gp-content-h: ${namedContentHeightsPx[a.page] ?? contentHeightPx}px; }`)
939
+ ...model.pageAssignments.map((a) => `${a.selector} { --gp-content-h: ${namedContentHeightsPx[a.page] ?? contentHeightPx}px; }`),
940
+ geometryVarsCss
859
941
  ].join(`
860
942
  `);
861
943
  await page.evaluate(`window.__gp.addCss("gp-page-vars", ${JSON.stringify(pageVarsCss)})`);
862
- log(`page geometry published (--gp-content-h: ${Math.round(contentHeightPx)}px)`);
944
+ log(`page geometry published (--gp-content-h: ${Math.round(contentHeightPx)}px, --gp-page-*)`);
863
945
  const relocationBoxes = (group, pseudos) => {
864
946
  const ctx = resolvePage(model, { name: group.authorPage, pseudos });
865
947
  return marginBoxesOnEdges(group.edges).map((name) => [name, ctx.marginBoxes[`@${name}`]]).filter((pair) => {
@@ -902,8 +984,6 @@ ${pinCss}`);
902
984
  const msg = `${headline}:
903
985
  ${describe(widthOffenders.boxes)}`;
904
986
  if (opts.allowShrink) {
905
- if (scale !== null)
906
- diagnose("engine.width.overflow", `${headline}.`);
907
987
  for (const o of widthOffenders.boxes)
908
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."}`);
909
989
  log(`WARNING: ${msg}`);
@@ -919,6 +999,54 @@ ${describe(widthOffenders.intrinsics)}`;
919
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%).`);
920
1000
  log(`WARNING: ${msg}`);
921
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
+ }
922
1050
  const rectoDecls = model.breaks.filter(isRectoVersoBreak);
923
1051
  const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0 || furnitureRoots.length > 0 || sideRoots.length > 0;
924
1052
  let tier = tier2.geometry.bleed > 0 || tier2.geometry.slug > 0 ? 2 : 1;
@@ -1063,8 +1191,8 @@ ${mapCss}`;
1063
1191
  const g = ctx.geometry;
1064
1192
  const eff = flushMargins(g.margin, group.edges);
1065
1193
  const boxes = [];
1066
- for (const [name, decls] of relocationBoxes(group, pseudos)) {
1067
- const text = evaluate(decls.content, {
1194
+ for (const [name, decls2] of relocationBoxes(group, pseudos)) {
1195
+ const text = evaluate(decls2.content, {
1068
1196
  page: toFolioPage(physical, pageValues),
1069
1197
  pages: pageCount,
1070
1198
  strings: (n, w) => stringValueAt(byName.get(n) ?? [], physical, parseWhich(w)),
@@ -1076,13 +1204,13 @@ ${mapCss}`;
1076
1204
  if (!text) {
1077
1205
  if (!relocationDiagnosed.has(name)) {
1078
1206
  relocationDiagnosed.add(name);
1079
- 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.`);
1207
+ 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.`);
1080
1208
  }
1081
1209
  continue;
1082
1210
  }
1083
1211
  const r = marginBoxRectPt(name, g);
1084
1212
  const outDecls = {};
1085
- for (const [prop, value] of Object.entries(decls)) {
1213
+ for (const [prop, value] of Object.entries(decls2)) {
1086
1214
  if (prop.toLowerCase() === "content" || isIgnoredMarginBoxProperty(prop))
1087
1215
  continue;
1088
1216
  outDecls[prop] = value;
@@ -1440,6 +1568,20 @@ ${mapCss}`;
1440
1568
  author: opts.author
1441
1569
  });
1442
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
+ }
1443
1585
  return {
1444
1586
  bytes: post.bytes,
1445
1587
  tier,
@@ -1474,7 +1616,7 @@ function shrinkScale(boxes, limitPx) {
1474
1616
  return null;
1475
1617
  return Math.max(1 / MAX_SHRINK, limitPx / maxRight);
1476
1618
  }
1477
- async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport) {
1619
+ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport, tolerancePx = 1) {
1478
1620
  const contexts = [
1479
1621
  resolvePage(model),
1480
1622
  ...model.pageNames.filter((n) => !n.startsWith("gp-") || n.startsWith("gp--pin-")).map((n) => resolvePage(model, { name: n }))
@@ -1500,7 +1642,8 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
1500
1642
  mobile: false
1501
1643
  });
1502
1644
  boxes.push(...JSON.parse(await page.evaluate(`(() => {
1503
- const LIMIT = ${widthPx} + 1;
1645
+ const TOL = ${tolerancePx};
1646
+ const LIMIT = Math.max(${widthPx} + TOL, ${Math.ceil(widthPx)});
1504
1647
  const NAMED = ${JSON.stringify(namedLimits)};
1505
1648
  const DEFAULT_LIMIT = ${defaultLimitPx};
1506
1649
  // The page an element lands on: the nearest ancestor with a non-auto
@@ -1540,10 +1683,10 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
1540
1683
  // pulls) AND left-edge protrusion (negative left with an ordinary
1541
1684
  // width) — both trigger the same whole-document shrink-to-fit,
1542
1685
  // measured: a left bleed alone cost 16%.
1543
- if (right <= LIMIT && r.width <= LIMIT && r.left >= -1) continue;
1686
+ if (right <= LIMIT && r.width <= LIMIT && r.left >= -TOL) continue;
1544
1687
  // ...but only where it actually escapes its clipping ancestors.
1545
1688
  const clip = clipEdges(el).edges;
1546
- 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;
1547
1690
  let deepest = true;
1548
1691
  for (const c of el.children) {
1549
1692
  if (c.getBoundingClientRect().width >= r.width - 1) { deepest = false; break; }
@@ -1580,7 +1723,7 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
1580
1723
  const over = el.scrollWidth - el.clientWidth;
1581
1724
  // clientWidth is 0 on inline/replaced boxes, where scrollWidth
1582
1725
  // carries no overflow information -- skip rather than guess.
1583
- if (el.clientWidth <= 0 || over <= 1) continue;
1726
+ if (el.clientWidth <= 0 || over <= TOL) continue;
1584
1727
  // A descendant assigned to a DIFFERENT page is not overflowing this
1585
1728
  // element's page -- it is laid out against its own, wider one. The
1586
1729
  // full-bleed art plate is the standard case: it is legitimately
@@ -1634,7 +1777,7 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
1634
1777
  }
1635
1778
  return JSON.stringify(out);
1636
1779
  })()`));
1637
- return { limitPx, boxes, intrinsics: replaced };
1780
+ return { limitPx, defaultLimitPx, boxes, intrinsics: replaced };
1638
1781
  } finally {
1639
1782
  await page.send("Emulation.setDeviceMetricsOverride", restoreViewport);
1640
1783
  }
@@ -1717,10 +1860,10 @@ function counterStyleCss(model, sources, pageMap, pageCount, resetSites = [], ha
1717
1860
  return "";
1718
1861
  const pairs = new Map;
1719
1862
  for (const rule of model.pageRules) {
1720
- for (const decls of Object.values(rule.marginBoxes)) {
1721
- if (!decls.content)
1863
+ for (const decls2 of Object.values(rule.marginBoxes)) {
1864
+ if (!decls2.content)
1722
1865
  continue;
1723
- for (const part of parseContent(decls.content)) {
1866
+ for (const part of parseContent(decls2.content)) {
1724
1867
  if (part.type !== "string" || !byName.has(part.name))
1725
1868
  continue;
1726
1869
  const which = parseWhich(part.which);
@@ -1759,13 +1902,13 @@ function counterStyleCss(model, sources, pageMap, pageCount, resetSites = [], ha
1759
1902
  for (const pseudos of variants) {
1760
1903
  const resolved = resolvePage(model, { name, pseudos });
1761
1904
  const lines = [];
1762
- for (const [box, decls] of Object.entries(resolved.marginBoxes)) {
1763
- if (!decls.content)
1905
+ for (const [box, decls2] of Object.entries(resolved.marginBoxes)) {
1906
+ if (!decls2.content)
1764
1907
  continue;
1765
- const hasString = /\bstring\s*\(/.test(decls.content);
1766
- const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls.content);
1908
+ const hasString = /\bstring\s*\(/.test(decls2.content);
1909
+ const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls2.content);
1767
1910
  const needsRewrite = hasString || hasPageCounter;
1768
- lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls.content) : decls.content}; }`);
1911
+ lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls2.content) : decls2.content}; }`);
1769
1912
  }
1770
1913
  if (!lines.length)
1771
1914
  continue;
@@ -1779,13 +1922,13 @@ ${lines.join(`
1779
1922
  if (hasBlank) {
1780
1923
  const blank = resolvePage(model, { pseudos: ["blank"] });
1781
1924
  const lines = [];
1782
- for (const [box, decls] of Object.entries(blank.marginBoxes)) {
1783
- if (!decls.content)
1925
+ for (const [box, decls2] of Object.entries(blank.marginBoxes)) {
1926
+ if (!decls2.content)
1784
1927
  continue;
1785
- const hasString = /\bstring\s*\(/.test(decls.content);
1786
- const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls.content);
1928
+ const hasString = /\bstring\s*\(/.test(decls2.content);
1929
+ const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls2.content);
1787
1930
  const needsRewrite = hasString || hasPageCounter;
1788
- lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls.content) : decls.content}; }`);
1931
+ lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls2.content) : decls2.content}; }`);
1789
1932
  }
1790
1933
  out.push(`@page ${BLANK_PAGE} {
1791
1934
  ${lines.length ? lines.join(`
@@ -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 };
@@ -398,7 +398,7 @@ import { satisfies, validRange } from "semver";
398
398
  // package.json
399
399
  var package_default = {
400
400
  name: "gutterpress",
401
- version: "0.11.23-alpha.7",
401
+ version: "0.11.23-beta.2",
402
402
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
403
403
  author: "itlackey",
404
404
  license: "MPL-2.0",
@@ -2122,8 +2122,9 @@ img.gp-shape {
2122
2122
 
2123
2123
  /* Math (the bundled gutterpress-math feature, math.ts): MathJax's own rules
2124
2124
  for the <mjx-container> SVG output it emits, from its SVG output jax's
2125
- stylesheet, less the ones for its interactive menu and tooltips. Inert in
2126
- a book without formulas. */
2125
+ stylesheet, less the ones for its interactive menu and tooltips, plus one
2126
+ bound of ours on the hidden MathML copy (#469, below). Inert in a book
2127
+ without formulas. */
2127
2128
  mjx-container[jax="SVG"] { direction: ltr; }
2128
2129
  mjx-container[jax="SVG"] > svg { overflow: visible; min-height: 1px; min-width: 1px; }
2129
2130
  mjx-container[jax="SVG"] > svg a { fill: blue; stroke: blue; }
@@ -2140,6 +2141,15 @@ mjx-assistive-mml {
2140
2141
  border: 0px !important;
2141
2142
  display: block !important;
2142
2143
  width: auto !important;
2144
+ /* Ours (#469). The browser lays the copy out in whatever math font it has,
2145
+ so it can be wider than the formula's SVG; ending a line, it crossed the
2146
+ column edge and Chromium shrank the whole print to fit. MathJax makes the
2147
+ container position: relative with the copy at its top left, so 100% is
2148
+ the formula's width. Clip and overflow still hide the copy and its MathML
2149
+ still reaches screen readers; only its box changes. Not the 1px
2150
+ visually-hidden box MathJax 2 used: its docs note VoiceOver on iOS could
2151
+ not reach a formula by touch in it. */
2152
+ max-width: 100% !important;
2143
2153
  overflow: hidden !important;
2144
2154
  user-select: none;
2145
2155
  }
@@ -3747,9 +3757,9 @@ async function loadNpmPackage(packageName, baseDir, version) {
3747
3757
  const looksLikeJsFilename = /\.(m?js|cjs)$/i.test(packageName);
3748
3758
  const suggestedPath = looksLikeJsFilename ? `./${packageName}` : `./plugins/${packageName}.js`;
3749
3759
  if (!looksLikeJsFilename && (!version || !isExactNpmVersion(version))) {
3750
- throw new Error(`Extension "${packageName}${version ? `@${version}` : ""}" is not pinned to an exact version, so it can't be downloaded automatically. Run \`gutterpress ext add ${packageName}\` (or use Book setup > Advanced in the desktop app) to download it and pin its version in your manifest.`);
3760
+ throw new Error(`Extension "${packageName}${version ? `@${version}` : ""}" is not pinned to an exact version, so it can't be downloaded automatically. Run \`gutterpress ext add ${packageName}\` (or add it in Book setup) to download it and pin its version in your manifest.`);
3751
3761
  }
3752
- throw new Error(`Extension "${packageName}" not found. Install it with \`gutterpress ext add ${version ? `${packageName}@${version}` : packageName}\` (or Book setup > Advanced in the desktop app),
3762
+ throw new Error(`Extension "${packageName}" not found. Install it with \`gutterpress ext add ${version ? `${packageName}@${version}` : packageName}\` (or add it in Book setup),
3753
3763
  or reference a local file under \`extensions:\`:
3754
3764
  extensions:
3755
3765
  - ${suggestedPath}`);
@@ -697,7 +697,7 @@ import { fileURLToPath } from "node:url";
697
697
  var favicon_default = "./favicon-272ga6y0.ico";
698
698
 
699
699
  // src/assets/manifest.schema.json
700
- var manifest_schema_default = "./manifest.schema-93sd7zje.json";
700
+ var manifest_schema_default = "./manifest.schema-0jgj0jt1.json";
701
701
 
702
702
  // src/assets/preview/scripts/preview-interface.js
703
703
  var preview_interface_default = "./preview-interface-0xdh0r5b.js";
@@ -712,31 +712,31 @@ var preview_shell_default = "./preview-shell-wk9k3gcd.js";
712
712
  var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
713
713
 
714
714
  // src/assets/engine/gutterpress-viewer.js
715
- var gutterpress_viewer_default = "./gutterpress-viewer-mafsby5k.js";
715
+ var gutterpress_viewer_default = "./gutterpress-viewer-ywkf49q5.js";
716
716
 
717
717
  // src/assets/engine/gutterpress-agent.js
718
- var gutterpress_agent_default = "./gutterpress-agent-d29ry3ak.js";
718
+ var gutterpress_agent_default = "./gutterpress-agent-wyryqehx.js";
719
719
 
720
720
  // src/assets/templates/book/manifest.yaml
721
- var manifest_default = "./manifest-41ddzm13.yaml";
721
+ var manifest_default = "./manifest-mffah3hr.yaml";
722
722
 
723
723
  // src/assets/templates/book/chapter-01.md
724
724
  var chapter_01_default = "./chapter-01-gsbjt584.md";
725
725
 
726
726
  // src/assets/templates/zine/manifest.yaml
727
- var manifest_default2 = "./manifest-sqzm4681.yaml";
727
+ var manifest_default2 = "./manifest-wbcv8cbp.yaml";
728
728
 
729
729
  // src/assets/templates/zine/chapter-01.md
730
730
  var chapter_01_default2 = "./chapter-01-wgjfz4w6.md";
731
731
 
732
732
  // src/assets/templates/technical/manifest.yaml
733
- var manifest_default3 = "./manifest-2kc56wvw.yaml";
733
+ var manifest_default3 = "./manifest-aya31jwt.yaml";
734
734
 
735
735
  // src/assets/templates/technical/chapter-01.md
736
736
  var chapter_01_default3 = "./chapter-01-6fn9pvd0.md";
737
737
 
738
738
  // src/assets/templates/paper/manifest.yaml
739
- var manifest_default4 = "./manifest-79zsgkpn.yaml";
739
+ var manifest_default4 = "./manifest-0vwaskj2.yaml";
740
740
 
741
741
  // src/assets/templates/paper/chapter-01.md
742
742
  var chapter_01_default4 = "./chapter-01-s71xrf5q.md";
@@ -769,7 +769,7 @@ var plugin_test_js_default = "./plugin.test.js-gxzqqfdh.tpl";
769
769
  var package_default2 = "./package-awwkt7sa.json";
770
770
 
771
771
  // src/assets/extension-templates/look/README.md
772
- var README_default2 = "./README-syg2hn83.md";
772
+ var README_default2 = "./README-8rct3axy.md";
773
773
 
774
774
  // src/assets/extension-templates/look/components.yaml
775
775
  var components_default = "./components-zzqdpt3m.yaml";
@@ -778,7 +778,7 @@ var components_default = "./components-zzqdpt3m.yaml";
778
778
  var callout_default = "./callout-hv56m16h.md";
779
779
 
780
780
  // src/assets/extension-templates/look/styles/tokens.css
781
- var tokens_default = "./tokens-n82t4k9h.css";
781
+ var tokens_default = "./tokens-a7grv940.css";
782
782
 
783
783
  // src/assets/extension-templates/look/styles/base.css
784
784
  var base_default = "./base-ypqh9n7f.css";
@@ -790,7 +790,7 @@ var components_default2 = "./components-aam0fe2z.css";
790
790
  var page_templates_default = "./page-templates-tx9ryt19.css";
791
791
 
792
792
  // src/assets/extension-templates/look/styles/page-rules.css
793
- var page_rules_default = "./page-rules-ytj9p04v.css";
793
+ var page_rules_default = "./page-rules-vnja6grt.css";
794
794
 
795
795
  // src/assets/extension-templates/look/styles/book.css
796
796
  var book_default = "./book-ted3tzrp.css";