gutterpress 0.10.0-alpha.4 → 0.10.0

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 (38) hide show
  1. package/README.md +2 -0
  2. package/dist/api/index.js +2 -2
  3. package/dist/{audit-k1vnfwvc.js → audit-cvarpa72.js} +4 -4
  4. package/dist/{build-hqmwgvdw.js → build-6r502chw.js} +6 -4
  5. package/dist/{cli-0r0tq16s.js → cli-149edp6b.js} +1 -1
  6. package/dist/{cli-ra0ed2xt.js → cli-cqtggsng.js} +1 -1
  7. package/dist/{cli-n25qycwz.js → cli-eq5naw4m.js} +222 -141
  8. package/dist/{cli-revgt4pr.js → cli-yzf38679.js} +1 -0
  9. package/dist/{cli-hp9r2pzt.js → cli-zfcryxg8.js} +327 -24
  10. package/dist/cli.js +16 -16
  11. package/dist/{doctor-dxms7ehm.js → doctor-akvxbtjb.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-z4p9sr4h.js → engine-b159tbns.js} +2 -2
  16. package/dist/{engine-wa7y9av9.js → engine-ft4cr3ep.js} +1 -1
  17. package/dist/{gutterpress-agent-1ctgfz92.js → gutterpress-agent-cazqstr1.js} +49 -1
  18. package/dist/{gutterpress-viewer-cem7dmr5.js → gutterpress-viewer-te8g5grx.js} +191 -71
  19. package/dist/{index-05y3dnxq.js → index-ge7q9xj3.js} +330 -249
  20. package/dist/{index-xxg4zfrg.js → index-wq3r5pj7.js} +287 -23
  21. package/dist/{index-9tyq9kks.js → index-ycpvr0am.js} +47 -4
  22. package/dist/index.js +3 -3
  23. package/dist/lib/build-runner.d.ts +7 -0
  24. package/dist/lib/build-staging.d.ts +51 -0
  25. package/dist/lib/engine.d.ts +9 -0
  26. package/dist/lib/markdown/gutterpress-css.d.ts +9 -1
  27. package/dist/lib/markdown/markers.d.ts +51 -1
  28. package/dist/{lint-xjwm5ep8.js → lint-p2sw53d9.js} +4 -4
  29. package/dist/{new-kwdwpf0j.js → new-hvq0x91q.js} +4 -4
  30. package/dist/{plugin-rg4tnn96.js → plugin-pssmk0dx.js} +4 -4
  31. package/dist/{preflight-3127y25z.js → preflight-4j00yd0g.js} +4 -4
  32. package/dist/{preview-ncgfhqmw.js → preview-d4s6gk6p.js} +6 -5
  33. package/dist/{preview-interface-435cczt5.js → preview-interface-0ssk8bmm.js} +153 -6
  34. package/dist/{publish-pr0rwh6p.js → publish-tkc26en7.js} +4 -4
  35. package/dist/render.js +83 -7
  36. package/dist/{repair-8270smfw.js → repair-2va4w12t.js} +4 -4
  37. package/dist/{validate-54e17rae.js → validate-w9vfefrw.js} +4 -4
  38. package/package.json +1 -1
@@ -891,6 +891,88 @@ function specificity(r) {
891
891
  return (r.name ? 2 : 0) + r.pseudos.length;
892
892
  }
893
893
 
894
+ // src/engine/shared/flush.ts
895
+ var FLUSH_EDGES = ["top", "right", "bottom", "left"];
896
+ function flushKey(edges) {
897
+ return FLUSH_EDGES.filter((e) => edges.includes(e)).map((e) => e[0]).join("");
898
+ }
899
+ function flushPageName(authorPage, edges) {
900
+ const safe = authorPage ? authorPage.replace(/[^A-Za-z0-9_-]/g, "_") : "";
901
+ return `gp--flush${safe ? `-${safe}` : ""}-${flushKey(edges)}`;
902
+ }
903
+ function flushMargins(margin, edges) {
904
+ return {
905
+ top: edges.includes("top") ? 0 : margin.top,
906
+ right: edges.includes("right") ? 0 : margin.right,
907
+ bottom: edges.includes("bottom") ? 0 : margin.bottom,
908
+ left: edges.includes("left") ? 0 : margin.left
909
+ };
910
+ }
911
+ function marginBoxesOnEdges(edges) {
912
+ const owners = {
913
+ "top-left-corner": ["top", "left"],
914
+ "top-left": ["top"],
915
+ "top-center": ["top"],
916
+ "top-right": ["top"],
917
+ "top-right-corner": ["top", "right"],
918
+ "bottom-left-corner": ["bottom", "left"],
919
+ "bottom-left": ["bottom"],
920
+ "bottom-center": ["bottom"],
921
+ "bottom-right": ["bottom"],
922
+ "bottom-right-corner": ["bottom", "right"],
923
+ "left-top": ["left"],
924
+ "left-middle": ["left"],
925
+ "left-bottom": ["left"],
926
+ "right-top": ["right"],
927
+ "right-middle": ["right"],
928
+ "right-bottom": ["right"]
929
+ };
930
+ return Object.entries(owners).filter(([, own]) => own.some((e) => edges.includes(e))).map(([name]) => name);
931
+ }
932
+
933
+ // src/engine/shared/margin-box-support.ts
934
+ var MARGIN_BOX_IGNORED_PROPERTIES = new Set([
935
+ "transform",
936
+ "rotate",
937
+ "translate",
938
+ "scale",
939
+ "box-shadow"
940
+ ]);
941
+ function isIgnoredMarginBoxProperty(property) {
942
+ return MARGIN_BOX_IGNORED_PROPERTIES.has(property.toLowerCase());
943
+ }
944
+ function marginBoxRectPt(name, g) {
945
+ const { top, right, bottom, left } = g.margin;
946
+ const cw = g.width - left - right;
947
+ const ch = g.height - top - bottom;
948
+ const third = (n) => n / 3;
949
+ const T = {
950
+ "top-left-corner": [0, 0, left, top],
951
+ "top-left": [left, 0, third(cw), top],
952
+ "top-center": [left + third(cw), 0, third(cw), top],
953
+ "top-right": [left + 2 * third(cw), 0, third(cw), top],
954
+ "top-right-corner": [g.width - right, 0, right, top],
955
+ "bottom-left-corner": [0, g.height - bottom, left, bottom],
956
+ "bottom-left": [left, g.height - bottom, third(cw), bottom],
957
+ "bottom-center": [left + third(cw), g.height - bottom, third(cw), bottom],
958
+ "bottom-right": [left + 2 * third(cw), g.height - bottom, third(cw), bottom],
959
+ "bottom-right-corner": [g.width - right, g.height - bottom, right, bottom],
960
+ "left-top": [0, top, left, third(ch)],
961
+ "left-middle": [0, top + third(ch), left, third(ch)],
962
+ "left-bottom": [0, top + 2 * third(ch), left, third(ch)],
963
+ "right-top": [g.width - right, top, right, third(ch)],
964
+ "right-middle": [g.width - right, top + third(ch), right, third(ch)],
965
+ "right-bottom": [g.width - right, top + 2 * third(ch), right, third(ch)]
966
+ };
967
+ const [x, y, w, h] = T[name] ?? [0, 0, 0, 0];
968
+ return { x, y, w, h };
969
+ }
970
+ function marginBoxAlign(name) {
971
+ if (name.includes("center") || name.includes("middle"))
972
+ return "center";
973
+ return /right/.test(name) ? "end" : "start";
974
+ }
975
+
894
976
  // src/engine/shared/synthesis.ts
895
977
  var RECTO_VERSO_VALUES = /^(right|recto|left|verso)$/;
896
978
  function isRectoVersoBreak(decl) {
@@ -1340,7 +1422,7 @@ var favicon_default = "./favicon-wkbm9cjn.ico";
1340
1422
  var manifest_schema_default = "./manifest.schema-zxgxnbg7.json";
1341
1423
 
1342
1424
  // src/assets/preview/scripts/preview-interface.js
1343
- var preview_interface_default = "./preview-interface-435cczt5.js";
1425
+ var preview_interface_default = "./preview-interface-0ssk8bmm.js";
1344
1426
 
1345
1427
  // src/assets/preview/scripts/preview-bridge.js
1346
1428
  var preview_bridge_default = "./preview-bridge-fz7vpk8m.js";
@@ -1352,10 +1434,10 @@ var preview_shell_default = "./preview-shell-c5mfa3q0.js";
1352
1434
  var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
1353
1435
 
1354
1436
  // src/assets/engine/gutterpress-viewer.js
1355
- var gutterpress_viewer_default = "./gutterpress-viewer-cem7dmr5.js";
1437
+ var gutterpress_viewer_default = "./gutterpress-viewer-te8g5grx.js";
1356
1438
 
1357
1439
  // src/assets/engine/gutterpress-agent.js
1358
- var gutterpress_agent_default = "./gutterpress-agent-1ctgfz92.js";
1440
+ var gutterpress_agent_default = "./gutterpress-agent-cazqstr1.js";
1359
1441
 
1360
1442
  // src/assets/templates/book/manifest.yaml
1361
1443
  var manifest_default = "./manifest-n1gh3qw5.yaml";
@@ -1739,8 +1821,7 @@ async function build(opts) {
1739
1821
  await page.evaluate(AGENT);
1740
1822
  await page.waitForReady();
1741
1823
  const cssText = await page.evaluate(`window.__gp.collectCss()`);
1742
- const model = extract(cssText);
1743
- const { tier3Reasons } = classify(model);
1824
+ let model = extract(cssText);
1744
1825
  const baseGeom = resolvePage(model).geometry;
1745
1826
  const sheetViewport = {
1746
1827
  width: Math.max(1, Math.round(baseGeom.width * 96 / 72)),
@@ -1751,6 +1832,97 @@ async function build(opts) {
1751
1832
  await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
1752
1833
  await page.send("Emulation.setEmulatedMedia", { media: "print" });
1753
1834
  log(`print media emulated for audits and measurement`);
1835
+ const flushRoots = await page.evaluate(`window.__gp.flushRoots()`);
1836
+ const flushGroups = new Map;
1837
+ for (const root of flushRoots) {
1838
+ if (flushGroups.has(root.key))
1839
+ continue;
1840
+ const authorPage = root.page === "auto" ? undefined : root.page;
1841
+ flushGroups.set(root.key, {
1842
+ authorPage,
1843
+ edges: root.edges,
1844
+ genName: flushPageName(authorPage, root.edges),
1845
+ key: root.key
1846
+ });
1847
+ }
1848
+ if (flushGroups.size) {
1849
+ const lines = [];
1850
+ for (const group of flushGroups.values()) {
1851
+ const relocated = new Set(marginBoxesOnEdges(group.edges));
1852
+ const pseudoSets = [[]];
1853
+ if (group.authorPage) {
1854
+ for (const rule of model.pageRules) {
1855
+ if (rule.name !== group.authorPage)
1856
+ continue;
1857
+ const pseudo = rule.pseudos.length ? `:${rule.pseudos.join(":")}` : "";
1858
+ if (rule.pseudos.length && !pseudoSets.some((ps) => ps.length === rule.pseudos.length && rule.pseudos.every((x) => ps.includes(x))))
1859
+ pseudoSets.push(rule.pseudos);
1860
+ const body = [];
1861
+ for (const [prop, value] of Object.entries(rule.decls))
1862
+ body.push(` ${prop}: ${value};`);
1863
+ for (const [box, decls] of Object.entries(rule.marginBoxes)) {
1864
+ if (relocated.has(box.slice(1)))
1865
+ continue;
1866
+ body.push(` ${box} {`);
1867
+ for (const [prop, value] of Object.entries(decls))
1868
+ body.push(` ${prop}: ${value};`);
1869
+ body.push(` }`);
1870
+ }
1871
+ lines.push(`@page ${group.genName}${pseudo} {
1872
+ ${body.join(`
1873
+ `)}
1874
+ }`);
1875
+ }
1876
+ }
1877
+ for (const pseudos of pseudoSets) {
1878
+ const pseudo = pseudos.length ? `:${pseudos.join(":")}` : "";
1879
+ const body = [];
1880
+ for (const edge of group.edges)
1881
+ body.push(` margin-${edge}: 0;`);
1882
+ for (const box of relocated)
1883
+ body.push(` @${box} { content: none; }`);
1884
+ lines.push(`@page ${group.genName}${pseudo} {
1885
+ ${body.join(`
1886
+ `)}
1887
+ }`);
1888
+ }
1889
+ lines.push(`:where(.page, .spread)[data-gp-flush="${group.key}"][data-gp-flush][data-gp-flush][data-gp-flush] { page: ${group.genName}; }`);
1890
+ }
1891
+ const flushCss = lines.join(`
1892
+ `);
1893
+ await page.evaluate(`window.__gp.addCss("gp-flush-css", ${JSON.stringify(flushCss)})`);
1894
+ model = extract(`${cssText}
1895
+ ${flushCss}`);
1896
+ log(`.gp-flush: ${flushRoots.length} pinned root(s) -> ${flushGroups.size} generated page context(s)`);
1897
+ }
1898
+ const { tier3Reasons } = classify(model);
1899
+ const pseudoContexts = pagePseudoContexts(model);
1900
+ const contentHeightPt = (name) => Math.min(...pseudoContexts.map((pseudos) => {
1901
+ const geometry = resolvePage(model, { name, pseudos }).geometry;
1902
+ return geometry.height - geometry.margin.top - geometry.margin.bottom;
1903
+ }));
1904
+ const contentHeightPx = contentHeightPt() * 96 / 72;
1905
+ const namedContentHeightsPx = Object.fromEntries(model.pageNames.map((name) => [name, contentHeightPt(name) * 96 / 72]));
1906
+ const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
1907
+ const pageVarsCss = [
1908
+ `:root { --gp-content-h: ${contentHeightPx}px; }`,
1909
+ ...model.pageAssignments.map((a) => `${a.selector} { --gp-content-h: ${namedContentHeightsPx[a.page] ?? contentHeightPx}px; }`)
1910
+ ].join(`
1911
+ `);
1912
+ await page.evaluate(`window.__gp.addCss("gp-page-vars", ${JSON.stringify(pageVarsCss)})`);
1913
+ log(`page geometry published (--gp-content-h: ${Math.round(contentHeightPx)}px)`);
1914
+ const relocationBoxes = (group, pseudos) => {
1915
+ const ctx = resolvePage(model, { name: group.authorPage, pseudos });
1916
+ return marginBoxesOnEdges(group.edges).map((name) => [name, ctx.marginBoxes[`@${name}`]]).filter((pair) => {
1917
+ const c = pair[1]?.content?.trim();
1918
+ return !!c && c !== "none" && c !== "normal";
1919
+ });
1920
+ };
1921
+ const furnitureRoots = flushRoots.filter((root) => {
1922
+ const group = flushGroups.get(root.key);
1923
+ return pseudoContexts.some((pseudos) => relocationBoxes(group, pseudos).length > 0);
1924
+ });
1925
+ const flushDiagnosed = new Set;
1754
1926
  const tier2 = synthesize({
1755
1927
  model,
1756
1928
  marks: opts.marks,
@@ -1770,11 +1942,15 @@ async function build(opts) {
1770
1942
  }).join(`
1771
1943
  `);
1772
1944
  if (widthOffenders.boxes.length) {
1773
- const msg = `content wider than the page content box triggers Chromium print ` + `shrink-to-fit (the WHOLE book scales down, silently):
1945
+ const scale = shrinkScale(widthOffenders.boxes, widthOffenders.limitPx);
1946
+ 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`;
1947
+ const msg = `${headline}:
1774
1948
  ${describe(widthOffenders.boxes)}`;
1775
1949
  if (opts.allowShrink) {
1950
+ if (scale !== null)
1951
+ diagnose("engine.width.overflow", `${headline}.`);
1776
1952
  for (const o of widthOffenders.boxes)
1777
- 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."}`);
1953
+ 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."}`);
1778
1954
  log(`WARNING: ${msg}`);
1779
1955
  } else {
1780
1956
  throw new Error(`${msg}
@@ -1789,7 +1965,7 @@ ${describe(widthOffenders.intrinsics)}`;
1789
1965
  log(`WARNING: ${msg}`);
1790
1966
  }
1791
1967
  const rectoDecls = model.breaks.filter(isRectoVersoBreak);
1792
- const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0;
1968
+ const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0 || furnitureRoots.length > 0;
1793
1969
  let tier = tier2.geometry.bleed > 0 || tier2.geometry.slug > 0 ? 2 : 1;
1794
1970
  let passes = 1;
1795
1971
  let pageMap = {};
@@ -1817,6 +1993,8 @@ ${describe(widthOffenders.intrinsics)}`;
1817
1993
  targets.add(s.id);
1818
1994
  for (const s of resetSites)
1819
1995
  targets.add(s.id);
1996
+ for (const r of furnitureRoots)
1997
+ targets.add(r.id);
1820
1998
  await page.evaluate(`window.__gp.instrument(${JSON.stringify([...targets])})`);
1821
1999
  const targetText = await page.evaluate(`window.__gp.targetTexts(${JSON.stringify([...targets])})`);
1822
2000
  const brokenXrefs = findBrokenXrefRefs(sites, targetText);
@@ -1901,6 +2079,73 @@ ${describe(widthOffenders.intrinsics)}`;
1901
2079
  /* Tier 3 */
1902
2080
  ${mapCss}`;
1903
2081
  }
2082
+ if (furnitureRoots.length) {
2083
+ const pageAt = (id) => map[id] ?? predictedForResult?.pageMap[id];
2084
+ const byName = new Map;
2085
+ for (const s of sources) {
2086
+ const p = pageAt(s.id);
2087
+ if (!p)
2088
+ continue;
2089
+ const list = byName.get(s.name) ?? [];
2090
+ list.push({ page: p, value: s.text });
2091
+ byName.set(s.name, list);
2092
+ }
2093
+ for (const entries of byName.values())
2094
+ entries.sort((a, b) => a.page - b.page);
2095
+ const PXPT = 96 / 72;
2096
+ const items = [];
2097
+ for (const root of furnitureRoots) {
2098
+ const physical = pageAt(root.id);
2099
+ if (!physical)
2100
+ continue;
2101
+ const group = flushGroups.get(root.key);
2102
+ const pseudos = [physical % 2 === 1 ? "right" : "left"];
2103
+ if (physical === 1)
2104
+ pseudos.push("first");
2105
+ const ctx = resolvePage(model, { name: group.authorPage, pseudos });
2106
+ const g = ctx.geometry;
2107
+ const eff = flushMargins(g.margin, group.edges);
2108
+ const boxes = [];
2109
+ for (const [name, decls] of relocationBoxes(group, pseudos)) {
2110
+ const text = evaluate(decls.content, {
2111
+ page: toFolioPage(physical, pageValues),
2112
+ pages: pageCount,
2113
+ strings: (n, w) => stringValueAt(byName.get(n) ?? [], physical, parseWhich(w)),
2114
+ targetPage: (u) => {
2115
+ const p = pageAt(u.replace(/^#/, ""));
2116
+ return p === undefined ? undefined : toFolioPage(p, pageValues);
2117
+ }
2118
+ });
2119
+ if (!text) {
2120
+ if (!flushDiagnosed.has(name)) {
2121
+ flushDiagnosed.add(name);
2122
+ 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.`);
2123
+ }
2124
+ continue;
2125
+ }
2126
+ const r = marginBoxRectPt(name, g);
2127
+ const outDecls = {};
2128
+ for (const [prop, value] of Object.entries(decls)) {
2129
+ if (prop.toLowerCase() === "content" || isIgnoredMarginBoxProperty(prop))
2130
+ continue;
2131
+ outDecls[prop] = value;
2132
+ }
2133
+ boxes.push({
2134
+ box: name,
2135
+ x: (r.x - eff.left) * PXPT,
2136
+ y: (r.y - eff.top) * PXPT,
2137
+ w: r.w * PXPT,
2138
+ h: r.h * PXPT,
2139
+ align: marginBoxAlign(name),
2140
+ text,
2141
+ decls: outDecls
2142
+ });
2143
+ }
2144
+ items.push({ id: root.id, boxes });
2145
+ }
2146
+ if (items.length)
2147
+ await page.evaluate(`window.__gp.setFlushFurniture(${JSON.stringify(items)})`);
2148
+ }
1904
2149
  };
1905
2150
  const predicted = await predictPageMap(browser, url, AGENT, VIEWER, {
1906
2151
  stringSets: model.stringSets.map((s) => ({ selector: s.selector, name: s.name, value: s.value })),
@@ -1939,14 +2184,6 @@ ${mapCss}`;
1939
2184
  log(`tier 3: NOT converged after ${passes} passes`);
1940
2185
  }
1941
2186
  }
1942
- const pseudoContexts = pagePseudoContexts(model);
1943
- const contentHeightPt = (name) => Math.min(...pseudoContexts.map((pseudos) => {
1944
- const geometry = resolvePage(model, { name, pseudos }).geometry;
1945
- return geometry.height - geometry.margin.top - geometry.margin.bottom;
1946
- }));
1947
- const contentHeightPx = contentHeightPt() * 96 / 72;
1948
- const namedContentHeightsPx = Object.fromEntries(model.pageNames.map((name) => [name, contentHeightPt(name) * 96 / 72]));
1949
- const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
1950
2187
  {
1951
2188
  const audit = await page.evaluate(`window.__gp.auditContent(${JSON.stringify({
1952
2189
  default: contentHeightPx,
@@ -2000,6 +2237,26 @@ ${mapCss}`;
2000
2237
  return reasons;
2001
2238
  };
2002
2239
 
2240
+ // Does this box establish the containing block for an absolutely
2241
+ // positioned descendant? Overflow clipping binds an abspos
2242
+ // .gp-pin only from its containing block outward — MEASURED: a
2243
+ // pin whose box lay entirely outside a STATIC overflow:hidden
2244
+ // (and overflow:clip) wrapper printed complete and still behind
2245
+ // the page text; the wrapper's clip never touched it.
2246
+ const establishesAbsContainingBlock = (cs) => {
2247
+ if (cs.position !== "static") return true;
2248
+ for (const prop of [
2249
+ "transform", "translate", "rotate", "scale", "perspective",
2250
+ "filter", "backdropFilter",
2251
+ ]) {
2252
+ const value = cs[prop];
2253
+ if (value && value !== "none") return true;
2254
+ }
2255
+ if (/\\b(layout|paint|strict|content)\\b/.test(cs.contain)) return true;
2256
+ if (cs.containerType && cs.containerType !== "normal") return true;
2257
+ return /\\b(transform|translate|rotate|scale|perspective|filter)\\b/.test(cs.willChange);
2258
+ };
2259
+
2003
2260
  for (const el of document.querySelectorAll("*")) {
2004
2261
  const cs = getComputedStyle(el);
2005
2262
  if (leaks.length < 20) {
@@ -2040,20 +2297,58 @@ ${mapCss}`;
2040
2297
  ) {
2041
2298
  const boundary = el.closest(".page, .spread");
2042
2299
  if (boundary) {
2300
+ const elRect = el.getBoundingClientRect();
2301
+ // Clip binding: an in-flow .gp-behind is bound by every
2302
+ // ancestor's overflow, but an abspos one only from its
2303
+ // containing block outward — static wrappers in between
2304
+ // never clip it (measured; see the pass comment above).
2305
+ let clipBinds = cs.position !== "absolute" && cs.position !== "fixed";
2043
2306
  for (let ancestor = el.parentElement; ancestor; ancestor = ancestor.parentElement) {
2044
2307
  const ancestorStyle = getComputedStyle(ancestor);
2308
+ if (!clipBinds && establishesAbsContainingBlock(ancestorStyle)) clipBinds = true;
2045
2309
  const reasons = stackingReasons(ancestor, ancestorStyle);
2046
- const clips = ancestorStyle.overflowX !== "visible" ||
2047
- ancestorStyle.overflowY !== "visible";
2048
- if ((reasons.length || clips) && !seenLayerTraps.has(ancestor)) {
2310
+ // Clipping never reorders layers it can only CUT the
2311
+ // art (measured, pass comment above): warn only where the
2312
+ // border box crosses a binding ancestor's clip edge on an
2313
+ // axis whose overflow is not \`visible\`. The clip edge is
2314
+ // the padding box, grown by overflow-clip-margin where
2315
+ // that axis's value is \`clip\` (px values only; keyword
2316
+ // forms parse NaN -> 0, i.e. the ungrown padding box).
2317
+ // 1px epsilon: the measured cut lands exactly at the
2318
+ // edge, and sub-pixel layout rounding is not an overhang.
2319
+ const cuts = [];
2320
+ if (clipBinds && (ancestorStyle.overflowX !== "visible" || ancestorStyle.overflowY !== "visible")) {
2321
+ const r = ancestor.getBoundingClientRect();
2322
+ const clipMargin = parseFloat(ancestorStyle.overflowClipMargin) || 0;
2323
+ if (ancestorStyle.overflowX !== "visible") {
2324
+ const grow = ancestorStyle.overflowX === "clip" ? clipMargin : 0;
2325
+ const left = r.left + parseFloat(ancestorStyle.borderLeftWidth) - grow;
2326
+ const right = r.right - parseFloat(ancestorStyle.borderRightWidth) + grow;
2327
+ if (elRect.left < left - 1)
2328
+ cuts.push(Math.round(left - elRect.left) + "px past its left clip edge");
2329
+ if (elRect.right > right + 1)
2330
+ cuts.push(Math.round(elRect.right - right) + "px past its right clip edge");
2331
+ }
2332
+ if (ancestorStyle.overflowY !== "visible") {
2333
+ const grow = ancestorStyle.overflowY === "clip" ? clipMargin : 0;
2334
+ const top = r.top + parseFloat(ancestorStyle.borderTopWidth) - grow;
2335
+ const bottom = r.bottom - parseFloat(ancestorStyle.borderBottomWidth) + grow;
2336
+ if (elRect.top < top - 1)
2337
+ cuts.push(Math.round(top - elRect.top) + "px past its top clip edge");
2338
+ if (elRect.bottom > bottom + 1)
2339
+ cuts.push(Math.round(elRect.bottom - bottom) + "px past its bottom clip edge");
2340
+ }
2341
+ }
2342
+ if ((reasons.length || cuts.length) && !seenLayerTraps.has(ancestor)) {
2049
2343
  seenLayerTraps.add(ancestor);
2050
2344
  const effects = [];
2051
2345
  if (reasons.length)
2052
2346
  effects.push("creates a stacking context (" + reasons.join(", ") + ")");
2053
- if (clips)
2347
+ if (cuts.length)
2054
2348
  effects.push(
2055
- "clips descendants (overflow-x: " + ancestorStyle.overflowX +
2056
- ", overflow-y: " + ancestorStyle.overflowY + ")"
2349
+ "clips it (overflow-x: " + ancestorStyle.overflowX +
2350
+ ", overflow-y: " + ancestorStyle.overflowY +
2351
+ ") — the art extends " + cuts.join(" and ") + " and is cut off there"
2057
2352
  );
2058
2353
  layerTraps.push({
2059
2354
  behind: desc(el),
@@ -2121,10 +2416,17 @@ async function printPdf(page) {
2121
2416
  await page.waitForReady();
2122
2417
  return page.printToPDF();
2123
2418
  }
2419
+ var MAX_SHRINK = 1.5;
2420
+ function shrinkScale(boxes, limitPx) {
2421
+ const maxRight = Math.max(...boxes.map((o) => o.px + Math.min(0, o.left)));
2422
+ if (!(maxRight > limitPx))
2423
+ return null;
2424
+ return Math.max(1 / MAX_SHRINK, limitPx / maxRight);
2425
+ }
2124
2426
  async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport) {
2125
2427
  const contexts = [
2126
2428
  resolvePage(model),
2127
- ...model.pageNames.filter((n) => !n.startsWith("gp-")).map((n) => resolvePage(model, { name: n }))
2429
+ ...model.pageNames.filter((n) => !n.startsWith("gp-") || n.startsWith("gp--flush-")).map((n) => resolvePage(model, { name: n }))
2128
2430
  ];
2129
2431
  const maxContentPt = Math.max(...contexts.map((c) => c.geometry.width - c.geometry.margin.left - c.geometry.margin.right)) + bleedSlugExtensionPt;
2130
2432
  const limitPx = maxContentPt * 96 / 72;
@@ -2211,6 +2513,7 @@ async function predictPageMap(browser, url, agentScript, viewerScript, args, she
2211
2513
  await page.waitForReady();
2212
2514
  await page.evaluate(`window.__GP_MANUAL__ = true;`);
2213
2515
  await page.evaluate(viewerScript);
2516
+ await page.evaluate(`window.__gp.flushRoots()`);
2214
2517
  await page.evaluate(`window.__gp.stringSources(${JSON.stringify(args.stringSets)})`);
2215
2518
  if (args.rectoDecls.length)
2216
2519
  await page.evaluate(`window.__gp.forcedBreakSites(${JSON.stringify(args.rectoDecls)})`);
@@ -2340,4 +2643,4 @@ ${lines.length ? lines.join(`
2340
2643
  `);
2341
2644
  }
2342
2645
 
2343
- export { RENDER_TIMEOUT_MS, prewarmBrowser, getBrowser, closeBrowser, getAssetPath, REQUIRED_MILESTONE, assertMilestone, connectChromium, build };
2646
+ export { RENDER_TIMEOUT_MS, prewarmBrowser, getBrowser, closeBrowser, getAssetPath, MARGIN_BOX_IGNORED_PROPERTIES, REQUIRED_MILESTONE, assertMilestone, connectChromium, build };
package/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  previewArgs
4
- } from "./cli-revgt4pr.js";
4
+ } from "./cli-yzf38679.js";
5
5
  import {
6
6
  UsageError,
7
7
  package_default,
8
8
  rejectUnknownFlags
9
- } from "./cli-0r0tq16s.js";
9
+ } from "./cli-149edp6b.js";
10
10
  import {
11
11
  EXIT_CODES
12
12
  } from "./cli-46ycxe6r.js";
@@ -18,17 +18,17 @@ import {
18
18
  import { defineCommand, parseArgs, runMain } from "citty";
19
19
  import { statSync } from "node:fs";
20
20
  var SUBCOMMANDS = {
21
- new: () => import("./new-kwdwpf0j.js").then((m) => m.default),
22
- preview: () => import("./preview-ncgfhqmw.js").then((m) => m.default),
23
- build: () => import("./build-hqmwgvdw.js").then((m) => m.default),
24
- publish: () => import("./publish-pr0rwh6p.js").then((m) => m.default),
25
- lint: () => import("./lint-xjwm5ep8.js").then((m) => m.default),
26
- validate: () => import("./validate-54e17rae.js").then((m) => m.default),
27
- audit: () => import("./audit-k1vnfwvc.js").then((m) => m.default),
28
- preflight: () => import("./preflight-3127y25z.js").then((m) => m.default),
29
- repair: () => import("./repair-8270smfw.js").then((m) => m.default),
30
- doctor: () => import("./doctor-dxms7ehm.js").then((m) => m.default),
31
- plugin: () => import("./plugin-rg4tnn96.js").then((m) => m.default)
21
+ new: () => import("./new-hvq0x91q.js").then((m) => m.default),
22
+ preview: () => import("./preview-d4s6gk6p.js").then((m) => m.default),
23
+ build: () => import("./build-6r502chw.js").then((m) => m.default),
24
+ publish: () => import("./publish-tkc26en7.js").then((m) => m.default),
25
+ lint: () => import("./lint-p2sw53d9.js").then((m) => m.default),
26
+ validate: () => import("./validate-w9vfefrw.js").then((m) => m.default),
27
+ audit: () => import("./audit-cvarpa72.js").then((m) => m.default),
28
+ preflight: () => import("./preflight-4j00yd0g.js").then((m) => m.default),
29
+ repair: () => import("./repair-2va4w12t.js").then((m) => m.default),
30
+ doctor: () => import("./doctor-akvxbtjb.js").then((m) => m.default),
31
+ plugin: () => import("./plugin-pssmk0dx.js").then((m) => m.default)
32
32
  };
33
33
  var VERSION = package_default.version;
34
34
  var main = defineCommand({
@@ -53,7 +53,7 @@ async function preflightRequiredInvocations(rawArgs) {
53
53
  const [command, ...commandArgs] = rawArgs;
54
54
  try {
55
55
  if (command === "new") {
56
- const { newArgs } = await import("./new-kwdwpf0j.js");
56
+ const { newArgs } = await import("./new-hvq0x91q.js");
57
57
  rejectUnknownFlags(commandArgs, newArgs, "new");
58
58
  const parsed2 = parseArgs(commandArgs, {
59
59
  ...newArgs,
@@ -65,7 +65,7 @@ async function preflightRequiredInvocations(rawArgs) {
65
65
  return;
66
66
  }
67
67
  if (command === "preflight") {
68
- const { preflightArgs } = await import("./preflight-3127y25z.js");
68
+ const { preflightArgs } = await import("./preflight-4j00yd0g.js");
69
69
  rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
70
70
  const parsed2 = parseArgs(commandArgs, {
71
71
  ...preflightArgs,
@@ -89,7 +89,7 @@ async function preflightRequiredInvocations(rawArgs) {
89
89
  if (subcommand !== "add") {
90
90
  throw new UsageError(`gutterpress plugin: unknown command "${subcommand}"`);
91
91
  }
92
- const { pluginAddArgs } = await import("./plugin-rg4tnn96.js");
92
+ const { pluginAddArgs } = await import("./plugin-pssmk0dx.js");
93
93
  rejectUnknownFlags(subcommandArgs, pluginAddArgs, "plugin add");
94
94
  const parsed = parseArgs(subcommandArgs, {
95
95
  ...pluginAddArgs,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  getSystemDiagnostics,
3
3
  log
4
- } from "./cli-ra0ed2xt.js";
4
+ } from "./cli-cqtggsng.js";
5
5
  import {
6
6
  UsageError,
7
7
  rejectExtraPositionals,
8
8
  rejectUnknownFlags
9
- } from "./cli-0r0tq16s.js";
9
+ } from "./cli-149edp6b.js";
10
10
  import"./cli-c41yr7he.js";
11
11
  import"./cli-46ycxe6r.js";
12
12
  import"./cli-37x76zdn.js";
@@ -7,7 +7,7 @@ import { type PostprocessResult } from "./postprocess.ts";
7
7
  * set is closed here so a surface's label table can be asserted complete
8
8
  * against it rather than drifting silently as checks are added.
9
9
  */
10
- export type BuildDiagnosticCode = "engine.width.overflow" | "engine.width.intrinsic" | "engine.xref.broken" | "engine.abspos.leak" | "engine.layer.trapped" | "engine.multicol.dead-column" | "engine.content.overheight" | "engine.image.low-dpi";
10
+ export type BuildDiagnosticCode = "engine.width.overflow" | "engine.width.intrinsic" | "engine.xref.broken" | "engine.abspos.leak" | "engine.layer.trapped" | "engine.multicol.dead-column" | "engine.content.overheight" | "engine.image.low-dpi" | "engine.flush.margin-box";
11
11
  export declare const BUILD_DIAGNOSTIC_CODES: readonly BuildDiagnosticCode[];
12
12
  export interface BuildDiagnostic {
13
13
  code: BuildDiagnosticCode;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `.gp-flush` policy — shared by the compiler and the viewer, like every rule
3
+ * that DECIDES something (see synthesis.ts's header).
4
+ *
5
+ * A pinned image reaches the paper only if the page's own margin is removed
6
+ * on that edge: Chromium's printable area IS the page area — MEASURED, a
7
+ * pinned box pulled into the margin with negative insets fragments onto the
8
+ * NEXT sheet, one moved there with a transform is clipped away entirely, and
9
+ * a margin box in a ~zero margin (1px, with padding/height compensation)
10
+ * simply does not render. So flush is implemented as page GEOMETRY, per page:
11
+ *
12
+ * - The COMPILER aliases the flush root's own page context under a
13
+ * generated name (verbatim rule copies — never a resolved flatten, which
14
+ * is the "re-implement the @page cascade" trap tier2's history records),
15
+ * zeroes the flushed margins on the alias, and assigns the root to it.
16
+ * - The VIEWER keeps the author's page context and adjusts the strip
17
+ * geometry in JS from these same functions.
18
+ *
19
+ * Margin boxes live IN the margin, so the flushed edge's furniture (folio,
20
+ * running head) cannot render natively there — MEASURED, no compensation
21
+ * trick survives. Both renderers therefore keep painting that furniture
22
+ * themselves at its ORIGINAL coordinates: the viewer already synthesizes all
23
+ * furniture as DOM; the compiler injects the flushed edge's boxes into the
24
+ * page root as positioned elements with engine-resolved text. Nothing the
25
+ * author declared is lost — that is the contract this module's box map
26
+ * exists to keep.
27
+ */
28
+ import type { PageGeometry } from "./gcpm-extract.ts";
29
+ export declare const FLUSH_EDGES: readonly ["top", "right", "bottom", "left"];
30
+ export type FlushEdge = (typeof FLUSH_EDGES)[number];
31
+ /** Minimal DOM shape needed by `flushEdgesIn` — keeps this module node-safe. */
32
+ interface QueryRoot {
33
+ querySelector(sel: string): unknown;
34
+ }
35
+ /**
36
+ * Which edges the flush pins inside `root` ask for, in canonical t/r/b/l
37
+ * order. A `.gp-flush` without `.gp-pin` or without an edge word is inert by
38
+ * construction — every selector here requires all three.
39
+ */
40
+ export declare function flushEdgesIn(root: QueryRoot): FlushEdge[];
41
+ /** Canonical short key for an edge set: "b", "rb", "trbl"… (t/r/b/l order). */
42
+ export declare function flushKey(edges: readonly FlushEdge[]): string;
43
+ /**
44
+ * Generated page name for (author page context, edge set). The `gp--` double
45
+ * dash marks an engine-generated page (the `gp--blank` convention); these
46
+ * exist only in builds that actually contain a flush pin, which is why the
47
+ * width check may treat them as real author contexts.
48
+ */
49
+ export declare function flushPageName(authorPage: string | undefined, edges: readonly FlushEdge[]): string;
50
+ /** The page's margins with the flushed edges freed. */
51
+ export declare function flushMargins(margin: PageGeometry["margin"], edges: readonly FlushEdge[]): PageGeometry["margin"];
52
+ /**
53
+ * Margin boxes that live (wholly or partly) in a flushed edge's margin area —
54
+ * the set both renderers must keep painting themselves. A corner box belongs
55
+ * to BOTH of its edges: `bottom-left-corner` dies when either the bottom or
56
+ * the left margin is freed.
57
+ */
58
+ export declare function marginBoxesOnEdges(edges: readonly FlushEdge[]): string[];
59
+ export {};
@@ -10,3 +10,27 @@
10
10
  */
11
11
  export declare const MARGIN_BOX_IGNORED_PROPERTIES: ReadonlySet<string>;
12
12
  export declare function isIgnoredMarginBoxProperty(property: string): boolean;
13
+ /**
14
+ * Geometry of each of the 16 margin boxes, per CSS Paged Media §5.3, in pt
15
+ * SHEET coordinates (origin at the sheet's top-left). One implementation for
16
+ * both furniture painters: the viewer's `drawMarginBoxes` overlays and the
17
+ * compiler's `.gp-flush` furniture relocation must place a box at the same
18
+ * spot or preview and print disagree about where the folio sits.
19
+ */
20
+ export declare function marginBoxRectPt(name: string, g: {
21
+ width: number;
22
+ height: number;
23
+ margin: {
24
+ top: number;
25
+ right: number;
26
+ bottom: number;
27
+ left: number;
28
+ };
29
+ }): {
30
+ x: number;
31
+ y: number;
32
+ w: number;
33
+ h: number;
34
+ };
35
+ /** Horizontal alignment of a margin box's content within its slot. */
36
+ export declare function marginBoxAlign(name: string): "start" | "center" | "end";