gutterpress 0.10.0-alpha.3 → 0.10.0-beta.1

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 (37) hide show
  1. package/README.md +2 -0
  2. package/dist/api/index.js +2 -2
  3. package/dist/{audit-xxegv0t0.js → audit-85zn35v8.js} +4 -4
  4. package/dist/{build-w0vb6mgq.js → build-1faqmp0y.js} +6 -4
  5. package/dist/{cli-5czby0dd.js → cli-5tffrg48.js} +90 -23
  6. package/dist/{cli-f4rcbt2t.js → cli-7e79d7se.js} +1 -1
  7. package/dist/{cli-yp3p0sf2.js → cli-afm8pa0h.js} +327 -24
  8. package/dist/{cli-gk3wsdpb.js → cli-v935c2ws.js} +1 -1
  9. package/dist/{cli-revgt4pr.js → cli-yzf38679.js} +1 -0
  10. package/dist/cli.js +16 -16
  11. package/dist/{doctor-ctgn1dnt.js → doctor-69v5c9wy.js} +2 -2
  12. package/dist/engine/compiler/build.d.ts +1 -1
  13. package/dist/engine/shared/flush.d.ts +59 -0
  14. package/dist/engine/shared/margin-box-support.d.ts +24 -0
  15. package/dist/{engine-eng4xh2q.js → engine-hrtn0b28.js} +1 -1
  16. package/dist/{engine-q5skhgms.js → engine-nbfdy0pp.js} +2 -2
  17. package/dist/{gutterpress-agent-1ctgfz92.js → gutterpress-agent-cazqstr1.js} +49 -1
  18. package/dist/{gutterpress-viewer-cem7dmr5.js → gutterpress-viewer-fb6fhwsy.js} +137 -51
  19. package/dist/{index-s416qpdv.js → index-052x5fef.js} +89 -22
  20. package/dist/{index-mvehxh0e.js → index-c2b6gyeg.js} +47 -4
  21. package/dist/{index-a4kr77td.js → index-wsydvp08.js} +287 -23
  22. package/dist/index.js +3 -3
  23. package/dist/lib/build-runner.d.ts +7 -0
  24. package/dist/lib/engine.d.ts +9 -0
  25. package/dist/lib/markdown/gutterpress-css.d.ts +9 -1
  26. package/dist/lib/markdown/markers.d.ts +51 -1
  27. package/dist/{lint-gsh6qwrq.js → lint-17404bsx.js} +4 -4
  28. package/dist/{new-nkhycqy4.js → new-vsgpxzek.js} +4 -4
  29. package/dist/{plugin-129wcs93.js → plugin-s4yfpx6p.js} +4 -4
  30. package/dist/{preflight-41hcejg9.js → preflight-1h3apvqh.js} +4 -4
  31. package/dist/{preview-pftsvr9z.js → preview-cqf383g4.js} +6 -5
  32. package/dist/{preview-interface-fnqb2y4v.js → preview-interface-hg5eqv88.js} +193 -4
  33. package/dist/{publish-dnjvb4jg.js → publish-arvycera.js} +4 -4
  34. package/dist/render.js +83 -7
  35. package/dist/{repair-a3enjttn.js → repair-vmwh2bhe.js} +4 -4
  36. package/dist/{validate-ph1xffqy.js → validate-ztzq7p35.js} +4 -4
  37. package/package.json +1 -1
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  getAssetPath,
3
- launchChromium
4
- } from "./index-mvehxh0e.js";
3
+ isIgnoredMarginBoxProperty,
4
+ launchChromium,
5
+ marginBoxAlign,
6
+ marginBoxRectPt
7
+ } from "./index-c2b6gyeg.js";
5
8
 
6
9
  // src/engine/compiler/build.ts
7
10
  import { readFile } from "node:fs/promises";
@@ -568,6 +571,45 @@ function specificity(r) {
568
571
  return (r.name ? 2 : 0) + r.pseudos.length;
569
572
  }
570
573
 
574
+ // src/engine/shared/flush.ts
575
+ var FLUSH_EDGES = ["top", "right", "bottom", "left"];
576
+ function flushKey(edges) {
577
+ return FLUSH_EDGES.filter((e) => edges.includes(e)).map((e) => e[0]).join("");
578
+ }
579
+ function flushPageName(authorPage, edges) {
580
+ const safe = authorPage ? authorPage.replace(/[^A-Za-z0-9_-]/g, "_") : "";
581
+ return `gp--flush${safe ? `-${safe}` : ""}-${flushKey(edges)}`;
582
+ }
583
+ function flushMargins(margin, edges) {
584
+ return {
585
+ top: edges.includes("top") ? 0 : margin.top,
586
+ right: edges.includes("right") ? 0 : margin.right,
587
+ bottom: edges.includes("bottom") ? 0 : margin.bottom,
588
+ left: edges.includes("left") ? 0 : margin.left
589
+ };
590
+ }
591
+ function marginBoxesOnEdges(edges) {
592
+ const owners = {
593
+ "top-left-corner": ["top", "left"],
594
+ "top-left": ["top"],
595
+ "top-center": ["top"],
596
+ "top-right": ["top"],
597
+ "top-right-corner": ["top", "right"],
598
+ "bottom-left-corner": ["bottom", "left"],
599
+ "bottom-left": ["bottom"],
600
+ "bottom-center": ["bottom"],
601
+ "bottom-right": ["bottom"],
602
+ "bottom-right-corner": ["bottom", "right"],
603
+ "left-top": ["left"],
604
+ "left-middle": ["left"],
605
+ "left-bottom": ["left"],
606
+ "right-top": ["right"],
607
+ "right-middle": ["right"],
608
+ "right-bottom": ["right"]
609
+ };
610
+ return Object.entries(owners).filter(([, own]) => own.some((e) => edges.includes(e))).map(([name]) => name);
611
+ }
612
+
571
613
  // src/engine/shared/synthesis.ts
572
614
  var RECTO_VERSO_VALUES = /^(right|recto|left|verso)$/;
573
615
  function isRectoVersoBreak(decl) {
@@ -1284,7 +1326,8 @@ var BUILD_DIAGNOSTIC_CODES = [
1284
1326
  "engine.layer.trapped",
1285
1327
  "engine.multicol.dead-column",
1286
1328
  "engine.content.overheight",
1287
- "engine.image.low-dpi"
1329
+ "engine.image.low-dpi",
1330
+ "engine.flush.margin-box"
1288
1331
  ];
1289
1332
  async function build(opts) {
1290
1333
  const log = opts.onProgress ?? (() => {});
@@ -1303,8 +1346,7 @@ async function build(opts) {
1303
1346
  await page.evaluate(AGENT);
1304
1347
  await page.waitForReady();
1305
1348
  const cssText = await page.evaluate(`window.__gp.collectCss()`);
1306
- const model = extract(cssText);
1307
- const { tier3Reasons } = classify(model);
1349
+ let model = extract(cssText);
1308
1350
  const baseGeom = resolvePage(model).geometry;
1309
1351
  const sheetViewport = {
1310
1352
  width: Math.max(1, Math.round(baseGeom.width * 96 / 72)),
@@ -1315,6 +1357,97 @@ async function build(opts) {
1315
1357
  await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
1316
1358
  await page.send("Emulation.setEmulatedMedia", { media: "print" });
1317
1359
  log(`print media emulated for audits and measurement`);
1360
+ const flushRoots = await page.evaluate(`window.__gp.flushRoots()`);
1361
+ const flushGroups = new Map;
1362
+ for (const root of flushRoots) {
1363
+ if (flushGroups.has(root.key))
1364
+ continue;
1365
+ const authorPage = root.page === "auto" ? undefined : root.page;
1366
+ flushGroups.set(root.key, {
1367
+ authorPage,
1368
+ edges: root.edges,
1369
+ genName: flushPageName(authorPage, root.edges),
1370
+ key: root.key
1371
+ });
1372
+ }
1373
+ if (flushGroups.size) {
1374
+ const lines = [];
1375
+ for (const group of flushGroups.values()) {
1376
+ const relocated = new Set(marginBoxesOnEdges(group.edges));
1377
+ const pseudoSets = [[]];
1378
+ if (group.authorPage) {
1379
+ for (const rule of model.pageRules) {
1380
+ if (rule.name !== group.authorPage)
1381
+ continue;
1382
+ const pseudo = rule.pseudos.length ? `:${rule.pseudos.join(":")}` : "";
1383
+ if (rule.pseudos.length && !pseudoSets.some((ps) => ps.length === rule.pseudos.length && rule.pseudos.every((x) => ps.includes(x))))
1384
+ pseudoSets.push(rule.pseudos);
1385
+ const body = [];
1386
+ for (const [prop, value] of Object.entries(rule.decls))
1387
+ body.push(` ${prop}: ${value};`);
1388
+ for (const [box, decls] of Object.entries(rule.marginBoxes)) {
1389
+ if (relocated.has(box.slice(1)))
1390
+ continue;
1391
+ body.push(` ${box} {`);
1392
+ for (const [prop, value] of Object.entries(decls))
1393
+ body.push(` ${prop}: ${value};`);
1394
+ body.push(` }`);
1395
+ }
1396
+ lines.push(`@page ${group.genName}${pseudo} {
1397
+ ${body.join(`
1398
+ `)}
1399
+ }`);
1400
+ }
1401
+ }
1402
+ for (const pseudos of pseudoSets) {
1403
+ const pseudo = pseudos.length ? `:${pseudos.join(":")}` : "";
1404
+ const body = [];
1405
+ for (const edge of group.edges)
1406
+ body.push(` margin-${edge}: 0;`);
1407
+ for (const box of relocated)
1408
+ body.push(` @${box} { content: none; }`);
1409
+ lines.push(`@page ${group.genName}${pseudo} {
1410
+ ${body.join(`
1411
+ `)}
1412
+ }`);
1413
+ }
1414
+ lines.push(`:where(.page, .spread)[data-gp-flush="${group.key}"][data-gp-flush][data-gp-flush][data-gp-flush] { page: ${group.genName}; }`);
1415
+ }
1416
+ const flushCss = lines.join(`
1417
+ `);
1418
+ await page.evaluate(`window.__gp.addCss("gp-flush-css", ${JSON.stringify(flushCss)})`);
1419
+ model = extract(`${cssText}
1420
+ ${flushCss}`);
1421
+ log(`.gp-flush: ${flushRoots.length} pinned root(s) -> ${flushGroups.size} generated page context(s)`);
1422
+ }
1423
+ const { tier3Reasons } = classify(model);
1424
+ const pseudoContexts = pagePseudoContexts(model);
1425
+ const contentHeightPt = (name) => Math.min(...pseudoContexts.map((pseudos) => {
1426
+ const geometry = resolvePage(model, { name, pseudos }).geometry;
1427
+ return geometry.height - geometry.margin.top - geometry.margin.bottom;
1428
+ }));
1429
+ const contentHeightPx = contentHeightPt() * 96 / 72;
1430
+ const namedContentHeightsPx = Object.fromEntries(model.pageNames.map((name) => [name, contentHeightPt(name) * 96 / 72]));
1431
+ const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
1432
+ const pageVarsCss = [
1433
+ `:root { --gp-content-h: ${contentHeightPx}px; }`,
1434
+ ...model.pageAssignments.map((a) => `${a.selector} { --gp-content-h: ${namedContentHeightsPx[a.page] ?? contentHeightPx}px; }`)
1435
+ ].join(`
1436
+ `);
1437
+ await page.evaluate(`window.__gp.addCss("gp-page-vars", ${JSON.stringify(pageVarsCss)})`);
1438
+ log(`page geometry published (--gp-content-h: ${Math.round(contentHeightPx)}px)`);
1439
+ const relocationBoxes = (group, pseudos) => {
1440
+ const ctx = resolvePage(model, { name: group.authorPage, pseudos });
1441
+ return marginBoxesOnEdges(group.edges).map((name) => [name, ctx.marginBoxes[`@${name}`]]).filter((pair) => {
1442
+ const c = pair[1]?.content?.trim();
1443
+ return !!c && c !== "none" && c !== "normal";
1444
+ });
1445
+ };
1446
+ const furnitureRoots = flushRoots.filter((root) => {
1447
+ const group = flushGroups.get(root.key);
1448
+ return pseudoContexts.some((pseudos) => relocationBoxes(group, pseudos).length > 0);
1449
+ });
1450
+ const flushDiagnosed = new Set;
1318
1451
  const tier2 = synthesize({
1319
1452
  model,
1320
1453
  marks: opts.marks,
@@ -1334,11 +1467,15 @@ async function build(opts) {
1334
1467
  }).join(`
1335
1468
  `);
1336
1469
  if (widthOffenders.boxes.length) {
1337
- const msg = `content wider than the page content box triggers Chromium print ` + `shrink-to-fit (the WHOLE book scales down, silently):
1470
+ const scale = shrinkScale(widthOffenders.boxes, widthOffenders.limitPx);
1471
+ const headline = scale === null ? `content outside the page content box risks Chromium print ` + `shrink-to-fit (the WHOLE book scales down, silently)` : `content wider than the page content box: Chromium print shrink-to-fit ` + `scales the WHOLE document — every page, every measurement — to about ` + `${scale.toFixed(2)}x its declared size (12pt type prints at ` + `${(12 * scale).toFixed(1)}pt). The page size and page count do not ` + `change, so the shrink is invisible in the PDF`;
1472
+ const msg = `${headline}:
1338
1473
  ${describe(widthOffenders.boxes)}`;
1339
1474
  if (opts.allowShrink) {
1475
+ if (scale !== null)
1476
+ diagnose("engine.width.overflow", `${headline}.`);
1340
1477
  for (const o of widthOffenders.boxes)
1341
- diagnose("engine.width.overflow", `${o.desc} is wider than the page — Chromium shrinks the WHOLE book to fit it. ${o.left < -1 ? "Keep it inside the page content box." : "Give it an explicit width that fits."}`);
1478
+ 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."}`);
1342
1479
  log(`WARNING: ${msg}`);
1343
1480
  } else {
1344
1481
  throw new Error(`${msg}
@@ -1353,7 +1490,7 @@ ${describe(widthOffenders.intrinsics)}`;
1353
1490
  log(`WARNING: ${msg}`);
1354
1491
  }
1355
1492
  const rectoDecls = model.breaks.filter(isRectoVersoBreak);
1356
- const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0;
1493
+ const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0 || furnitureRoots.length > 0;
1357
1494
  let tier = tier2.geometry.bleed > 0 || tier2.geometry.slug > 0 ? 2 : 1;
1358
1495
  let passes = 1;
1359
1496
  let pageMap = {};
@@ -1381,6 +1518,8 @@ ${describe(widthOffenders.intrinsics)}`;
1381
1518
  targets.add(s.id);
1382
1519
  for (const s of resetSites)
1383
1520
  targets.add(s.id);
1521
+ for (const r of furnitureRoots)
1522
+ targets.add(r.id);
1384
1523
  await page.evaluate(`window.__gp.instrument(${JSON.stringify([...targets])})`);
1385
1524
  const targetText = await page.evaluate(`window.__gp.targetTexts(${JSON.stringify([...targets])})`);
1386
1525
  const brokenXrefs = findBrokenXrefRefs(sites, targetText);
@@ -1465,6 +1604,73 @@ ${describe(widthOffenders.intrinsics)}`;
1465
1604
  /* Tier 3 */
1466
1605
  ${mapCss}`;
1467
1606
  }
1607
+ if (furnitureRoots.length) {
1608
+ const pageAt = (id) => map[id] ?? predictedForResult?.pageMap[id];
1609
+ const byName = new Map;
1610
+ for (const s of sources) {
1611
+ const p = pageAt(s.id);
1612
+ if (!p)
1613
+ continue;
1614
+ const list = byName.get(s.name) ?? [];
1615
+ list.push({ page: p, value: s.text });
1616
+ byName.set(s.name, list);
1617
+ }
1618
+ for (const entries of byName.values())
1619
+ entries.sort((a, b) => a.page - b.page);
1620
+ const PXPT = 96 / 72;
1621
+ const items = [];
1622
+ for (const root of furnitureRoots) {
1623
+ const physical = pageAt(root.id);
1624
+ if (!physical)
1625
+ continue;
1626
+ const group = flushGroups.get(root.key);
1627
+ const pseudos = [physical % 2 === 1 ? "right" : "left"];
1628
+ if (physical === 1)
1629
+ pseudos.push("first");
1630
+ const ctx = resolvePage(model, { name: group.authorPage, pseudos });
1631
+ const g = ctx.geometry;
1632
+ const eff = flushMargins(g.margin, group.edges);
1633
+ const boxes = [];
1634
+ for (const [name, decls] of relocationBoxes(group, pseudos)) {
1635
+ const text = evaluate(decls.content, {
1636
+ page: toFolioPage(physical, pageValues),
1637
+ pages: pageCount,
1638
+ strings: (n, w) => stringValueAt(byName.get(n) ?? [], physical, parseWhich(w)),
1639
+ targetPage: (u) => {
1640
+ const p = pageAt(u.replace(/^#/, ""));
1641
+ return p === undefined ? undefined : toFolioPage(p, pageValues);
1642
+ }
1643
+ });
1644
+ if (!text) {
1645
+ if (!flushDiagnosed.has(name)) {
1646
+ flushDiagnosed.add(name);
1647
+ diagnose("engine.flush.margin-box", `@${name} sits on an edge a .gp-flush pin 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 drop .gp-flush there.`);
1648
+ }
1649
+ continue;
1650
+ }
1651
+ const r = marginBoxRectPt(name, g);
1652
+ const outDecls = {};
1653
+ for (const [prop, value] of Object.entries(decls)) {
1654
+ if (prop.toLowerCase() === "content" || isIgnoredMarginBoxProperty(prop))
1655
+ continue;
1656
+ outDecls[prop] = value;
1657
+ }
1658
+ boxes.push({
1659
+ box: name,
1660
+ x: (r.x - eff.left) * PXPT,
1661
+ y: (r.y - eff.top) * PXPT,
1662
+ w: r.w * PXPT,
1663
+ h: r.h * PXPT,
1664
+ align: marginBoxAlign(name),
1665
+ text,
1666
+ decls: outDecls
1667
+ });
1668
+ }
1669
+ items.push({ id: root.id, boxes });
1670
+ }
1671
+ if (items.length)
1672
+ await page.evaluate(`window.__gp.setFlushFurniture(${JSON.stringify(items)})`);
1673
+ }
1468
1674
  };
1469
1675
  const predicted = await predictPageMap(browser, url, AGENT, VIEWER, {
1470
1676
  stringSets: model.stringSets.map((s) => ({ selector: s.selector, name: s.name, value: s.value })),
@@ -1503,14 +1709,6 @@ ${mapCss}`;
1503
1709
  log(`tier 3: NOT converged after ${passes} passes`);
1504
1710
  }
1505
1711
  }
1506
- const pseudoContexts = pagePseudoContexts(model);
1507
- const contentHeightPt = (name) => Math.min(...pseudoContexts.map((pseudos) => {
1508
- const geometry = resolvePage(model, { name, pseudos }).geometry;
1509
- return geometry.height - geometry.margin.top - geometry.margin.bottom;
1510
- }));
1511
- const contentHeightPx = contentHeightPt() * 96 / 72;
1512
- const namedContentHeightsPx = Object.fromEntries(model.pageNames.map((name) => [name, contentHeightPt(name) * 96 / 72]));
1513
- const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
1514
1712
  {
1515
1713
  const audit = await page.evaluate(`window.__gp.auditContent(${JSON.stringify({
1516
1714
  default: contentHeightPx,
@@ -1564,6 +1762,26 @@ ${mapCss}`;
1564
1762
  return reasons;
1565
1763
  };
1566
1764
 
1765
+ // Does this box establish the containing block for an absolutely
1766
+ // positioned descendant? Overflow clipping binds an abspos
1767
+ // .gp-pin only from its containing block outward — MEASURED: a
1768
+ // pin whose box lay entirely outside a STATIC overflow:hidden
1769
+ // (and overflow:clip) wrapper printed complete and still behind
1770
+ // the page text; the wrapper's clip never touched it.
1771
+ const establishesAbsContainingBlock = (cs) => {
1772
+ if (cs.position !== "static") return true;
1773
+ for (const prop of [
1774
+ "transform", "translate", "rotate", "scale", "perspective",
1775
+ "filter", "backdropFilter",
1776
+ ]) {
1777
+ const value = cs[prop];
1778
+ if (value && value !== "none") return true;
1779
+ }
1780
+ if (/\\b(layout|paint|strict|content)\\b/.test(cs.contain)) return true;
1781
+ if (cs.containerType && cs.containerType !== "normal") return true;
1782
+ return /\\b(transform|translate|rotate|scale|perspective|filter)\\b/.test(cs.willChange);
1783
+ };
1784
+
1567
1785
  for (const el of document.querySelectorAll("*")) {
1568
1786
  const cs = getComputedStyle(el);
1569
1787
  if (leaks.length < 20) {
@@ -1604,20 +1822,58 @@ ${mapCss}`;
1604
1822
  ) {
1605
1823
  const boundary = el.closest(".page, .spread");
1606
1824
  if (boundary) {
1825
+ const elRect = el.getBoundingClientRect();
1826
+ // Clip binding: an in-flow .gp-behind is bound by every
1827
+ // ancestor's overflow, but an abspos one only from its
1828
+ // containing block outward — static wrappers in between
1829
+ // never clip it (measured; see the pass comment above).
1830
+ let clipBinds = cs.position !== "absolute" && cs.position !== "fixed";
1607
1831
  for (let ancestor = el.parentElement; ancestor; ancestor = ancestor.parentElement) {
1608
1832
  const ancestorStyle = getComputedStyle(ancestor);
1833
+ if (!clipBinds && establishesAbsContainingBlock(ancestorStyle)) clipBinds = true;
1609
1834
  const reasons = stackingReasons(ancestor, ancestorStyle);
1610
- const clips = ancestorStyle.overflowX !== "visible" ||
1611
- ancestorStyle.overflowY !== "visible";
1612
- if ((reasons.length || clips) && !seenLayerTraps.has(ancestor)) {
1835
+ // Clipping never reorders layers — it can only CUT the
1836
+ // art (measured, pass comment above): warn only where the
1837
+ // border box crosses a binding ancestor's clip edge on an
1838
+ // axis whose overflow is not \`visible\`. The clip edge is
1839
+ // the padding box, grown by overflow-clip-margin where
1840
+ // that axis's value is \`clip\` (px values only; keyword
1841
+ // forms parse NaN -> 0, i.e. the ungrown padding box).
1842
+ // 1px epsilon: the measured cut lands exactly at the
1843
+ // edge, and sub-pixel layout rounding is not an overhang.
1844
+ const cuts = [];
1845
+ if (clipBinds && (ancestorStyle.overflowX !== "visible" || ancestorStyle.overflowY !== "visible")) {
1846
+ const r = ancestor.getBoundingClientRect();
1847
+ const clipMargin = parseFloat(ancestorStyle.overflowClipMargin) || 0;
1848
+ if (ancestorStyle.overflowX !== "visible") {
1849
+ const grow = ancestorStyle.overflowX === "clip" ? clipMargin : 0;
1850
+ const left = r.left + parseFloat(ancestorStyle.borderLeftWidth) - grow;
1851
+ const right = r.right - parseFloat(ancestorStyle.borderRightWidth) + grow;
1852
+ if (elRect.left < left - 1)
1853
+ cuts.push(Math.round(left - elRect.left) + "px past its left clip edge");
1854
+ if (elRect.right > right + 1)
1855
+ cuts.push(Math.round(elRect.right - right) + "px past its right clip edge");
1856
+ }
1857
+ if (ancestorStyle.overflowY !== "visible") {
1858
+ const grow = ancestorStyle.overflowY === "clip" ? clipMargin : 0;
1859
+ const top = r.top + parseFloat(ancestorStyle.borderTopWidth) - grow;
1860
+ const bottom = r.bottom - parseFloat(ancestorStyle.borderBottomWidth) + grow;
1861
+ if (elRect.top < top - 1)
1862
+ cuts.push(Math.round(top - elRect.top) + "px past its top clip edge");
1863
+ if (elRect.bottom > bottom + 1)
1864
+ cuts.push(Math.round(elRect.bottom - bottom) + "px past its bottom clip edge");
1865
+ }
1866
+ }
1867
+ if ((reasons.length || cuts.length) && !seenLayerTraps.has(ancestor)) {
1613
1868
  seenLayerTraps.add(ancestor);
1614
1869
  const effects = [];
1615
1870
  if (reasons.length)
1616
1871
  effects.push("creates a stacking context (" + reasons.join(", ") + ")");
1617
- if (clips)
1872
+ if (cuts.length)
1618
1873
  effects.push(
1619
- "clips descendants (overflow-x: " + ancestorStyle.overflowX +
1620
- ", overflow-y: " + ancestorStyle.overflowY + ")"
1874
+ "clips it (overflow-x: " + ancestorStyle.overflowX +
1875
+ ", overflow-y: " + ancestorStyle.overflowY +
1876
+ ") — the art extends " + cuts.join(" and ") + " and is cut off there"
1621
1877
  );
1622
1878
  layerTraps.push({
1623
1879
  behind: desc(el),
@@ -1685,10 +1941,17 @@ async function printPdf(page) {
1685
1941
  await page.waitForReady();
1686
1942
  return page.printToPDF();
1687
1943
  }
1944
+ var MAX_SHRINK = 1.5;
1945
+ function shrinkScale(boxes, limitPx) {
1946
+ const maxRight = Math.max(...boxes.map((o) => o.px + Math.min(0, o.left)));
1947
+ if (!(maxRight > limitPx))
1948
+ return null;
1949
+ return Math.max(1 / MAX_SHRINK, limitPx / maxRight);
1950
+ }
1688
1951
  async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport) {
1689
1952
  const contexts = [
1690
1953
  resolvePage(model),
1691
- ...model.pageNames.filter((n) => !n.startsWith("gp-")).map((n) => resolvePage(model, { name: n }))
1954
+ ...model.pageNames.filter((n) => !n.startsWith("gp-") || n.startsWith("gp--flush-")).map((n) => resolvePage(model, { name: n }))
1692
1955
  ];
1693
1956
  const maxContentPt = Math.max(...contexts.map((c) => c.geometry.width - c.geometry.margin.left - c.geometry.margin.right)) + bleedSlugExtensionPt;
1694
1957
  const limitPx = maxContentPt * 96 / 72;
@@ -1775,6 +2038,7 @@ async function predictPageMap(browser, url, agentScript, viewerScript, args, she
1775
2038
  await page.waitForReady();
1776
2039
  await page.evaluate(`window.__GP_MANUAL__ = true;`);
1777
2040
  await page.evaluate(viewerScript);
2041
+ await page.evaluate(`window.__gp.flushRoots()`);
1778
2042
  await page.evaluate(`window.__gp.stringSources(${JSON.stringify(args.stringSets)})`);
1779
2043
  if (args.rectoDecls.length)
1780
2044
  await page.evaluate(`window.__gp.forcedBreakSites(${JSON.stringify(args.rectoDecls)})`);
package/dist/index.js CHANGED
@@ -140,15 +140,15 @@ import {
140
140
  validateProjectPlugins,
141
141
  verifyRepoReadable,
142
142
  writeAppHeartbeat
143
- } from "./index-s416qpdv.js";
143
+ } from "./index-052x5fef.js";
144
144
  import {
145
145
  BUILD_DIAGNOSTIC_CODES
146
- } from "./index-a4kr77td.js";
146
+ } from "./index-wsydvp08.js";
147
147
  import {
148
148
  BuildError,
149
149
  DEFAULT_PRINT_OPTS,
150
150
  readyProbeExpr
151
- } from "./index-mvehxh0e.js";
151
+ } from "./index-c2b6gyeg.js";
152
152
  import {
153
153
  AUTO_SNAPSHOT_MESSAGE,
154
154
  HISTORY_PAGE_LIMIT,
@@ -22,6 +22,13 @@ export interface BuildRunnerOptions {
22
22
  skipLint?: boolean;
23
23
  skipPreValidate?: boolean;
24
24
  skipPostValidate?: boolean;
25
+ /**
26
+ * Proceed past the engine's over-wide-content check (pdf/pdfx only), which
27
+ * otherwise hard-errors because Chromium silently scales the WHOLE book down
28
+ * to fit the offending box. Each offender is still reported as a warning +
29
+ * diagnostic — this buys an eyes-open build, not a clean one.
30
+ */
31
+ allowShrink?: boolean;
25
32
  /**
26
33
  * Keep the pooled headless browser alive after the build returns. A one-shot
27
34
  * CLI build leaves this false so the process can exit; a long-lived
@@ -6,6 +6,15 @@ export interface NativePdfOptions {
6
6
  title?: string;
7
7
  author?: string;
8
8
  signature?: number;
9
+ /**
10
+ * Downgrade the engine's over-wide-content hard error to a warning +
11
+ * diagnostic. The engine's message tells the author to "pass allowShrink to
12
+ * build anyway"; without this the advice is unreachable from every product
13
+ * path (only a test and the parity gate could set it). The book still prints
14
+ * at Chromium's mystery shrink scale, which is why this is opt-in per build
15
+ * and never a config default.
16
+ */
17
+ allowShrink?: boolean;
9
18
  }
10
19
  /**
11
20
  * Render `htmlFile` to `outPdf` via the Gutterpress engine. No HTTP staging,
@@ -41,6 +41,14 @@
41
41
  * .gp-pin — pins within the nearest @page/@spread container;
42
42
  * centered on both axes unless combined with the edge
43
43
  * modifiers .gp-top/.gp-bottom/.gp-left/.gp-right.
44
+ * .gp-flush — with .gp-pin + an edge, the art sits on the PAPER's
45
+ * edge rather than on the text block's. No CSS rule here:
46
+ * the class is a marker both ENGINES implement (see
47
+ * engine/shared/flush.ts), because reaching the paper
48
+ * requires freeing that page's margin — per page — and
49
+ * relocating the furniture that lived in it, neither of
50
+ * which a stylesheet can do. Inert without .gp-pin + an
51
+ * edge word, and inert under plain markdown-it.
44
52
  * .gp-bleed — forces its own page (break-before) and spans it
45
53
  * edge-to-edge horizontally. This does NOT cancel the
46
54
  * top/bottom margins, extend past the trim into printer
@@ -122,4 +130,4 @@
122
130
  * carrying gp-left could start shifting; that is standards-tracking per
123
131
  * CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
124
132
  */
125
- export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible), the same mechanism\n that clips a .gp-bleed plate back to the wrapper's width.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n";
133
+ export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n";
@@ -10,6 +10,56 @@ export default function plugin(md: any, pluginOptions?: {}): void;
10
10
  * on its own page instead of resolving against the document canvas and
11
11
  * painting on the last page of the book.
12
12
  *
13
+ * `min-height: var(--gp-content-h)` is the other half of that contract: a
14
+ * page root that only shrink-wraps its text is a containing block whose
15
+ * `bottom` edge is the end of the PROSE, so `.gp-pin .gp-bottom` (and every
16
+ * hand-written `position: absolute; bottom: 0`) lands under the last
17
+ * paragraph instead of at the page foot. Paged.js used to supply this height
18
+ * for free — `.pagedjs_pagebox > .pagedjs_area > .pagedjs_page_content > div
19
+ * { height: inherit; }` stretched the page root to the page area — and
20
+ * deleting the polyfill (native-only migration, Phase 6) silently took
21
+ * page-boundary pinning with it. Both renderers now publish the page
22
+ * CONTENT height as `--gp-content-h` for the page context the element is in
23
+ * (the viewer on each `.gp-strip`; the compiler on `:root` plus every
24
+ * `page:` assignment selector), and custom properties inherit, so this one
25
+ * rule reaches a page root at any wrapper depth. Undefined var (plain
26
+ * markdown-it use, no engine) falls back to `1px`, which the cushion below
27
+ * takes back to zero — the rule is inert outside the pipeline, leaving the
28
+ * page root shrink-wrapped exactly as it is today.
29
+ *
30
+ * It is `min-height`, not `height`: a `.page` whose content runs past one
31
+ * sheet must still fragment normally. The whole rule is `:where()`, so an
32
+ * author who wants any of it back sets their own value at any specificity.
33
+ *
34
+ * `box-sizing` and `display` are what make a page-sized min-height safe, and
35
+ * both were found by MEASURING a real book (docs/fixtures/css-authoring-spike)
36
+ * print one extra sheet without them:
37
+ *
38
+ * - `border-box`, because a page root with padding (that fixture's cover
39
+ * has `padding-top: 2.5in`) would otherwise be one content box PLUS its
40
+ * padding tall, which is one sheet plus 2.5in — the overflow lands on a
41
+ * spurious blank sheet.
42
+ * - `flow-root`, because a child margin collapsing THROUGH the root's edge
43
+ * survives the forced break and eats into the new sheet: that fixture's
44
+ * last page opens with an `<h2>` whose 22.4px margin-top pushed a
45
+ * 719px-tall root down 22.4px on a 720px sheet, spilling it. A page root
46
+ * is a page; containing its children's margins is what a page does. The
47
+ * viewer already forces `flow-root` on displaced page roots for exactly
48
+ * this reason (`stabilizeFullHeightPageRoots`, fragment.ts) — declaring
49
+ * it once in core means both renderers start from the same box. Zero
50
+ * specificity, so an authored `display: flex`/`grid` page root still
51
+ * wins, which is the case that function's comment warns about.
52
+ *
53
+ * The `- 1px` is the fragmentation cushion, and it is load-bearing. A box
54
+ * whose bottom edge lands EXACTLY on the fragmentainer's edge costs an empty
55
+ * fragment: MEASURED (Chromium 148, 384x480px sheet, 24px margins, one
56
+ * short page root) `min-height: 432px` prints 2 sheets with the second
57
+ * blank while `431px` prints 1 — and the viewer's multicol strip does the
58
+ * same, which is how it first showed up (an 8pp preview of a 7pp book on
59
+ * the gp-image-positioning fixture). One px at 96dpi is 0.75pt: it moves a
60
+ * pinned foot up by less than a point and absorbs sub-pixel rounding in the
61
+ * published value. Do not "clean it up" — the sheet it costs is silent.
62
+ *
13
63
  * The break/orphan rules below (`break-after` on headings, image sizing,
14
64
  * first-child glue) are all `:where()` so they carry zero specificity —
15
65
  * author CSS at any specificity wins outright, reusing this same
@@ -29,4 +79,4 @@ export default function plugin(md: any, pluginOptions?: {}): void;
29
79
  * `text-align: center` still centers it — `display: block` would not).
30
80
  *
31
81
  */
32
- export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
82
+ export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  MANIFEST_FILENAMES,
3
3
  runLint
4
- } from "./cli-5czby0dd.js";
4
+ } from "./cli-5tffrg48.js";
5
5
  import {
6
6
  log
7
- } from "./cli-gk3wsdpb.js";
7
+ } from "./cli-v935c2ws.js";
8
8
  import {
9
9
  UsageError,
10
10
  rejectExtraPositionals,
11
11
  rejectUnknownFlags
12
- } from "./cli-f4rcbt2t.js";
13
- import"./cli-yp3p0sf2.js";
12
+ } from "./cli-7e79d7se.js";
13
+ import"./cli-afm8pa0h.js";
14
14
  import"./cli-c41yr7he.js";
15
15
  import {
16
16
  EXIT_CODES
@@ -5,16 +5,16 @@ import {
5
5
  TARGETS,
6
6
  TARGET_IDS,
7
7
  scaffoldProject
8
- } from "./cli-5czby0dd.js";
8
+ } from "./cli-5tffrg48.js";
9
9
  import {
10
10
  resolveGhostscript
11
- } from "./cli-gk3wsdpb.js";
11
+ } from "./cli-v935c2ws.js";
12
12
  import {
13
13
  UsageError,
14
14
  rejectExtraPositionals,
15
15
  rejectUnknownFlags
16
- } from "./cli-f4rcbt2t.js";
17
- import"./cli-yp3p0sf2.js";
16
+ } from "./cli-7e79d7se.js";
17
+ import"./cli-afm8pa0h.js";
18
18
  import {
19
19
  isToolAvailable
20
20
  } from "./cli-c41yr7he.js";
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  addNpmPlugin,
3
3
  parseNpmPluginSpec
4
- } from "./cli-5czby0dd.js";
5
- import"./cli-gk3wsdpb.js";
4
+ } from "./cli-5tffrg48.js";
5
+ import"./cli-v935c2ws.js";
6
6
  import {
7
7
  UsageError,
8
8
  rejectExtraPositionals,
9
9
  rejectUnknownFlags
10
- } from "./cli-f4rcbt2t.js";
11
- import"./cli-yp3p0sf2.js";
10
+ } from "./cli-7e79d7se.js";
11
+ import"./cli-afm8pa0h.js";
12
12
  import"./cli-c41yr7he.js";
13
13
  import {
14
14
  EXIT_CODES