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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +13 -3
  2. package/dist/api/index.d.ts +5 -5
  3. package/dist/api/index.js +19 -7
  4. package/dist/{build-dg8a566j.js → build-fq3mexhy.js} +9 -9
  5. package/dist/checks/asset/approved-fonts.d.ts +8 -0
  6. package/dist/checks/asset/extensions.d.ts +1 -1
  7. package/dist/checks/asset/index.d.ts +1 -0
  8. package/dist/{cli-e31jshz6.js → cli-5zbdb6mt.js} +296 -65
  9. package/dist/{cli-ttj95aex.js → cli-7mt0dftw.js} +47 -18
  10. package/dist/{cli-989eg8jz.js → cli-ah93yyzb.js} +163 -55
  11. package/dist/{cli-31anr0r9.js → cli-eymdsefg.js} +69 -29
  12. package/dist/{cli-1fy1pm9d.js → cli-fh685nqs.js} +1 -1
  13. package/dist/{cli-k087gj0p.js → cli-fsjre61x.js} +123 -12
  14. package/dist/{cli-s1zt779w.js → cli-q7t04gjt.js} +1 -1
  15. package/dist/{cli-vhav8g82.js → cli-rybz2dfs.js} +1 -1
  16. package/dist/cli.js +12 -12
  17. package/dist/{doctor-g4a42tcz.js → doctor-tg9ht1n3.js} +3 -3
  18. package/dist/engine/compiler/postprocess.d.ts +3 -0
  19. package/dist/engine/shared/pdf-inspect.d.ts +33 -0
  20. package/dist/{engine-jvkcahr1.js → engine-17v87vq5.js} +1 -1
  21. package/dist/{engine-wgkaqjsc.js → engine-y9fnarfa.js} +2 -2
  22. package/dist/{ext-w9d5ftcg.js → ext-esz03jz4.js} +27 -10
  23. package/dist/{extension-restore-5x4mj79d.js → extension-restore-91gy1e28.js} +3 -3
  24. package/dist/{extension-restore-as0g38p1.js → extension-restore-q70wqah5.js} +5 -5
  25. package/dist/{gutterpress-agent-d29ry3ak.js → gutterpress-agent-wyryqehx.js} +13 -4
  26. package/dist/{gutterpress-viewer-ze691rrd.js → gutterpress-viewer-ywkf49q5.js} +38 -153
  27. package/dist/{index-a4j3xcfp.js → index-67g8f7r4.js} +47 -18
  28. package/dist/{index-798jt0qc.js → index-d9w3r89w.js} +3 -3
  29. package/dist/{index-rj5vrhz6.js → index-fntpr44x.js} +321 -66
  30. package/dist/{index-5x42amh8.js → index-g3te5ey1.js} +2 -1
  31. package/dist/{index-cw03e9p4.js → index-kw4tbthz.js} +69 -29
  32. package/dist/{index-fk2kpf2t.js → index-santcs4q.js} +121 -10
  33. package/dist/{index-c6y0p7jy.js → index-z6vzj3s5.js} +243 -73
  34. package/dist/index.js +35 -8
  35. package/dist/lib/check-options.d.ts +70 -0
  36. package/dist/lib/extension-manager.d.ts +16 -1
  37. package/dist/lib/extension-scaffold.d.ts +15 -2
  38. package/dist/lib/extension-specifier.d.ts +8 -1
  39. package/dist/lib/manifest-config.d.ts +3 -1
  40. package/dist/lib/manifest-old-spellings.d.ts +32 -9
  41. package/dist/lib/markdown/gutterpress-css.d.ts +1 -1
  42. package/dist/lib/markdown/math.d.ts +1 -1
  43. package/dist/lib/markdown/typography.d.ts +1 -0
  44. package/dist/lib/page-size.d.ts +12 -4
  45. package/dist/lib/plugin-vendor.d.ts +6 -0
  46. package/dist/lib/presets.d.ts +2 -2
  47. package/dist/lib/root-tokens.d.ts +2 -1
  48. package/dist/lib/source-provider.d.ts +27 -0
  49. package/dist/lib/style-resolver.d.ts +2 -1
  50. package/dist/lib/version-changes.d.ts +22 -6
  51. package/dist/{manifest.schema-0jgj0jt1.json → manifest.schema-5cm13vr3.json} +215 -5
  52. package/dist/{new-jtk028rf.js → new-rmfyb7va.js} +22 -15
  53. package/dist/{plugins-bcbe7xs8.js → plugins-gs21xz5s.js} +2 -2
  54. package/dist/{plugins-zsm6wv79.js → plugins-tqv1s16f.js} +4 -4
  55. package/dist/{preflight-cs6mj1d6.js → preflight-bjyeeq0d.js} +9 -9
  56. package/dist/{preview-d1j1g4qf.js → preview-ke03fk5d.js} +9 -9
  57. package/dist/{publish-7yw4jyrt.js → publish-wpnz0qng.js} +9 -9
  58. package/dist/render.js +55 -13
  59. package/dist/schema/manifest.types.d.ts +45 -7
  60. package/dist/{source-provider-n7vehh6t.js → source-provider-3g3yrn1f.js} +6 -2
  61. package/dist/{source-provider-tgzet3j5.js → source-provider-azg9ywhb.js} +6 -2
  62. package/dist/test-helpers/vocabulary.d.ts +12 -0
  63. package/dist/{validate-rkcdnzhw.js → validate-zcwykb43.js} +9 -9
  64. package/package.json +1 -1
  65. /package/dist/{cli-y28j019d.js → cli-1dha1nxk.js} +0 -0
@@ -1032,12 +1032,14 @@ function needsMeasurement(value) {
1032
1032
 
1033
1033
  // src/engine/shared/pdf-inspect.ts
1034
1034
  import {
1035
+ decodePDFRawStream,
1035
1036
  PDFArray,
1036
1037
  PDFDict,
1037
1038
  PDFDocument,
1038
1039
  PDFHexString,
1039
1040
  PDFName,
1040
1041
  PDFNumber,
1042
+ PDFRawStream,
1041
1043
  PDFRef,
1042
1044
  PDFString
1043
1045
  } from "pdf-lib";
@@ -1158,6 +1160,52 @@ async function inspectPdf(bytes) {
1158
1160
  walkOutline(outlines);
1159
1161
  return { pageCount: pages.length, boxes, linkTargets, namedDests, outline };
1160
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
+ }
1161
1209
 
1162
1210
  // src/lib/embedded-assets.ts
1163
1211
  import { existsSync } from "node:fs";
@@ -1170,7 +1218,7 @@ import { fileURLToPath } from "node:url";
1170
1218
  var favicon_default = "./favicon-272ga6y0.ico";
1171
1219
 
1172
1220
  // src/assets/manifest.schema.json
1173
- var manifest_schema_default = "./manifest.schema-0jgj0jt1.json";
1221
+ var manifest_schema_default = "./manifest.schema-5cm13vr3.json";
1174
1222
 
1175
1223
  // src/assets/preview/scripts/preview-interface.js
1176
1224
  var preview_interface_default = "./preview-interface-0xdh0r5b.js";
@@ -1185,10 +1233,10 @@ var preview_shell_default = "./preview-shell-wk9k3gcd.js";
1185
1233
  var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
1186
1234
 
1187
1235
  // src/assets/engine/gutterpress-viewer.js
1188
- var gutterpress_viewer_default = "./gutterpress-viewer-ze691rrd.js";
1236
+ var gutterpress_viewer_default = "./gutterpress-viewer-ywkf49q5.js";
1189
1237
 
1190
1238
  // src/assets/engine/gutterpress-agent.js
1191
- var gutterpress_agent_default = "./gutterpress-agent-d29ry3ak.js";
1239
+ var gutterpress_agent_default = "./gutterpress-agent-wyryqehx.js";
1192
1240
 
1193
1241
  // src/assets/templates/book/manifest.yaml
1194
1242
  var manifest_default = "./manifest-mffah3hr.yaml";
@@ -1483,6 +1531,7 @@ function round(n) {
1483
1531
  import { PDFDocument as PDFDocument2, PDFName as PDFName2, PDFNumber as PDFNumber2, rgb } from "pdf-lib";
1484
1532
  async function postprocess(input, opts) {
1485
1533
  const doc = await PDFDocument2.load(input, { updateMetadata: false });
1534
+ const scale = printScale(doc);
1486
1535
  const g = opts.geometry;
1487
1536
  const inset = g.bleed + g.slug;
1488
1537
  const media = [0, 0, g.media.width, g.media.height];
@@ -1529,7 +1578,8 @@ async function postprocess(input, opts) {
1529
1578
  bytes,
1530
1579
  pageCount: doc.getPageCount(),
1531
1580
  padded,
1532
- boxes: { media, bleed: bleedBox, trim: trimBox }
1581
+ boxes: { media, bleed: bleedBox, trim: trimBox },
1582
+ scale
1533
1583
  };
1534
1584
  }
1535
1585
  function drawCropMarks(page, g) {
@@ -1820,8 +1870,6 @@ ${pinCss}`);
1820
1870
  const msg = `${headline}:
1821
1871
  ${describe(widthOffenders.boxes)}`;
1822
1872
  if (opts.allowShrink) {
1823
- if (scale !== null)
1824
- diagnose("engine.width.overflow", `${headline}.`);
1825
1873
  for (const o of widthOffenders.boxes)
1826
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."}`);
1827
1875
  log(`WARNING: ${msg}`);
@@ -1837,6 +1885,54 @@ ${describe(widthOffenders.intrinsics)}`;
1837
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%).`);
1838
1886
  log(`WARNING: ${msg}`);
1839
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
+ }
1840
1936
  const rectoDecls = model.breaks.filter(isRectoVersoBreak);
1841
1937
  const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0 || furnitureRoots.length > 0 || sideRoots.length > 0;
1842
1938
  let tier = tier2.geometry.bleed > 0 || tier2.geometry.slug > 0 ? 2 : 1;
@@ -2358,6 +2454,20 @@ ${mapCss}`;
2358
2454
  author: opts.author
2359
2455
  });
2360
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
+ }
2361
2471
  return {
2362
2472
  bytes: post.bytes,
2363
2473
  tier,
@@ -2392,7 +2502,7 @@ function shrinkScale(boxes, limitPx) {
2392
2502
  return null;
2393
2503
  return Math.max(1 / MAX_SHRINK, limitPx / maxRight);
2394
2504
  }
2395
- async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport) {
2505
+ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport, tolerancePx = 1) {
2396
2506
  const contexts = [
2397
2507
  resolvePage(model),
2398
2508
  ...model.pageNames.filter((n) => !n.startsWith("gp-") || n.startsWith("gp--pin-")).map((n) => resolvePage(model, { name: n }))
@@ -2418,7 +2528,8 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
2418
2528
  mobile: false
2419
2529
  });
2420
2530
  boxes.push(...JSON.parse(await page.evaluate(`(() => {
2421
- const LIMIT = ${widthPx} + 1;
2531
+ const TOL = ${tolerancePx};
2532
+ const LIMIT = Math.max(${widthPx} + TOL, ${Math.ceil(widthPx)});
2422
2533
  const NAMED = ${JSON.stringify(namedLimits)};
2423
2534
  const DEFAULT_LIMIT = ${defaultLimitPx};
2424
2535
  // The page an element lands on: the nearest ancestor with a non-auto
@@ -2458,10 +2569,10 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
2458
2569
  // pulls) AND left-edge protrusion (negative left with an ordinary
2459
2570
  // width) — both trigger the same whole-document shrink-to-fit,
2460
2571
  // measured: a left bleed alone cost 16%.
2461
- if (right <= LIMIT && r.width <= LIMIT && r.left >= -1) continue;
2572
+ if (right <= LIMIT && r.width <= LIMIT && r.left >= -TOL) continue;
2462
2573
  // ...but only where it actually escapes its clipping ancestors.
2463
2574
  const clip = clipEdges(el).edges;
2464
- 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;
2465
2576
  let deepest = true;
2466
2577
  for (const c of el.children) {
2467
2578
  if (c.getBoundingClientRect().width >= r.width - 1) { deepest = false; break; }
@@ -2498,7 +2609,7 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
2498
2609
  const over = el.scrollWidth - el.clientWidth;
2499
2610
  // clientWidth is 0 on inline/replaced boxes, where scrollWidth
2500
2611
  // carries no overflow information -- skip rather than guess.
2501
- if (el.clientWidth <= 0 || over <= 1) continue;
2612
+ if (el.clientWidth <= 0 || over <= TOL) continue;
2502
2613
  // A descendant assigned to a DIFFERENT page is not overflowing this
2503
2614
  // element's page -- it is laid out against its own, wider one. The
2504
2615
  // full-bleed art plate is the standard case: it is legitimately
@@ -2552,7 +2663,7 @@ async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreView
2552
2663
  }
2553
2664
  return JSON.stringify(out);
2554
2665
  })()`));
2555
- return { limitPx, boxes, intrinsics: replaced };
2666
+ return { limitPx, defaultLimitPx, boxes, intrinsics: replaced };
2556
2667
  } finally {
2557
2668
  await page.send("Emulation.setDeviceMetricsOverride", restoreViewport);
2558
2669
  }
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "gutterpress",
4
- version: "0.11.23-beta.1",
4
+ version: "0.11.23-beta.3",
5
5
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
6
6
  author: "itlackey",
7
7
  license: "MPL-2.0",
@@ -14,7 +14,7 @@ import {
14
14
  import {
15
15
  PACKAGE_VERSION,
16
16
  log
17
- } from "./cli-1fy1pm9d.js";
17
+ } from "./cli-fh685nqs.js";
18
18
  import {
19
19
  __require
20
20
  } from "./cli-37x76zdn.js";
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./cli-d8xjns66.js";
9
9
  import {
10
10
  package_default
11
- } from "./cli-s1zt779w.js";
11
+ } from "./cli-q7t04gjt.js";
12
12
  import {
13
13
  EXIT_CODES
14
14
  } from "./cli-46ycxe6r.js";
@@ -20,14 +20,14 @@ import {
20
20
  import { defineCommand, parseArgs, runMain } from "citty";
21
21
  import { statSync } from "node:fs";
22
22
  var SUBCOMMANDS = {
23
- new: () => import("./new-jtk028rf.js").then((m) => m.default),
24
- preview: () => import("./preview-d1j1g4qf.js").then((m) => m.default),
25
- build: () => import("./build-dg8a566j.js").then((m) => m.default),
26
- publish: () => import("./publish-7yw4jyrt.js").then((m) => m.default),
27
- validate: () => import("./validate-rkcdnzhw.js").then((m) => m.default),
28
- preflight: () => import("./preflight-cs6mj1d6.js").then((m) => m.default),
29
- doctor: () => import("./doctor-g4a42tcz.js").then((m) => m.default),
30
- ext: () => import("./ext-w9d5ftcg.js").then((m) => m.default)
23
+ new: () => import("./new-rmfyb7va.js").then((m) => m.default),
24
+ preview: () => import("./preview-ke03fk5d.js").then((m) => m.default),
25
+ build: () => import("./build-fq3mexhy.js").then((m) => m.default),
26
+ publish: () => import("./publish-wpnz0qng.js").then((m) => m.default),
27
+ validate: () => import("./validate-zcwykb43.js").then((m) => m.default),
28
+ preflight: () => import("./preflight-bjyeeq0d.js").then((m) => m.default),
29
+ doctor: () => import("./doctor-tg9ht1n3.js").then((m) => m.default),
30
+ ext: () => import("./ext-esz03jz4.js").then((m) => m.default)
31
31
  };
32
32
  var VERSION = package_default.version;
33
33
  var main = defineCommand({
@@ -52,7 +52,7 @@ async function preflightRequiredInvocations(rawArgs) {
52
52
  const [command, ...commandArgs] = rawArgs;
53
53
  try {
54
54
  if (command === "new") {
55
- const { newArgs } = await import("./new-jtk028rf.js");
55
+ const { newArgs } = await import("./new-rmfyb7va.js");
56
56
  rejectUnknownFlags(commandArgs, newArgs, "new");
57
57
  const parsed = parseArgs(commandArgs, {
58
58
  ...newArgs,
@@ -64,7 +64,7 @@ async function preflightRequiredInvocations(rawArgs) {
64
64
  return;
65
65
  }
66
66
  if (command === "preflight") {
67
- const { preflightArgs } = await import("./preflight-cs6mj1d6.js");
67
+ const { preflightArgs } = await import("./preflight-bjyeeq0d.js");
68
68
  rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
69
69
  const parsed = parseArgs(commandArgs, {
70
70
  ...preflightArgs,
@@ -84,7 +84,7 @@ async function preflightRequiredInvocations(rawArgs) {
84
84
  }
85
85
  if (subcommand.startsWith("-"))
86
86
  return;
87
- const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-w9d5ftcg.js");
87
+ const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-esz03jz4.js");
88
88
  if (!EXT_SUBCOMMANDS.includes(subcommand)) {
89
89
  throw new UsageError(`gutterpress ext: unknown command "${subcommand}"`);
90
90
  }
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  getSystemDiagnostics
3
- } from "./cli-vhav8g82.js";
3
+ } from "./cli-rybz2dfs.js";
4
4
  import"./cli-5crpmz42.js";
5
5
  import"./cli-asap4t1z.js";
6
6
  import {
7
7
  log
8
- } from "./cli-1fy1pm9d.js";
8
+ } from "./cli-fh685nqs.js";
9
9
  import {
10
10
  UsageError,
11
11
  rejectExtraPositionals,
12
12
  rejectUnknownFlags
13
13
  } from "./cli-d8xjns66.js";
14
- import"./cli-s1zt779w.js";
14
+ import"./cli-q7t04gjt.js";
15
15
  import"./cli-46ycxe6r.js";
16
16
  import"./cli-37x76zdn.js";
17
17
 
@@ -27,5 +27,8 @@ export interface PostprocessResult {
27
27
  bleed: number[];
28
28
  trim: number[];
29
29
  };
30
+ /** The scale Chromium printed at, read before anything here draws:
31
+ * `printScale` in shared/pdf-inspect.ts (1 = not shrunk). */
32
+ scale: number | null;
30
33
  }
31
34
  export declare function postprocess(input: Uint8Array, opts: PostprocessOptions): Promise<PostprocessResult>;
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Read-side PDF utilities (pdf-lib, pure JS).
3
+ *
4
+ * Three jobs:
5
+ * 1. Verification: page count, page boxes, embedded fonts — the evidence the
6
+ * tests assert on.
7
+ * 2. Tier 3 measurement channel (§8): harvest Chromium's own link annotations
8
+ * and document outline to learn "which page is element X on", with no text
9
+ * heuristics.
10
+ * 3. The print scale: whether Chromium's shrink-to-fit scaled the page area
11
+ * ({@link printScale}).
12
+ */
13
+ import { PDFDocument } from "pdf-lib";
1
14
  export interface PageBoxes {
2
15
  media: number[];
3
16
  crop?: number[];
@@ -18,3 +31,23 @@ export interface PdfFacts {
18
31
  }>;
19
32
  }
20
33
  export declare function inspectPdf(bytes: Uint8Array): Promise<PdfFacts>;
34
+ /**
35
+ * The scale Chromium printed the page area at: 1 when it laid the document
36
+ * out at the page content box's own width, below 1 when print shrink-to-fit
37
+ * scaled the whole document down because something reached past that box
38
+ * (ENGINE.md §9). `null` when no page shows the transform — a reader that
39
+ * cannot find it makes no claim. Pass Chromium's own print, before anything
40
+ * redraws its pages.
41
+ *
42
+ * Read off the content streams rather than predicted from element widths.
43
+ * Each page draws its page box (background, margin boxes) under an unscaled
44
+ * `3.125 0 0 3.125 0 0 cm` — CSS px to 300-per-inch device units — then clips
45
+ * to the page area, its largest clip, and draws the page area's content
46
+ * under `s 0 0 s x y cm`, where x is the clip's own left edge and s is 3.125
47
+ * times the shrink. An element with a CSS transform or its own clip draws
48
+ * under other matrices, so one page can read wrong; Chromium applies one
49
+ * factor to the whole document, so the first reading three pages agree on
50
+ * (or the most common, in a shorter document) is the answer. The prologue
51
+ * fixes the device resolution: the scale is s × prologue × 96/72.
52
+ */
53
+ export declare function printScale(doc: PDFDocument): number | null;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  build
3
- } from "./cli-k087gj0p.js";
3
+ } from "./cli-fsjre61x.js";
4
4
  import"./cli-eyyggect.js";
5
5
  import"./cli-asap4t1z.js";
6
6
  import"./cli-65gy8ask.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  build
3
- } from "./index-fk2kpf2t.js";
4
- import"./index-798jt0qc.js";
3
+ } from "./index-santcs4q.js";
4
+ import"./index-d9w3r89w.js";
5
5
  import {
6
6
  BuildError
7
7
  } from "./index-tayzfr9c.js";
@@ -2,28 +2,30 @@ import {
2
2
  importExtensionFromFile,
3
3
  importExtensionFromUrl,
4
4
  searchNpmExtensions
5
- } from "./cli-989eg8jz.js";
6
- import"./cli-vhav8g82.js";
5
+ } from "./cli-ah93yyzb.js";
6
+ import"./cli-rybz2dfs.js";
7
7
  import"./cli-ht1bcqdd.js";
8
- import"./cli-k087gj0p.js";
8
+ import"./cli-fsjre61x.js";
9
9
  import"./cli-5crpmz42.js";
10
10
  import"./cli-eyyggect.js";
11
11
  import"./cli-asap4t1z.js";
12
- import"./cli-31anr0r9.js";
12
+ import"./cli-eymdsefg.js";
13
13
  import"./cli-v5mp7a6q.js";
14
14
  import {
15
+ BUILT_IN_LOOKS,
16
+ addBuiltInStyleSet,
15
17
  addExtension,
16
18
  checkExtensionUpdates,
17
19
  listProjectExtensions,
18
20
  removeExtension,
19
21
  setExtensionEnabled,
20
22
  updateExtensions
21
- } from "./cli-e31jshz6.js";
23
+ } from "./cli-5zbdb6mt.js";
22
24
  import {
23
25
  isPathSpecifier,
24
26
  parseExtensionSpecifier
25
- } from "./cli-ttj95aex.js";
26
- import"./cli-1fy1pm9d.js";
27
+ } from "./cli-7mt0dftw.js";
28
+ import"./cli-fh685nqs.js";
27
29
  import {
28
30
  UsageError,
29
31
  exitForUsage,
@@ -31,12 +33,12 @@ import {
31
33
  rejectUnknownFlags,
32
34
  resolveProjectDir
33
35
  } from "./cli-d8xjns66.js";
34
- import"./cli-s1zt779w.js";
36
+ import"./cli-q7t04gjt.js";
35
37
  import"./cli-65gy8ask.js";
36
38
  import {
37
39
  EXIT_CODES
38
40
  } from "./cli-46ycxe6r.js";
39
- import"./cli-y28j019d.js";
41
+ import"./cli-1dha1nxk.js";
40
42
  import"./cli-37x76zdn.js";
41
43
 
42
44
  // src/commands/ext.ts
@@ -65,6 +67,11 @@ var extAddArgs = {
65
67
  type: "string",
66
68
  description: "Named module export to use as the plugin function (npm and path extensions only)",
67
69
  required: false
70
+ },
71
+ look: {
72
+ type: "boolean",
73
+ description: "Treat SOURCE as a built-in look (clean-book, zine, technical-doc, academic): copy it into extensions/ as your own files and list the copy",
74
+ required: false
68
75
  }
69
76
  };
70
77
  var extRemoveArgs = { specifier: specifierArg, dir: dirArg };
@@ -202,7 +209,13 @@ var add = defineCommand({
202
209
  if (typeof args.export === "string" && !exportName) {
203
210
  throw new UsageError("gutterpress ext add: --export requires a non-empty name");
204
211
  }
205
- if (!isUrl && !existing) {
212
+ if (args.look && !BUILT_IN_LOOKS.includes(source)) {
213
+ throw new UsageError(`gutterpress ext add --look: unknown built-in look "${source}" (one of: ${BUILT_IN_LOOKS.join(", ")})`);
214
+ }
215
+ if (args.look && exportName) {
216
+ throw new UsageError("gutterpress ext add --look: a look takes no --export");
217
+ }
218
+ if (!args.look && !isUrl && !existing) {
206
219
  try {
207
220
  if (path.isAbsolute(source) || isPathSpecifier(source)) {
208
221
  throw new Error(`source not found: ${candidate}`);
@@ -216,6 +229,10 @@ var add = defineCommand({
216
229
  exitForUsage(error);
217
230
  }
218
231
  try {
232
+ if (args.look) {
233
+ printAdded(await addBuiltInStyleSet(projectDir, source), projectDir);
234
+ return;
235
+ }
219
236
  if (isUrl) {
220
237
  const { entry: entry2, warnings } = await importExtensionFromUrl(projectDir, source);
221
238
  printAdded(entry2, projectDir, warnings.map((w) => w.message));
@@ -3,10 +3,10 @@ import {
3
3
  restoreForCommand,
4
4
  restoreOfflineMessage,
5
5
  restorePinnedExtensions
6
- } from "./index-rj5vrhz6.js";
7
- import"./index-a4j3xcfp.js";
6
+ } from "./index-fntpr44x.js";
7
+ import"./index-67g8f7r4.js";
8
8
  import"./index-tayzfr9c.js";
9
- import"./index-5x42amh8.js";
9
+ import"./index-g3te5ey1.js";
10
10
  import"./index-37x76zdn.js";
11
11
  export {
12
12
  restorePinnedExtensions,
@@ -3,14 +3,14 @@ import {
3
3
  restoreForCommand,
4
4
  restoreOfflineMessage,
5
5
  restorePinnedExtensions
6
- } from "./cli-e31jshz6.js";
7
- import"./cli-ttj95aex.js";
8
- import"./cli-1fy1pm9d.js";
6
+ } from "./cli-5zbdb6mt.js";
7
+ import"./cli-7mt0dftw.js";
8
+ import"./cli-fh685nqs.js";
9
9
  import"./cli-d8xjns66.js";
10
- import"./cli-s1zt779w.js";
10
+ import"./cli-q7t04gjt.js";
11
11
  import"./cli-65gy8ask.js";
12
12
  import"./cli-46ycxe6r.js";
13
- import"./cli-y28j019d.js";
13
+ import"./cli-1dha1nxk.js";
14
14
  import"./cli-37x76zdn.js";
15
15
  export {
16
16
  restorePinnedExtensions,
@@ -59,6 +59,7 @@
59
59
  });
60
60
 
61
61
  // src/engine/shared/synthesis.ts
62
+ var RECTO_VERSO_VALUES = /^(right|recto|left|verso)$/;
62
63
  var WHICH_VALUES = new Set(["first", "start", "last", "first-except"]);
63
64
  var LEADER_RE = /\uE000([^\uE001]*)\uE001/;
64
65
  function leaderFillCount(gapPx, gluePx) {
@@ -422,7 +423,8 @@
422
423
  return out;
423
424
  }
424
425
  function forcedBreakSites(decls) {
425
- const out = [];
426
+ const found = [];
427
+ const seen = new Set;
426
428
  for (const d of decls) {
427
429
  let els = [];
428
430
  try {
@@ -430,10 +432,17 @@
430
432
  } catch {
431
433
  continue;
432
434
  }
433
- for (const el of els)
434
- out.push({ id: ensureAnchor(el), prop: d.prop, value: d.value, selector: d.selector });
435
+ for (const el of els) {
436
+ const id = ensureAnchor(el);
437
+ const value = getComputedStyle(el).breakBefore;
438
+ if (seen.has(el) || !RECTO_VERSO_VALUES.test(value))
439
+ continue;
440
+ seen.add(el);
441
+ found.push({ el, id, prop: d.prop, value, selector: d.selector });
442
+ }
435
443
  }
436
- return out;
444
+ found.sort((a, b) => a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
445
+ return found.map(({ el: _el, ...site }) => site);
437
446
  }
438
447
  function counterResetSites(resets) {
439
448
  const out = [];