gutterpress 0.10.1 → 0.10.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/api/index.js +2 -2
  2. package/dist/{audit-p4e7v2m7.js → audit-qz3np6k3.js} +4 -4
  3. package/dist/{build-h0x2w036.js → build-41eds5kk.js} +5 -5
  4. package/dist/{cli-eewynen9.js → cli-c2yvktan.js} +88 -7
  5. package/dist/{cli-4n1p25df.js → cli-daahjr6z.js} +1 -1
  6. package/dist/{cli-yzf38679.js → cli-pfpb1t3h.js} +1 -1
  7. package/dist/{cli-bq7vkea9.js → cli-w3spkryk.js} +36 -19
  8. package/dist/{cli-n79ghfhs.js → cli-wef7qh62.js} +1 -1
  9. package/dist/cli.js +15 -15
  10. package/dist/{doctor-y2w1kcqf.js → doctor-dqmz9wr7.js} +2 -2
  11. package/dist/engine/compiler/build.d.ts +1 -1
  12. package/dist/engine/shared/margin-box-support.d.ts +18 -1
  13. package/dist/{engine-57vw8zpd.js → engine-36jts441.js} +1 -1
  14. package/dist/{engine-8kdwy0jz.js → engine-qg5tdaqv.js} +2 -2
  15. package/dist/{gutterpress-viewer-tma1qwz2.js → gutterpress-viewer-mfeccbys.js} +12 -1
  16. package/dist/{index-j6b6cees.js → index-5vfsbjwz.js} +35 -18
  17. package/dist/{index-p0ebr7dh.js → index-my3v298z.js} +75 -4
  18. package/dist/{index-7hrvksrb.js → index-wbm6p5j3.js} +16 -5
  19. package/dist/index.js +3 -3
  20. package/dist/lib/asset-inline.d.ts +9 -7
  21. package/dist/lib/build-runner.d.ts +6 -7
  22. package/dist/lib/cli-args.d.ts +4 -5
  23. package/dist/lib/engine.d.ts +3 -3
  24. package/dist/lib/markdown/assemble.d.ts +37 -0
  25. package/dist/lib/markdown/gutterpress-css.d.ts +5 -10
  26. package/dist/lib/markdown/markers.d.ts +4 -6
  27. package/dist/lib/markdown/source-range.d.ts +1 -1
  28. package/dist/lib/static-serve.d.ts +1 -2
  29. package/dist/{lint-7wss5p35.js → lint-67xx8zms.js} +4 -4
  30. package/dist/{manifest.schema-zxgxnbg7.json → manifest.schema-084rxtwp.json} +3 -3
  31. package/dist/{new-6gz60bbx.js → new-a4052xsg.js} +4 -4
  32. package/dist/{plugin-7awkmgq0.js → plugin-n4b7b0nx.js} +4 -4
  33. package/dist/{preflight-3tb7stqb.js → preflight-7201sv7c.js} +4 -4
  34. package/dist/preview/server-context.d.ts +6 -6
  35. package/dist/{preview-7a35z1v1.js → preview-ez7yfe8p.js} +5 -5
  36. package/dist/{preview-interface-2f5v96hd.js → preview-interface-9rwz21yw.js} +2 -2
  37. package/dist/{preview-shell-kyjq59d9.js → preview-shell-gqnc5xxe.js} +3 -5
  38. package/dist/{publish-dpx2cebg.js → publish-thxa0cz7.js} +4 -4
  39. package/dist/render.js +3 -1
  40. package/dist/schema/manifest.types.d.ts +4 -5
  41. package/dist/test-helpers/testkit.d.ts +31 -0
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{validate-hbe05b26.js → validate-0tbsdxz2.js} +4 -4
  44. package/package.json +1 -1
@@ -17,7 +17,7 @@ import {
17
17
  resolveChromiumExecutable,
18
18
  run,
19
19
  spawnCapture
20
- } from "./index-7hrvksrb.js";
20
+ } from "./index-wbm6p5j3.js";
21
21
  import {
22
22
  gitFs,
23
23
  gitScopeFor,
@@ -503,10 +503,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
503
503
  throw new UsageError(`Unknown engine "${String(requestedEngine)}". Expected: paged | native`);
504
504
  }
505
505
  if (requestedEngine === "paged") {
506
- warnOnce("engine-paged-removed", "[gutterpress] Paged.js has been removed; the native engine is the " + 'only engine. "engine: paged" is ignored — building natively.');
506
+ warnOnce("engine-paged-removed", "[gutterpress] The native engine is the only engine. " + '"engine: paged" is ignored — building natively.');
507
507
  }
508
508
  if ((m.engineStyles?.paged?.length ?? 0) > 0) {
509
- warnOnce("engine-styles-paged-removed", "[gutterpress] engineStyles.paged is ignored — Paged.js has been removed.");
509
+ warnOnce("engine-styles-paged-removed", "[gutterpress] engineStyles.paged is ignored — the native engine is the only engine.");
510
510
  }
511
511
  const engine = "native";
512
512
  const rawPlugins = c.plugins ?? m.plugins ?? [];
@@ -2002,12 +2002,14 @@ ${projectCss.trim()}` : null
2002
2002
  ].filter(Boolean).join(`
2003
2003
 
2004
2004
  `);
2005
+ const preloadTags = (opts.preloadImages ?? []).map((href) => `
2006
+ <link rel="preload" as="image" href="${href}">`).join("");
2005
2007
  return `<!DOCTYPE html>
2006
2008
  <html lang="en">
2007
2009
  <head>
2008
2010
  <meta charset="UTF-8">
2009
2011
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2010
- <title>${title}</title>
2012
+ <title>${title}</title>${preloadTags}
2011
2013
  <style data-project-css>
2012
2014
  ${inlineCss}
2013
2015
  </style>
@@ -2127,7 +2129,6 @@ import { readFile as readFile2 } from "node:fs/promises";
2127
2129
  import path2 from "node:path";
2128
2130
  import postcss from "postcss";
2129
2131
  var FONT_EXTS = new Set([".woff2", ".woff", ".ttf", ".otf"]);
2130
- var IMAGE_INLINE_MAX_BYTES = 512 * 1024;
2131
2132
  var HASHED_ASSET_DIR = "assets";
2132
2133
  var MIME_BY_EXT = {
2133
2134
  ".woff2": "font/woff2",
@@ -2247,7 +2248,7 @@ function importTarget(atRule) {
2247
2248
  return null;
2248
2249
  return target;
2249
2250
  }
2250
- async function inlineOne(cssPath, projectDir, copies, warnings, seen) {
2251
+ async function inlineOne(cssPath, copies, warnings, seen) {
2251
2252
  const abs = path2.resolve(cssPath);
2252
2253
  if (seen.has(abs))
2253
2254
  return "";
@@ -2282,11 +2283,7 @@ async function inlineOne(cssPath, projectDir, copies, warnings, seen) {
2282
2283
  return dataUri(bytes2, ext);
2283
2284
  }
2284
2285
  const bytes = await readOrThrow(absAsset, "asset", abs);
2285
- if (bytes.byteLength <= IMAGE_INLINE_MAX_BYTES) {
2286
- return dataUri(bytes, ext);
2287
- }
2288
- const projectRel = path2.relative(projectDir, absAsset);
2289
- const dest = projectRel && !escapesProjectRoot(projectDir, absAsset) ? toPosix(projectRel) : `${HASHED_ASSET_DIR}/${contentHash(bytes)}${ext}`;
2286
+ const dest = `${HASHED_ASSET_DIR}/${contentHash(bytes)}${ext}`;
2290
2287
  copies.set(dest, { from: absAsset, to: dest });
2291
2288
  return dest;
2292
2289
  });
@@ -2299,7 +2296,7 @@ async function inlineOne(cssPath, projectDir, copies, warnings, seen) {
2299
2296
  });
2300
2297
  for (const { node, target } of imports) {
2301
2298
  const importedAbs = path2.resolve(cssDir, stripUrlSuffix(decodeRef(target)));
2302
- const inlined = await inlineOne(importedAbs, projectDir, copies, warnings, seen);
2299
+ const inlined = await inlineOne(importedAbs, copies, warnings, seen);
2303
2300
  node.replaceWith(wrapImportConditions(node, postcss.parse(inlined, { from: importedAbs })));
2304
2301
  }
2305
2302
  return root.toString();
@@ -2311,7 +2308,7 @@ async function inlineStyles(projectDir, stylePaths) {
2311
2308
  const parts = [];
2312
2309
  for (const rel of stylePaths) {
2313
2310
  const abs = path2.resolve(projectDir, rel);
2314
- const css = await inlineOne(abs, projectDir, copies, warnings, seen);
2311
+ const css = await inlineOne(abs, copies, warnings, seen);
2315
2312
  if (css.trim().length > 0) {
2316
2313
  parts.push(`/* ${toPosix(path2.relative(projectDir, abs))} */
2317
2314
  ${css.trim()}`);
@@ -2451,6 +2448,7 @@ async function renderChapters(inputDir, opts = {}) {
2451
2448
  files,
2452
2449
  readText: (relPath) => readFile3(join(inputDir, canonicalChapterId(relPath)), "utf-8"),
2453
2450
  projectCss: inlined.css,
2451
+ preloadImages: inlined.copies.map((c) => c.to),
2454
2452
  title: opts.title,
2455
2453
  plugins: opts.plugins,
2456
2454
  pluginCss: opts.pluginCss,
@@ -3980,7 +3978,7 @@ import git from "isomorphic-git";
3980
3978
  // package.json
3981
3979
  var package_default = {
3982
3980
  name: "gutterpress",
3983
- version: "0.10.1",
3981
+ version: "0.10.2-alpha.2",
3984
3982
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3985
3983
  author: "itlackey",
3986
3984
  license: "MPL-2.0",
@@ -4302,6 +4300,18 @@ var marginBoxAtRuleNames = new Set([
4302
4300
  "right-bottom"
4303
4301
  ]);
4304
4302
  var marginBoxIgnoredProperties = MARGIN_BOX_IGNORED_PROPERTIES;
4303
+ function isInPageBox(decl) {
4304
+ const page = decl.parent;
4305
+ return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
4306
+ }
4307
+ function isDroppedPageGradient(decl) {
4308
+ const prop = decl.prop.toLowerCase();
4309
+ if (prop !== "background" && prop !== "background-image")
4310
+ return false;
4311
+ if (!/(^|[\s,(])[\w-]*gradient\(/i.test(decl.value))
4312
+ return false;
4313
+ return isInPageBox(decl);
4314
+ }
4305
4315
  function isInPageMarginBox(decl) {
4306
4316
  const box = decl.parent;
4307
4317
  if (!box || box.type !== "atrule")
@@ -4402,7 +4412,14 @@ function checkCss(css, from) {
4402
4412
  root.walkAtRules((at) => reportRemoteUrls(at.params || "", at));
4403
4413
  root.walkDecls((decl) => {
4404
4414
  const prop = decl.prop.toLowerCase();
4405
- if (prop === "filter") {
4415
+ if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
4416
+ warnings.push({
4417
+ rule: ruleRiskyProps,
4418
+ severity: "warning",
4419
+ message: `Property "${decl.prop}" is not supported in Chromium @page margin boxes and is silently ignored — the chrome renders square/unshadowed. Anything that would paint outside the box (box-shadow, outline, filter) or make it a stacking context (transform, opacity, mix-blend-mode) is dropped; text-shadow, border, border-radius, padding and background gradients all work inside the box.`,
4420
+ ...nodeLoc(decl)
4421
+ });
4422
+ } else if (prop === "filter") {
4406
4423
  warnings.push({
4407
4424
  rule: ruleRiskyProps,
4408
4425
  severity: "warning",
@@ -4416,11 +4433,11 @@ function checkCss(css, from) {
4416
4433
  message: `Property is high-risk for print/PDF (can force rasterization): ${decl.prop}`,
4417
4434
  ...nodeLoc(decl)
4418
4435
  });
4419
- } else if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
4436
+ } else if (isDroppedPageGradient(decl)) {
4420
4437
  warnings.push({
4421
4438
  rule: ruleRiskyProps,
4422
4439
  severity: "warning",
4423
- message: `Property "${decl.prop}" is not supported in Chromium @page margin boxes and is silently ignored the chrome renders square/unshadowed.`,
4440
+ message: `"${decl.prop}: ${decl.value}" on @page paints nothing in print — Chromium drops a gradient in the @page box's own background, with no error and a valid-looking PDF. A solid colour there paints the whole sheet; the same gradient also works on "html" (it covers the content area, not the sheet) or in a margin box.`,
4424
4441
  ...nodeLoc(decl)
4425
4442
  });
4426
4443
  } else if (isUnpairedFitContentWidth(decl)) {
@@ -8016,7 +8033,7 @@ class PdfOutput {
8016
8033
  const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
8017
8034
  await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
8018
8035
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
8019
- const { buildNativePdf } = await import("./engine-8kdwy0jz.js");
8036
+ const { buildNativePdf } = await import("./engine-qg5tdaqv.js");
8020
8037
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
8021
8038
  title: config.title,
8022
8039
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -4,7 +4,7 @@ import {
4
4
  launchChromium,
5
5
  marginBoxAlign,
6
6
  marginBoxRectPt
7
- } from "./index-7hrvksrb.js";
7
+ } from "./index-wbm6p5j3.js";
8
8
 
9
9
  // src/engine/compiler/build.ts
10
10
  import { readFile } from "node:fs/promises";
@@ -1383,7 +1383,8 @@ var BUILD_DIAGNOSTIC_CODES = [
1383
1383
  "engine.multicol.dead-column",
1384
1384
  "engine.content.overheight",
1385
1385
  "engine.image.low-dpi",
1386
- "engine.flush.margin-box"
1386
+ "engine.flush.margin-box",
1387
+ "engine.page-background.unreferenced"
1387
1388
  ];
1388
1389
  async function build(opts) {
1389
1390
  const log = opts.onProgress ?? (() => {});
@@ -1410,6 +1411,7 @@ async function build(opts) {
1410
1411
  deviceScaleFactor: 1,
1411
1412
  mobile: false
1412
1413
  };
1414
+ await page.send("Emulation.setScrollbarsHidden", { hidden: true });
1413
1415
  await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
1414
1416
  await page.send("Emulation.setEmulatedMedia", { media: "print" });
1415
1417
  log(`print media emulated for audits and measurement`);
@@ -1780,13 +1782,77 @@ ${mapCss}`;
1780
1782
  log(`audit: ${audit.length} print-quality warning(s)`);
1781
1783
  }
1782
1784
  {
1783
- const { leaks, multicol, layerTraps } = JSON.parse(await page.evaluate(`(() => {
1785
+ const { leaks, multicol, layerTraps, pageBackgrounds } = JSON.parse(await page.evaluate(`(() => {
1784
1786
  const desc = ${DESC_JS};
1785
1787
  const leaks = [];
1786
1788
  const multicol = [];
1787
1789
  const layerTraps = [];
1788
1790
  const seenLayerTraps = new Set();
1789
1791
 
1792
+ // Images referenced ONLY from inside an @page rule. Chromium fetches
1793
+ // them and then paints nothing — see the pass comment above.
1794
+ const pageBackgrounds = (() => {
1795
+ const absolute = (u) => {
1796
+ try { return new URL(u, document.baseURI).href } catch { return u }
1797
+ };
1798
+ const urlsIn = (text) => {
1799
+ const out = [];
1800
+ const re = /url\\(\\s*(['"]?)([^'")]*)\\1\\s*\\)/g;
1801
+ let m;
1802
+ while ((m = re.exec(text))) if (m[2]) out.push(m[2]);
1803
+ return out;
1804
+ };
1805
+ const owned = new Map();
1806
+ // Resolved by something OTHER than an @page rule — a CSS rule
1807
+ // outside @page, or an element's inline style. Unconditionally
1808
+ // protective: the image is a StyleImage the load already resolved.
1809
+ const cssReferenced = new Set();
1810
+ // Named by a <link rel="preload">. Protective, unless an element
1811
+ // src names the same URL (see srcNamed).
1812
+ const preloaded = new Set();
1813
+ // Named by an element's src. NEVER protective — measured, this is
1814
+ // the one reference type that BREAKS the page box, and it breaks
1815
+ // it whether or not a preload is present (12/12, either document
1816
+ // order). An element reference is evidence of the failure, not of
1817
+ // safety.
1818
+ const srcNamed = new Set();
1819
+ for (const el of document.querySelectorAll("[src]"))
1820
+ srcNamed.add(absolute(el.getAttribute("src")));
1821
+ for (const el of document.querySelectorAll("link[rel~=preload][href]"))
1822
+ preloaded.add(absolute(el.getAttribute("href")));
1823
+ for (const el of document.querySelectorAll("[style]"))
1824
+ for (const u of urlsIn(el.getAttribute("style"))) cssReferenced.add(absolute(u));
1825
+ const walk = (rules, owner) => {
1826
+ for (const rule of rules) {
1827
+ let own = owner;
1828
+ if (rule.constructor.name === "CSSPageRule")
1829
+ own = "@page" + (rule.selectorText ? " " + rule.selectorText : "");
1830
+ else if (owner && rule.name) own = owner + " { @" + rule.name + " }";
1831
+ for (const u of urlsIn(rule.style ? rule.style.cssText : "")) {
1832
+ if (!own) cssReferenced.add(absolute(u));
1833
+ else if (!owned.has(absolute(u))) owned.set(absolute(u), { url: u, where: own });
1834
+ }
1835
+ if (rule.cssRules) walk(rule.cssRules, own);
1836
+ }
1837
+ };
1838
+ for (const sheet of document.styleSheets) {
1839
+ let rules;
1840
+ try { rules = sheet.cssRules } catch { continue }
1841
+ walk(rules, null);
1842
+ }
1843
+ // PROTECTED iff another CSS rule resolves it, OR a preload names
1844
+ // it and no element src names it too. "Something else mentions
1845
+ // this URL" is NOT the test — that inference is inverted for
1846
+ // [src], the very reference type that breaks the page box.
1847
+ return [...owned]
1848
+ .filter(([abs, hit]) =>
1849
+ !/^data:/i.test(hit.url) &&
1850
+ !cssReferenced.has(abs) &&
1851
+ !(preloaded.has(abs) && !srcNamed.has(abs)))
1852
+ .map(([, hit]) => hit)
1853
+ .slice(0, 20);
1854
+ })();
1855
+
1790
1856
  const stackingReasons = (el, cs) => {
1791
1857
  const reasons = [];
1792
1858
  const positioned = cs.position !== "static";
@@ -1907,7 +1973,7 @@ ${mapCss}`;
1907
1973
  }
1908
1974
  }
1909
1975
  }
1910
- return JSON.stringify({ leaks, multicol, layerTraps });
1976
+ return JSON.stringify({ leaks, multicol, layerTraps, pageBackgrounds });
1911
1977
  })()`));
1912
1978
  for (const d of leaks)
1913
1979
  diagnose("engine.abspos.leak", `${d} uses position: absolute with nothing positioned around it, so it is placed against the whole document rather than the page it sits on in your markdown — it can print on a completely different page.`);
@@ -1917,6 +1983,10 @@ ${mapCss}`;
1917
1983
  diagnose("engine.layer.trapped", `${trap.behind} cannot paint behind the page as intended because ancestor ${trap.ancestor} ${trap.detail}. Remove or narrowly scope that containment style around the .gp-behind element.`);
1918
1984
  if (layerTraps.length)
1919
1985
  log(`audit: ${layerTraps.length} trapped layer warning(s)`);
1986
+ for (const hit of pageBackgrounds)
1987
+ diagnose("engine.page-background.unreferenced", `"${hit.url}" is only referenced from "${hit.where}", and Chromium will not print an image referenced nowhere else — the page prints with its background colour alone, with no error. Gutterpress stages and preloads every image your project stylesheets reference, so this one is outside that: it is remote (a url(https://...), which is never staged), or it comes from CSS that does not pass through your stylesheets, or an element in the document uses the same URL as its src, which drops it on its own. Use a local image, referenced from one of your project stylesheets, and not also used as an <img>.`);
1988
+ if (pageBackgrounds.length)
1989
+ log(`audit: ${pageBackgrounds.length} unreferenced @page background image(s)`);
1920
1990
  for (const d of multicol)
1921
1991
  diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. Add column-fill: auto to ${d}.`);
1922
1992
  if (multicol.length)
@@ -2069,6 +2139,7 @@ async function predictPageMap(browser, url, agentScript, viewerScript, args, she
2069
2139
  let page;
2070
2140
  try {
2071
2141
  page = await browser.newPage();
2142
+ await page.send("Emulation.setScrollbarsHidden", { hidden: true });
2072
2143
  await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
2073
2144
  await page.navigate(url);
2074
2145
  await page.evaluate(agentScript);
@@ -593,22 +593,22 @@ import { fileURLToPath } from "node:url";
593
593
  var favicon_default = "./favicon-wkbm9cjn.ico";
594
594
 
595
595
  // src/assets/manifest.schema.json
596
- var manifest_schema_default = "./manifest.schema-zxgxnbg7.json";
596
+ var manifest_schema_default = "./manifest.schema-084rxtwp.json";
597
597
 
598
598
  // src/assets/preview/scripts/preview-interface.js
599
- var preview_interface_default = "./preview-interface-2f5v96hd.js";
599
+ var preview_interface_default = "./preview-interface-9rwz21yw.js";
600
600
 
601
601
  // src/assets/preview/scripts/preview-bridge.js
602
602
  var preview_bridge_default = "./preview-bridge-2nnrs2hy.js";
603
603
 
604
604
  // src/assets/preview/scripts/preview-shell.js
605
- var preview_shell_default = "./preview-shell-kyjq59d9.js";
605
+ var preview_shell_default = "./preview-shell-gqnc5xxe.js";
606
606
 
607
607
  // profiles/CGATS21_CRPC1.icc
608
608
  var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
609
609
 
610
610
  // src/assets/engine/gutterpress-viewer.js
611
- var gutterpress_viewer_default = "./gutterpress-viewer-tma1qwz2.js";
611
+ var gutterpress_viewer_default = "./gutterpress-viewer-mfeccbys.js";
612
612
 
613
613
  // src/assets/engine/gutterpress-agent.js
614
614
  var gutterpress_agent_default = "./gutterpress-agent-cazqstr1.js";
@@ -711,7 +711,18 @@ var MARGIN_BOX_IGNORED_PROPERTIES = new Set([
711
711
  "rotate",
712
712
  "translate",
713
713
  "scale",
714
- "box-shadow"
714
+ "perspective",
715
+ "box-shadow",
716
+ "opacity",
717
+ "outline",
718
+ "outline-color",
719
+ "outline-style",
720
+ "outline-width",
721
+ "outline-offset",
722
+ "filter",
723
+ "backdrop-filter",
724
+ "mix-blend-mode",
725
+ "clip-path"
715
726
  ]);
716
727
  function isIgnoredMarginBoxProperty(property) {
717
728
  return MARGIN_BOX_IGNORED_PROPERTIES.has(property.toLowerCase());
package/dist/index.js CHANGED
@@ -113,15 +113,15 @@ import {
113
113
  syncProject,
114
114
  testRemoteAccess,
115
115
  validateProjectPlugins
116
- } from "./index-j6b6cees.js";
116
+ } from "./index-5vfsbjwz.js";
117
117
  import {
118
118
  BUILD_DIAGNOSTIC_CODES
119
- } from "./index-p0ebr7dh.js";
119
+ } from "./index-my3v298z.js";
120
120
  import {
121
121
  BuildError,
122
122
  DEFAULT_PRINT_OPTS,
123
123
  readyProbeExpr
124
- } from "./index-7hrvksrb.js";
124
+ } from "./index-wbm6p5j3.js";
125
125
  import {
126
126
  AUTO_SNAPSHOT_MESSAGE,
127
127
  HISTORY_PAGE_LIMIT,
@@ -6,16 +6,15 @@
6
6
  * local `@import` closure) is inlined, so a stylesheet's location is irrelevant
7
7
  * to the output and themes/shared design systems need no copying. Fonts become
8
8
  * `data:` URIs — which is what guarantees the byte-identical face reaches
9
- * Chromium, and therefore the PDF. Small images inline; large ones are copied.
9
+ * Chromium, and therefore the PDF. Images are files: every CSS image is copied
10
+ * under a content-addressed name, whatever it weighs.
10
11
  *
11
12
  * A missing stylesheet or font is a build error here, at read time, rather than
12
- * a 404 during pagination that Paged.js parsed as CSS or silently replaced with
13
- * a system face.
13
+ * a 404 during pagination that gets parsed as CSS or silently replaced with a
14
+ * system face.
14
15
  *
15
16
  * Bundle-safe (CLAUDE.md §1/§3): postcss only.
16
17
  */
17
- /** Inline images up to this size; copy larger ones (full-bleed page art). */
18
- export declare const IMAGE_INLINE_MAX_BYTES: number;
19
18
  /** One file the build must copy into the output directory. */
20
19
  export interface AssetCopy {
21
20
  /** Absolute path of the source file. */
@@ -92,8 +91,11 @@ export declare function inlineStyles(projectDir: string, stylePaths: string[]):
92
91
  */
93
92
  export declare function collectStyleDependencies(projectDir: string, stylePaths: string[]): Promise<string[]>;
94
93
  /**
95
- * Turn the renderer's recorded image references into a copy plan. Images keep
96
- * their authored relative path, so the author's folder layout is what ships.
94
+ * Turn the renderer's recorded image references into a copy plan. PROSE images
95
+ * keep their authored relative path, so the author's folder layout is what
96
+ * ships for them. CSS images do NOT — `inlineOne` content-addresses every one,
97
+ * deliberately, so a CSS URL and a prose URL can never coincide (see the
98
+ * comment there). A file used both ways therefore ships twice under two names.
97
99
  * A `../` reference is rejected rather than silently relocated — it has no
98
100
  * representable path under the output root.
99
101
  */
@@ -39,10 +39,10 @@ export interface BuildRunnerOptions {
39
39
  keepBrowserAlive?: boolean;
40
40
  rawArgs: Record<string, unknown>;
41
41
  /**
42
- * CLI `--engine` override. Paged.js has been removed the native engine is
43
- * the only engine — so this is a deprecated no-op accepted for backward
44
- * compatibility only: `"paged"` triggers a one-line warning
45
- * (`manifest.ts`'s resolution) and the build proceeds natively regardless.
42
+ * CLI `--engine` override. The native engine is the only engine, so this is
43
+ * a deprecated no-op accepted for backward compatibility only: `"paged"`
44
+ * triggers a one-line warning (`manifest.ts`'s resolution) and the build
45
+ * proceeds natively regardless.
46
46
  */
47
47
  engine?: "paged" | "native";
48
48
  /**
@@ -70,9 +70,8 @@ export interface BuildRunnerResult {
70
70
  /** As {@link htmlPath}: `null` when nothing but the artifact was published. */
71
71
  fingerprintPath: string | null;
72
72
  /**
73
- * Author-facing print-quality findings from the render (native engine only
74
- * — Paged.js has no equivalent audit). Empty for a clean build. The desktop
75
- * maps these into the Problems panel; the CLI logs them.
73
+ * Author-facing print-quality findings from the render. Empty for a clean
74
+ * build. The desktop maps these into the Problems panel; the CLI logs them.
76
75
  */
77
76
  diagnostics: BuildDiagnostic[];
78
77
  }
@@ -42,11 +42,10 @@ export declare function parseFormat(raw: unknown, opts: {
42
42
  /** Parse `--pdfx-flavor`, which is only valid alongside `--format pdfx`. */
43
43
  export declare function parsePdfxFlavor(raw: unknown, format: BuildFormat): PdfxFlavor | undefined;
44
44
  /**
45
- * Parse `--engine`, shared by `build` and `preview`. Paged.js has been
46
- * removed — the native engine is the only engine so this flag is now an
47
- * accepted-but-ignored no-op: `--engine paged` produces a one-line warning
48
- * (`manifest.ts`'s resolution) and the build proceeds natively regardless.
49
- * `undefined` means "not passed".
45
+ * Parse `--engine`, shared by `build` and `preview`. The native engine is the
46
+ * only engine, so this flag is an accepted-but-ignored no-op: `--engine paged`
47
+ * produces a one-line warning (`manifest.ts`'s resolution) and the build
48
+ * proceeds natively regardless. `undefined` means "not passed".
50
49
  */
51
50
  export declare function parseEngine(raw: unknown): "paged" | "native" | undefined;
52
51
  /** Parse `--port`, defaulting to {@link NETWORK.DEFAULT_PORT} (0 = OS-assigned). */
@@ -17,9 +17,9 @@ export interface NativePdfOptions {
17
17
  allowShrink?: boolean;
18
18
  }
19
19
  /**
20
- * Render `htmlFile` to `outPdf` via the Gutterpress engine. No HTTP staging,
21
- * no Paged.js polyfill — but the pooled/pre-warmed Chromium IS reused (see
22
- * module doc comment): this module never launches or closes a browser itself.
20
+ * Render `htmlFile` to `outPdf` via the Gutterpress engine. No HTTP staging
21
+ * but the pooled/pre-warmed Chromium IS reused (see module doc comment): this
22
+ * module never launches or closes a browser itself.
23
23
  *
24
24
  * Returns the build's author-facing diagnostics so the caller can surface
25
25
  * them (the desktop Problems panel, the CLI's own output). Dropping them here
@@ -39,6 +39,43 @@ export interface AssembleBookHtmlOptions {
39
39
  * relocate or lose during staging.
40
40
  */
41
41
  projectCss?: string;
42
+ /**
43
+ * SHIM — spec gap #152. Output-relative hrefs of the images the project's
44
+ * stylesheets staged (`inlineStyles`'s copy plan, verbatim), each emitted as
45
+ * one `<link rel="preload" as="image">`.
46
+ *
47
+ * Chromium reaches an `@page`-only `url()` lazily, during the print, and the
48
+ * print path CDP drives never waits for a pending resource — so the sheet
49
+ * comes back with its background colour alone, no error, a valid PDF of
50
+ * blank paper (docs/known-limitations.md §3; mechanism in
51
+ * PR #187's `docs/analysis/why-page-background-drops.md`).
52
+ *
53
+ * What the preload buys is that the fetch STARTS during document load
54
+ * instead of during the print. That is not a timing guarantee: a response
55
+ * slow enough still loses (measured — held 1500 ms server-side, the preload
56
+ * row drops too). On the PDF path the asset is a local file staged beside
57
+ * `book.html`, so there is no server to be slow; a published `--format html`
58
+ * bundle read over a slow network can still lose the race.
59
+ *
60
+ * A second ELEMENT reference is not an alternative. Any `[src]` naming the
61
+ * URL drops the page box (measured 12/12, with or without a preload, in
62
+ * either document order) — which is why `asset-inline.ts` content-addresses
63
+ * every CSS image so no element can name one.
64
+ *
65
+ * WHAT PROVES IT IS STILL NEEDED: the expiry canary,
66
+ * `engine/compiler/page-background-chromium-bug.canary.test.ts`. The day it
67
+ * goes red, Chromium has fixed the bug — delete this option, the `.map()`
68
+ * that feeds it in `markdown/index.ts`, and the canary.
69
+ *
70
+ * The copy plan is the source, NOT a scan of the assembled CSS: `pluginCss`
71
+ * never passes through `inlineStyles`, so a `url()` inside it is never
72
+ * staged and a scan would emit a `<link>` to a file that does not exist.
73
+ * The plan is already deduped (keyed by destination), already excludes fonts
74
+ * (inlined) and remote urls (left alone), and already covers the
75
+ * `--paper: url()` + `var(--paper)` shape, because `walkDecls` sees custom
76
+ * properties like any other declaration.
77
+ */
78
+ preloadImages?: string[];
42
79
  title?: string;
43
80
  plugins?: LoadedPlugin[];
44
81
  pluginCss?: string;
@@ -61,17 +61,12 @@
61
61
  * and a plain `width: 100%` already reaches both edges —
62
62
  * with no shrink-to-fit trigger, because nothing out-dents
63
63
  * past the content box. MEASURED (Chromium 148, 6x4in
64
- * sheet, 0.75in margins): the earlier mechanism (a negative
65
- * out-dent of the real page margins, inherited from the
66
- * Paged.js era, where the polyfill published them as
67
- * `--pagedjs-margin-left/right`) shrank the WHOLE document
68
- * ~10% under native print (text run 204.4pt -> 182.9pt),
64
+ * sheet, 0.75in margins): the obvious alternative a
65
+ * negative out-dent of the real page margins shrank the
66
+ * WHOLE document ~10% (text run 204.4pt -> 182.9pt),
69
67
  * because the shrink-to-fit trigger is the page CONTENT
70
- * box, not the sheet that failure mode is why the named
71
- * page exists. Paged.js has since been removed
72
- * (native-only-migration-plan.md Phase 6), so the out-dent
73
- * (whose custom properties nothing sets any more, making it
74
- * a permanent no-op) went with it.
68
+ * box, not the sheet. That failure mode is why the named
69
+ * page exists.
75
70
  *
76
71
  * KNOWN GAP: on the bleed page, native's running head/folio
77
72
  * move onto the trim line (margin boxes are positioned by
@@ -14,12 +14,10 @@ export default function plugin(md: any, pluginOptions?: {}): void;
14
14
  * page root that only shrink-wraps its text is a containing block whose
15
15
  * `bottom` edge is the end of the PROSE, so `.gp-pin .gp-bottom` (and every
16
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
17
+ * paragraph instead of at the page foot. Nothing in a continuous document
18
+ * stretches a page root to the page area on its own, so both renderers
19
+ * publish the page CONTENT height as `--gp-content-h` for the page context
20
+ * the element is in
23
21
  * (the viewer on each `.gp-strip`; the compiler on `:root` plus every
24
22
  * `page:` assignment selector), and custom properties inherit, so this one
25
23
  * rule reaches a page root at any wrapper depth. Undefined var (plain
@@ -12,7 +12,7 @@
12
12
  * items, blockquotes, table rows, fences, footnote definitions, …).
13
13
  * 2. `token.meta.line` — the 1-based marker line threaded onto
14
14
  * `layout_*_open` / `layout_page_break` / `layout_column_break` tokens
15
- * by `markdown-it-paged.js` (see that file's header and the
15
+ * by `markers.js` (see that file's header and the
16
16
  * `t.meta = …` assignment sites). Converted to the same half-open
17
17
  * convention as `[line - 1, line)`.
18
18
  *
@@ -14,8 +14,7 @@ import http from "node:http";
14
14
  * answering a separate question, "what Content-Type does a data: URI need
15
15
  * when embedding this file inline," vs. this table's "what Content-Type does
16
16
  * an HTTP response need" — had drifted again, missing `.webp`/`.avif` here.
17
- * A large (>512KB, over the inliner's threshold) WebP/AVIF image was copied
18
- * as a real file and then served as `application/octet-stream` by both
17
+ * A copied WebP/AVIF image was served as `application/octet-stream` by both
19
18
  * servers above. Added below; if a third image format shows up, add it to
20
19
  * BOTH tables in the same commit (asset-inline.ts is out of scope for this
21
20
  * PR — see docs/reviews/duplication-audit-2026-07-28.md).
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  MANIFEST_FILENAMES,
3
3
  runLint
4
- } from "./cli-bq7vkea9.js";
4
+ } from "./cli-w3spkryk.js";
5
5
  import {
6
6
  log
7
- } from "./cli-n79ghfhs.js";
7
+ } from "./cli-wef7qh62.js";
8
8
  import {
9
9
  UsageError,
10
10
  rejectExtraPositionals,
11
11
  rejectUnknownFlags
12
- } from "./cli-4n1p25df.js";
13
- import"./cli-eewynen9.js";
12
+ } from "./cli-daahjr6z.js";
13
+ import"./cli-c2yvktan.js";
14
14
  import"./cli-c41yr7he.js";
15
15
  import {
16
16
  EXIT_CODES
@@ -31,18 +31,18 @@
31
31
  },
32
32
  "engine": {
33
33
  "type": "string",
34
- "description": "Pagination engine. Paged.js has been removed — the Gutterpress engine (native Chromium pagination) is the only engine, and \"native\" is the only meaningful value. \"paged\" is accepted for backward-compatible manifest parsing only: it produces a one-line warning and the build proceeds natively regardless. `--engine` on the CLI is likewise an accepted-but-ignored no-op.",
34
+ "description": "Pagination engine. The Gutterpress engine (native Chromium pagination) is the only engine, and \"native\" is the only meaningful value. \"paged\" is accepted for backward-compatible manifest parsing only: it produces a one-line warning and the build proceeds natively regardless. `--engine` on the CLI is likewise an accepted-but-ignored no-op.",
35
35
  "enum": ["paged", "native"],
36
36
  "default": "native",
37
37
  "examples": ["native"]
38
38
  },
39
39
  "engineStyles": {
40
40
  "type": "object",
41
- "description": "Engine-conditional stylesheets, appended AFTER `styles`. `.native` is the only list that still applies; `.paged` is accepted-but-ignored (a warning fires if it has entries) now that Paged.js has been removed.",
41
+ "description": "Engine-conditional stylesheets, appended AFTER `styles`. `.native` is the only list that still applies; `.paged` is accepted-but-ignored (a warning fires if it has entries).",
42
42
  "properties": {
43
43
  "paged": {
44
44
  "type": "array",
45
- "description": "REMOVED — Paged.js has been deleted. Accepted for backward-compatible manifest parsing only; entries here are ignored with a warning.",
45
+ "description": "Accepted for backward-compatible manifest parsing only; entries here are ignored with a warning.",
46
46
  "items": { "type": "string" }
47
47
  },
48
48
  "native": { "type": "array", "items": { "type": "string" } }
@@ -5,16 +5,16 @@ import {
5
5
  TARGETS,
6
6
  TARGET_IDS,
7
7
  scaffoldProject
8
- } from "./cli-bq7vkea9.js";
8
+ } from "./cli-w3spkryk.js";
9
9
  import {
10
10
  resolveGhostscript
11
- } from "./cli-n79ghfhs.js";
11
+ } from "./cli-wef7qh62.js";
12
12
  import {
13
13
  UsageError,
14
14
  rejectExtraPositionals,
15
15
  rejectUnknownFlags
16
- } from "./cli-4n1p25df.js";
17
- import"./cli-eewynen9.js";
16
+ } from "./cli-daahjr6z.js";
17
+ import"./cli-c2yvktan.js";
18
18
  import {
19
19
  isToolAvailable
20
20
  } from "./cli-c41yr7he.js";