gutterpress 0.11.23-beta.1 → 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 (44) hide show
  1. package/README.md +1 -1
  2. package/dist/api/index.js +5 -5
  3. package/dist/{build-dg8a566j.js → build-3e18wnqj.js} +8 -8
  4. package/dist/{cli-vhav8g82.js → cli-0ep11b0q.js} +1 -1
  5. package/dist/{cli-e31jshz6.js → cli-88arpdke.js} +2 -2
  6. package/dist/{cli-1fy1pm9d.js → cli-9n9kg58f.js} +1 -1
  7. package/dist/{cli-k087gj0p.js → cli-pd2cje5b.js} +122 -11
  8. package/dist/{cli-989eg8jz.js → cli-pnrararp.js} +11 -10
  9. package/dist/{cli-ttj95aex.js → cli-tgnyvra3.js} +13 -3
  10. package/dist/{cli-31anr0r9.js → cli-v0evdpdf.js} +68 -28
  11. package/dist/{cli-s1zt779w.js → cli-x7gcdg6w.js} +1 -1
  12. package/dist/cli.js +12 -12
  13. package/dist/{doctor-g4a42tcz.js → doctor-2f4s9dyn.js} +3 -3
  14. package/dist/engine/compiler/postprocess.d.ts +3 -0
  15. package/dist/engine/shared/pdf-inspect.d.ts +33 -0
  16. package/dist/{engine-jvkcahr1.js → engine-c4pxz79g.js} +1 -1
  17. package/dist/{engine-wgkaqjsc.js → engine-cs5g2ez0.js} +2 -2
  18. package/dist/{ext-w9d5ftcg.js → ext-70gv86v5.js} +8 -8
  19. package/dist/{extension-restore-5x4mj79d.js → extension-restore-chj5nx3t.js} +2 -2
  20. package/dist/{extension-restore-as0g38p1.js → extension-restore-pd87p8r2.js} +4 -4
  21. package/dist/{gutterpress-agent-d29ry3ak.js → gutterpress-agent-wyryqehx.js} +13 -4
  22. package/dist/{gutterpress-viewer-ze691rrd.js → gutterpress-viewer-ywkf49q5.js} +38 -153
  23. package/dist/{index-c6y0p7jy.js → index-0k43evt0.js} +66 -22
  24. package/dist/{index-fk2kpf2t.js → index-2t1gdma7.js} +121 -10
  25. package/dist/{index-cw03e9p4.js → index-7j3hkwya.js} +68 -28
  26. package/dist/{index-a4j3xcfp.js → index-80vg3ed6.js} +13 -3
  27. package/dist/{index-798jt0qc.js → index-swfdgn2n.js} +2 -2
  28. package/dist/{index-rj5vrhz6.js → index-ytjkvwy5.js} +1 -1
  29. package/dist/index.js +6 -6
  30. package/dist/lib/markdown/gutterpress-css.d.ts +1 -1
  31. package/dist/lib/source-provider.d.ts +27 -0
  32. package/dist/lib/version-changes.d.ts +14 -4
  33. package/dist/{new-jtk028rf.js → new-d61n5cn0.js} +8 -8
  34. package/dist/{plugins-bcbe7xs8.js → plugins-0hhw6s17.js} +1 -1
  35. package/dist/{plugins-zsm6wv79.js → plugins-e0n46s6m.js} +3 -3
  36. package/dist/{preflight-cs6mj1d6.js → preflight-fjayhjr5.js} +8 -8
  37. package/dist/{preview-d1j1g4qf.js → preview-h815jrfb.js} +8 -8
  38. package/dist/{publish-7yw4jyrt.js → publish-ft39zgma.js} +8 -8
  39. package/dist/render.js +12 -2
  40. package/dist/{source-provider-n7vehh6t.js → source-provider-j1w7byeq.js} +5 -1
  41. package/dist/{source-provider-tgzet3j5.js → source-provider-tjaf303m.js} +5 -1
  42. package/dist/test-helpers/vocabulary.d.ts +12 -0
  43. package/dist/{validate-rkcdnzhw.js → validate-b201dbrb.js} +8 -8
  44. package/package.json +1 -1
@@ -29,7 +29,7 @@ import {
29
29
  validatePageMargin,
30
30
  withFetchTimeout,
31
31
  writeManifestDoc
32
- } from "./index-rj5vrhz6.js";
32
+ } from "./index-ytjkvwy5.js";
33
33
  import {
34
34
  EXTENSION_MANIFEST_FILENAME,
35
35
  GUTTERPRESS_CSS,
@@ -65,7 +65,7 @@ import {
65
65
  setLogLevel,
66
66
  slugify,
67
67
  warn
68
- } from "./index-a4j3xcfp.js";
68
+ } from "./index-80vg3ed6.js";
69
69
  import {
70
70
  blackTextToGray
71
71
  } from "./index-ht1bcqdd.js";
@@ -76,7 +76,7 @@ import {
76
76
  getAssetPath,
77
77
  lastPageMode,
78
78
  resolvePage
79
- } from "./index-798jt0qc.js";
79
+ } from "./index-swfdgn2n.js";
80
80
  import {
81
81
  BuildError,
82
82
  cssLength,
@@ -124,14 +124,16 @@ import {
124
124
  hasUncommittedChanges,
125
125
  isContained,
126
126
  listWorkdirChanges,
127
+ nextInHistory,
127
128
  readRevisionRefs,
128
129
  resolveGitAuthor,
129
130
  resolveLogger,
130
131
  revisionNameFrom,
131
132
  snapshotStagingMarkerPath,
132
133
  snapshotWorkingTreeUnlocked,
134
+ startHistoryWalk,
133
135
  withRepoLock
134
- } from "./index-cw03e9p4.js";
136
+ } from "./index-7j3hkwya.js";
135
137
  import {
136
138
  EXTENSIONS_DIR,
137
139
  resolveVendoredPluginInstallRoot,
@@ -5577,7 +5579,7 @@ class PdfOutput {
5577
5579
  const rawPdf = pdfxMode ? path7.join(stage, "raw.pdf") : path7.resolve(pdfFile);
5578
5580
  await fsp2.mkdir(path7.dirname(path7.resolve(pdfFile)), { recursive: true });
5579
5581
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
5580
- const { buildNativePdf } = await import("./engine-wgkaqjsc.js");
5582
+ const { buildNativePdf } = await import("./engine-cs5g2ez0.js");
5581
5583
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
5582
5584
  title: config.title,
5583
5585
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -7406,7 +7408,7 @@ async function scaffoldProject(options) {
7406
7408
  let versionHistoryError;
7407
7409
  if (requested === "local-git") {
7408
7410
  try {
7409
- const { providerFor } = await import("./source-provider-tgzet3j5.js");
7411
+ const { providerFor } = await import("./source-provider-tjaf303m.js");
7410
7412
  const provider = providerFor({ type: "local-folder", path: projectDir });
7411
7413
  await provider.initVersionHistory({
7412
7414
  projectDir,
@@ -7497,7 +7499,7 @@ styles:
7497
7499
  let versionHistoryError;
7498
7500
  if (requested === "local-git") {
7499
7501
  try {
7500
- const { providerFor } = await import("./source-provider-tgzet3j5.js");
7502
+ const { providerFor } = await import("./source-provider-tjaf303m.js");
7501
7503
  const provider = providerFor({ type: "local-folder", path: dir });
7502
7504
  await provider.initVersionHistory({
7503
7505
  projectDir: dir,
@@ -9265,25 +9267,67 @@ async function describeVersions(projectDir, entries) {
9265
9267
  }
9266
9268
  return out;
9267
9269
  }
9270
+ var CLOCK_SKEW_SECONDS = 10 * 60;
9268
9271
  async function commitsAhead(dir, fromOid, baseOid, cache2 = {}, limit = 100) {
9269
9272
  if (fromOid === baseOid)
9270
9273
  return [];
9271
- const bases = await git3.findMergeBase({ fs: gitFs, dir, cache: cache2, oids: [fromOid, baseOid] });
9272
- const stopAt = new Set(bases);
9273
- if (stopAt.size === 0)
9274
- return [];
9275
- const commits = await git3.log({ fs: gitFs, dir, cache: cache2, ref: fromOid, depth: limit });
9276
- const seen = new Set;
9277
- const out = [];
9278
- for (const c of commits) {
9279
- if (stopAt.has(c.oid))
9274
+ const walk = await startHistoryWalk(dir, cache2, [baseOid, fromOid]);
9275
+ const local = new Set([baseOid]);
9276
+ const online = new Set([fromOid]);
9277
+ const parentsOf = new Map;
9278
+ const listed = [];
9279
+ let shared = false;
9280
+ const mark = (oid) => {
9281
+ const stack = [oid];
9282
+ for (let next = stack.pop();next !== undefined; next = stack.pop()) {
9283
+ if (local.has(next))
9284
+ continue;
9285
+ local.add(next);
9286
+ if (online.has(next))
9287
+ shared = true;
9288
+ stack.push(...parentsOf.get(next) ?? []);
9289
+ }
9290
+ };
9291
+ const firstAhead = () => {
9292
+ const out = [];
9293
+ for (const c of listed) {
9294
+ if (out.length === limit)
9295
+ break;
9296
+ if (!local.has(c.oid))
9297
+ out.push(c);
9298
+ }
9299
+ return out;
9300
+ };
9301
+ const time = (c) => c.commit.committer.timestamp;
9302
+ for (;; ) {
9303
+ const searching = walk.tips.some((c2) => !local.has(c2.oid));
9304
+ if (!searching || shared && listed.length >= limit) {
9305
+ const shown = firstAhead();
9306
+ if (!searching || shown.length === limit) {
9307
+ const oldest = Math.min(...shown.map(time));
9308
+ const newestMarked = walk.tips.findLast((c2) => local.has(c2.oid));
9309
+ if (!newestMarked || time(newestMarked) < oldest - CLOCK_SKEW_SECONDS)
9310
+ break;
9311
+ }
9312
+ }
9313
+ const c = await nextInHistory(dir, walk, cache2, 1);
9314
+ if (!c)
9280
9315
  break;
9281
- if (seen.has(c.oid))
9316
+ const parents = walk.shallow.has(c.oid) ? [] : c.commit.parent;
9317
+ parentsOf.set(c.oid, parents);
9318
+ if (local.has(c.oid)) {
9319
+ for (const p of parents)
9320
+ mark(p);
9282
9321
  continue;
9283
- seen.add(c.oid);
9284
- out.push(toEntry(c));
9322
+ }
9323
+ listed.push(c);
9324
+ for (const p of parents) {
9325
+ online.add(p);
9326
+ if (local.has(p))
9327
+ shared = true;
9328
+ }
9285
9329
  }
9286
- return out;
9330
+ return shared ? firstAhead().map(toEntry) : [];
9287
9331
  }
9288
9332
  async function listOnlineVersions(options) {
9289
9333
  const http2 = options.httpClient ?? defaultGitHttp;
@@ -12761,7 +12805,7 @@ async function resolvePublishProvider(id, book) {
12761
12805
  const ext = await bookExtensions(book);
12762
12806
  const npmEntry = ext.extensions.some((c) => !c.path && c.name === id);
12763
12807
  if (npmEntry) {
12764
- const { restoreForCommand: restoreForCommand2 } = await import("./extension-restore-5x4mj79d.js");
12808
+ const { restoreForCommand: restoreForCommand2 } = await import("./extension-restore-chj5nx3t.js");
12765
12809
  await restoreForCommand2(book.projectDir, { manifestPath: book.manifestPath, failFast: true });
12766
12810
  }
12767
12811
  const publishers = await discover(ext);
@@ -12799,7 +12843,7 @@ function infoFor(p) {
12799
12843
  }
12800
12844
  function pluginProvider(p) {
12801
12845
  let loading;
12802
- const load = () => loading ??= import("./plugins-bcbe7xs8.js").then((m) => m.loadPublishFunction(p));
12846
+ const load = () => loading ??= import("./plugins-0hhw6s17.js").then((m) => m.loadPublishFunction(p));
12803
12847
  return {
12804
12848
  info: infoFor(p),
12805
12849
  async authenticate() {
@@ -19,7 +19,7 @@ import {
19
19
  stringValueAt,
20
20
  toFolioPage,
21
21
  wantsRecto
22
- } from "./index-798jt0qc.js";
22
+ } from "./index-swfdgn2n.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
  }
@@ -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-beta.1",
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
  }
@@ -712,10 +712,10 @@ 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-ze691rrd.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
721
  var manifest_default = "./manifest-mffah3hr.yaml";
@@ -14,7 +14,7 @@ import {
14
14
  readExtensionMeta,
15
15
  resolveExtension,
16
16
  slugify
17
- } from "./index-a4j3xcfp.js";
17
+ } from "./index-80vg3ed6.js";
18
18
  import {
19
19
  BuildError,
20
20
  EXIT_CODES,
package/dist/index.js CHANGED
@@ -122,7 +122,7 @@ import {
122
122
  substituteVariables,
123
123
  syncProject,
124
124
  testRemoteAccess
125
- } from "./index-c6y0p7jy.js";
125
+ } from "./index-0k43evt0.js";
126
126
  import {
127
127
  DEFAULT_NPM_REGISTRY,
128
128
  MANIFEST_FILENAMES,
@@ -152,7 +152,7 @@ import {
152
152
  setExtensionEnabled,
153
153
  updateExtensions,
154
154
  validateProjectExtensions
155
- } from "./index-rj5vrhz6.js";
155
+ } from "./index-ytjkvwy5.js";
156
156
  import {
157
157
  BUNDLED_EXTENSIONS,
158
158
  EXTENSION_MANIFEST_FILENAME,
@@ -169,12 +169,12 @@ import {
169
169
  pathEscapesFolder,
170
170
  readExtensionMeta,
171
171
  resolveExtension
172
- } from "./index-a4j3xcfp.js";
172
+ } from "./index-80vg3ed6.js";
173
173
  import"./index-ht1bcqdd.js";
174
174
  import {
175
175
  BUILD_DIAGNOSTIC_CODES
176
- } from "./index-fk2kpf2t.js";
177
- import"./index-798jt0qc.js";
176
+ } from "./index-2t1gdma7.js";
177
+ import"./index-swfdgn2n.js";
178
178
  import {
179
179
  BuildError
180
180
  } from "./index-tayzfr9c.js";
@@ -199,7 +199,7 @@ import {
199
199
  revisionNameFrom,
200
200
  switchBranch,
201
201
  whenGitIdle
202
- } from "./index-cw03e9p4.js";
202
+ } from "./index-7j3hkwya.js";
203
203
  import {
204
204
  EXTENSIONS_DIR
205
205
  } from "./index-5x42amh8.js";