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
package/dist/api/index.js CHANGED
@@ -95,12 +95,12 @@ import {
95
95
  syncProject,
96
96
  testRemoteAccess,
97
97
  validateProjectPlugins
98
- } from "../index-j6b6cees.js";
98
+ } from "../index-5vfsbjwz.js";
99
99
  import {
100
100
  BuildError,
101
101
  DEFAULT_PRINT_OPTS,
102
102
  readyProbeExpr
103
- } from "../index-7hrvksrb.js";
103
+ } from "../index-wbm6p5j3.js";
104
104
  import {
105
105
  AUTO_SNAPSHOT_MESSAGE,
106
106
  HISTORY_PAGE_LIMIT,
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-bq7vkea9.js";
3
+ } from "./cli-w3spkryk.js";
4
4
  import {
5
5
  log
6
- } from "./cli-n79ghfhs.js";
6
+ } from "./cli-wef7qh62.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-4n1p25df.js";
12
- import"./cli-eewynen9.js";
11
+ } from "./cli-daahjr6z.js";
12
+ import"./cli-c2yvktan.js";
13
13
  import"./cli-c41yr7he.js";
14
14
  import {
15
15
  EXIT_CODES
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runBuild,
3
3
  splitOutPath
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
  parseEngine,
@@ -12,8 +12,8 @@ import {
12
12
  parsePdfxFlavor,
13
13
  rejectExtraPositionals,
14
14
  rejectUnknownFlags
15
- } from "./cli-4n1p25df.js";
16
- import"./cli-eewynen9.js";
15
+ } from "./cli-daahjr6z.js";
16
+ import"./cli-c2yvktan.js";
17
17
  import"./cli-c41yr7he.js";
18
18
  import {
19
19
  BuildError
@@ -38,7 +38,7 @@ var commandArgs = {
38
38
  "skip-pre-validate": { type: "boolean", description: "Skip pre-build validation" },
39
39
  "skip-post-validate": { type: "boolean", description: "Skip post-build PDF/X validation" },
40
40
  "allow-shrink": { type: "boolean", description: "Build anyway when content is wider than the page content box, instead of failing. Chromium then scales the WHOLE book down to fit; each offender is reported as a warning." },
41
- engine: { type: "string", description: "Pagination engine. Paged.js has been removed; native is the only engine. This flag is accepted but ignored (a warning fires for --engine paged)." }
41
+ engine: { type: "string", description: "Pagination engine. Native is the only engine. This flag is accepted but ignored (a warning fires for --engine paged)." }
42
42
  };
43
43
  var build_default = defineCommand({
44
44
  meta: {
@@ -936,7 +936,18 @@ var MARGIN_BOX_IGNORED_PROPERTIES = new Set([
936
936
  "rotate",
937
937
  "translate",
938
938
  "scale",
939
- "box-shadow"
939
+ "perspective",
940
+ "box-shadow",
941
+ "opacity",
942
+ "outline",
943
+ "outline-color",
944
+ "outline-style",
945
+ "outline-width",
946
+ "outline-offset",
947
+ "filter",
948
+ "backdrop-filter",
949
+ "mix-blend-mode",
950
+ "clip-path"
940
951
  ]);
941
952
  function isIgnoredMarginBoxProperty(property) {
942
953
  return MARGIN_BOX_IGNORED_PROPERTIES.has(property.toLowerCase());
@@ -1419,22 +1430,22 @@ import { fileURLToPath } from "node:url";
1419
1430
  var favicon_default = "./favicon-wkbm9cjn.ico";
1420
1431
 
1421
1432
  // src/assets/manifest.schema.json
1422
- var manifest_schema_default = "./manifest.schema-zxgxnbg7.json";
1433
+ var manifest_schema_default = "./manifest.schema-084rxtwp.json";
1423
1434
 
1424
1435
  // src/assets/preview/scripts/preview-interface.js
1425
- var preview_interface_default = "./preview-interface-2f5v96hd.js";
1436
+ var preview_interface_default = "./preview-interface-9rwz21yw.js";
1426
1437
 
1427
1438
  // src/assets/preview/scripts/preview-bridge.js
1428
1439
  var preview_bridge_default = "./preview-bridge-2nnrs2hy.js";
1429
1440
 
1430
1441
  // src/assets/preview/scripts/preview-shell.js
1431
- var preview_shell_default = "./preview-shell-kyjq59d9.js";
1442
+ var preview_shell_default = "./preview-shell-gqnc5xxe.js";
1432
1443
 
1433
1444
  // profiles/CGATS21_CRPC1.icc
1434
1445
  var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
1435
1446
 
1436
1447
  // src/assets/engine/gutterpress-viewer.js
1437
- var gutterpress_viewer_default = "./gutterpress-viewer-tma1qwz2.js";
1448
+ var gutterpress_viewer_default = "./gutterpress-viewer-mfeccbys.js";
1438
1449
 
1439
1450
  // src/assets/engine/gutterpress-agent.js
1440
1451
  var gutterpress_agent_default = "./gutterpress-agent-cazqstr1.js";
@@ -1885,6 +1896,7 @@ async function build(opts) {
1885
1896
  deviceScaleFactor: 1,
1886
1897
  mobile: false
1887
1898
  };
1899
+ await page.send("Emulation.setScrollbarsHidden", { hidden: true });
1888
1900
  await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
1889
1901
  await page.send("Emulation.setEmulatedMedia", { media: "print" });
1890
1902
  log(`print media emulated for audits and measurement`);
@@ -2255,13 +2267,77 @@ ${mapCss}`;
2255
2267
  log(`audit: ${audit.length} print-quality warning(s)`);
2256
2268
  }
2257
2269
  {
2258
- const { leaks, multicol, layerTraps } = JSON.parse(await page.evaluate(`(() => {
2270
+ const { leaks, multicol, layerTraps, pageBackgrounds } = JSON.parse(await page.evaluate(`(() => {
2259
2271
  const desc = ${DESC_JS};
2260
2272
  const leaks = [];
2261
2273
  const multicol = [];
2262
2274
  const layerTraps = [];
2263
2275
  const seenLayerTraps = new Set();
2264
2276
 
2277
+ // Images referenced ONLY from inside an @page rule. Chromium fetches
2278
+ // them and then paints nothing — see the pass comment above.
2279
+ const pageBackgrounds = (() => {
2280
+ const absolute = (u) => {
2281
+ try { return new URL(u, document.baseURI).href } catch { return u }
2282
+ };
2283
+ const urlsIn = (text) => {
2284
+ const out = [];
2285
+ const re = /url\\(\\s*(['"]?)([^'")]*)\\1\\s*\\)/g;
2286
+ let m;
2287
+ while ((m = re.exec(text))) if (m[2]) out.push(m[2]);
2288
+ return out;
2289
+ };
2290
+ const owned = new Map();
2291
+ // Resolved by something OTHER than an @page rule — a CSS rule
2292
+ // outside @page, or an element's inline style. Unconditionally
2293
+ // protective: the image is a StyleImage the load already resolved.
2294
+ const cssReferenced = new Set();
2295
+ // Named by a <link rel="preload">. Protective, unless an element
2296
+ // src names the same URL (see srcNamed).
2297
+ const preloaded = new Set();
2298
+ // Named by an element's src. NEVER protective — measured, this is
2299
+ // the one reference type that BREAKS the page box, and it breaks
2300
+ // it whether or not a preload is present (12/12, either document
2301
+ // order). An element reference is evidence of the failure, not of
2302
+ // safety.
2303
+ const srcNamed = new Set();
2304
+ for (const el of document.querySelectorAll("[src]"))
2305
+ srcNamed.add(absolute(el.getAttribute("src")));
2306
+ for (const el of document.querySelectorAll("link[rel~=preload][href]"))
2307
+ preloaded.add(absolute(el.getAttribute("href")));
2308
+ for (const el of document.querySelectorAll("[style]"))
2309
+ for (const u of urlsIn(el.getAttribute("style"))) cssReferenced.add(absolute(u));
2310
+ const walk = (rules, owner) => {
2311
+ for (const rule of rules) {
2312
+ let own = owner;
2313
+ if (rule.constructor.name === "CSSPageRule")
2314
+ own = "@page" + (rule.selectorText ? " " + rule.selectorText : "");
2315
+ else if (owner && rule.name) own = owner + " { @" + rule.name + " }";
2316
+ for (const u of urlsIn(rule.style ? rule.style.cssText : "")) {
2317
+ if (!own) cssReferenced.add(absolute(u));
2318
+ else if (!owned.has(absolute(u))) owned.set(absolute(u), { url: u, where: own });
2319
+ }
2320
+ if (rule.cssRules) walk(rule.cssRules, own);
2321
+ }
2322
+ };
2323
+ for (const sheet of document.styleSheets) {
2324
+ let rules;
2325
+ try { rules = sheet.cssRules } catch { continue }
2326
+ walk(rules, null);
2327
+ }
2328
+ // PROTECTED iff another CSS rule resolves it, OR a preload names
2329
+ // it and no element src names it too. "Something else mentions
2330
+ // this URL" is NOT the test — that inference is inverted for
2331
+ // [src], the very reference type that breaks the page box.
2332
+ return [...owned]
2333
+ .filter(([abs, hit]) =>
2334
+ !/^data:/i.test(hit.url) &&
2335
+ !cssReferenced.has(abs) &&
2336
+ !(preloaded.has(abs) && !srcNamed.has(abs)))
2337
+ .map(([, hit]) => hit)
2338
+ .slice(0, 20);
2339
+ })();
2340
+
2265
2341
  const stackingReasons = (el, cs) => {
2266
2342
  const reasons = [];
2267
2343
  const positioned = cs.position !== "static";
@@ -2382,7 +2458,7 @@ ${mapCss}`;
2382
2458
  }
2383
2459
  }
2384
2460
  }
2385
- return JSON.stringify({ leaks, multicol, layerTraps });
2461
+ return JSON.stringify({ leaks, multicol, layerTraps, pageBackgrounds });
2386
2462
  })()`));
2387
2463
  for (const d of leaks)
2388
2464
  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.`);
@@ -2392,6 +2468,10 @@ ${mapCss}`;
2392
2468
  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.`);
2393
2469
  if (layerTraps.length)
2394
2470
  log(`audit: ${layerTraps.length} trapped layer warning(s)`);
2471
+ for (const hit of pageBackgrounds)
2472
+ 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>.`);
2473
+ if (pageBackgrounds.length)
2474
+ log(`audit: ${pageBackgrounds.length} unreferenced @page background image(s)`);
2395
2475
  for (const d of multicol)
2396
2476
  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}.`);
2397
2477
  if (multicol.length)
@@ -2544,6 +2624,7 @@ async function predictPageMap(browser, url, agentScript, viewerScript, args, she
2544
2624
  let page;
2545
2625
  try {
2546
2626
  page = await browser.newPage();
2627
+ await page.send("Emulation.setScrollbarsHidden", { hidden: true });
2547
2628
  await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
2548
2629
  await page.navigate(url);
2549
2630
  await page.evaluate(agentScript);
@@ -4,7 +4,7 @@ import {
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "gutterpress",
7
- version: "0.10.1",
7
+ version: "0.10.2-alpha.2",
8
8
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
9
9
  author: "itlackey",
10
10
  license: "MPL-2.0",
@@ -22,7 +22,7 @@ var previewArgs = {
22
22
  "skip-pre-validate": { type: "boolean", description: "Skip pre-build validation (pdf|pdfx only)" },
23
23
  "skip-post-validate": { type: "boolean", description: "Skip post-build PDF/X validation (pdfx only)" },
24
24
  "allow-shrink": { type: "boolean", description: "Build anyway when content is wider than the page content box, instead of failing. Chromium then scales the WHOLE book down to fit; the scale and each offender are reported as warnings. (pdf|pdfx only)" },
25
- engine: { type: "string", description: "Pagination engine. Paged.js has been removed; native is the only engine. This flag is accepted but ignored (a warning fires for --engine paged)." }
25
+ engine: { type: "string", description: "Pagination engine. Native is the only engine. This flag is accepted but ignored (a warning fires for --engine paged)." }
26
26
  };
27
27
 
28
28
  export { previewArgs };
@@ -13,12 +13,12 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-n79ghfhs.js";
16
+ } from "./cli-wef7qh62.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-4n1p25df.js";
21
+ } from "./cli-daahjr6z.js";
22
22
  import {
23
23
  MARGIN_BOX_IGNORED_PROPERTIES,
24
24
  RENDER_TIMEOUT_MS,
@@ -27,7 +27,7 @@ import {
27
27
  getAssetPath,
28
28
  getBrowser,
29
29
  prewarmBrowser
30
- } from "./cli-eewynen9.js";
30
+ } from "./cli-c2yvktan.js";
31
31
  import {
32
32
  INSTALL_HINTS,
33
33
  execCapture,
@@ -466,10 +466,10 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
466
466
  throw new UsageError(`Unknown engine "${String(requestedEngine)}". Expected: paged | native`);
467
467
  }
468
468
  if (requestedEngine === "paged") {
469
- warnOnce("engine-paged-removed", "[gutterpress] Paged.js has been removed; the native engine is the " + 'only engine. "engine: paged" is ignored — building natively.');
469
+ warnOnce("engine-paged-removed", "[gutterpress] The native engine is the only engine. " + '"engine: paged" is ignored — building natively.');
470
470
  }
471
471
  if ((m.engineStyles?.paged?.length ?? 0) > 0) {
472
- warnOnce("engine-styles-paged-removed", "[gutterpress] engineStyles.paged is ignored — Paged.js has been removed.");
472
+ warnOnce("engine-styles-paged-removed", "[gutterpress] engineStyles.paged is ignored — the native engine is the only engine.");
473
473
  }
474
474
  const engine = "native";
475
475
  const rawPlugins = c.plugins ?? m.plugins ?? [];
@@ -1126,6 +1126,18 @@ var marginBoxAtRuleNames = new Set([
1126
1126
  "right-bottom"
1127
1127
  ]);
1128
1128
  var marginBoxIgnoredProperties = MARGIN_BOX_IGNORED_PROPERTIES;
1129
+ function isInPageBox(decl) {
1130
+ const page = decl.parent;
1131
+ return !!page && page.type === "atrule" && page.name.toLowerCase() === "page";
1132
+ }
1133
+ function isDroppedPageGradient(decl) {
1134
+ const prop = decl.prop.toLowerCase();
1135
+ if (prop !== "background" && prop !== "background-image")
1136
+ return false;
1137
+ if (!/(^|[\s,(])[\w-]*gradient\(/i.test(decl.value))
1138
+ return false;
1139
+ return isInPageBox(decl);
1140
+ }
1129
1141
  function isInPageMarginBox(decl) {
1130
1142
  const box = decl.parent;
1131
1143
  if (!box || box.type !== "atrule")
@@ -1226,7 +1238,14 @@ function checkCss(css, from) {
1226
1238
  root.walkAtRules((at) => reportRemoteUrls(at.params || "", at));
1227
1239
  root.walkDecls((decl) => {
1228
1240
  const prop = decl.prop.toLowerCase();
1229
- if (prop === "filter") {
1241
+ if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
1242
+ warnings.push({
1243
+ rule: ruleRiskyProps,
1244
+ severity: "warning",
1245
+ 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.`,
1246
+ ...nodeLoc(decl)
1247
+ });
1248
+ } else if (prop === "filter") {
1230
1249
  warnings.push({
1231
1250
  rule: ruleRiskyProps,
1232
1251
  severity: "warning",
@@ -1240,11 +1259,11 @@ function checkCss(css, from) {
1240
1259
  message: `Property is high-risk for print/PDF (can force rasterization): ${decl.prop}`,
1241
1260
  ...nodeLoc(decl)
1242
1261
  });
1243
- } else if (marginBoxIgnoredProperties.has(prop) && isInPageMarginBox(decl)) {
1262
+ } else if (isDroppedPageGradient(decl)) {
1244
1263
  warnings.push({
1245
1264
  rule: ruleRiskyProps,
1246
1265
  severity: "warning",
1247
- message: `Property "${decl.prop}" is not supported in Chromium @page margin boxes and is silently ignored the chrome renders square/unshadowed.`,
1266
+ 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.`,
1248
1267
  ...nodeLoc(decl)
1249
1268
  });
1250
1269
  } else if (isUnpairedFitContentWidth(decl)) {
@@ -2776,7 +2795,6 @@ import { readFile as readFile9 } from "node:fs/promises";
2776
2795
  import path3 from "node:path";
2777
2796
  import postcss2 from "postcss";
2778
2797
  var FONT_EXTS = new Set([".woff2", ".woff", ".ttf", ".otf"]);
2779
- var IMAGE_INLINE_MAX_BYTES = 512 * 1024;
2780
2798
  var HASHED_ASSET_DIR = "assets";
2781
2799
  var MIME_BY_EXT = {
2782
2800
  ".woff2": "font/woff2",
@@ -2896,7 +2914,7 @@ function importTarget(atRule) {
2896
2914
  return null;
2897
2915
  return target;
2898
2916
  }
2899
- async function inlineOne(cssPath, projectDir, copies, warnings, seen) {
2917
+ async function inlineOne(cssPath, copies, warnings, seen) {
2900
2918
  const abs = path3.resolve(cssPath);
2901
2919
  if (seen.has(abs))
2902
2920
  return "";
@@ -2931,11 +2949,7 @@ async function inlineOne(cssPath, projectDir, copies, warnings, seen) {
2931
2949
  return dataUri(bytes2, ext);
2932
2950
  }
2933
2951
  const bytes = await readOrThrow(absAsset, "asset", abs);
2934
- if (bytes.byteLength <= IMAGE_INLINE_MAX_BYTES) {
2935
- return dataUri(bytes, ext);
2936
- }
2937
- const projectRel = path3.relative(projectDir, absAsset);
2938
- const dest = projectRel && !escapesProjectRoot(projectDir, absAsset) ? toPosix(projectRel) : `${HASHED_ASSET_DIR}/${contentHash(bytes)}${ext}`;
2952
+ const dest = `${HASHED_ASSET_DIR}/${contentHash(bytes)}${ext}`;
2939
2953
  copies.set(dest, { from: absAsset, to: dest });
2940
2954
  return dest;
2941
2955
  });
@@ -2948,7 +2962,7 @@ async function inlineOne(cssPath, projectDir, copies, warnings, seen) {
2948
2962
  });
2949
2963
  for (const { node, target } of imports) {
2950
2964
  const importedAbs = path3.resolve(cssDir, stripUrlSuffix(decodeRef(target)));
2951
- const inlined = await inlineOne(importedAbs, projectDir, copies, warnings, seen);
2965
+ const inlined = await inlineOne(importedAbs, copies, warnings, seen);
2952
2966
  node.replaceWith(wrapImportConditions(node, postcss2.parse(inlined, { from: importedAbs })));
2953
2967
  }
2954
2968
  return root.toString();
@@ -2960,7 +2974,7 @@ async function inlineStyles(projectDir, stylePaths) {
2960
2974
  const parts = [];
2961
2975
  for (const rel of stylePaths) {
2962
2976
  const abs = path3.resolve(projectDir, rel);
2963
- const css = await inlineOne(abs, projectDir, copies, warnings, seen);
2977
+ const css = await inlineOne(abs, copies, warnings, seen);
2964
2978
  if (css.trim().length > 0) {
2965
2979
  parts.push(`/* ${toPosix(path3.relative(projectDir, abs))} */
2966
2980
  ${css.trim()}`);
@@ -6287,12 +6301,14 @@ ${projectCss.trim()}` : null
6287
6301
  ].filter(Boolean).join(`
6288
6302
 
6289
6303
  `);
6304
+ const preloadTags = (opts.preloadImages ?? []).map((href) => `
6305
+ <link rel="preload" as="image" href="${href}">`).join("");
6290
6306
  return `<!DOCTYPE html>
6291
6307
  <html lang="en">
6292
6308
  <head>
6293
6309
  <meta charset="UTF-8">
6294
6310
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6295
- <title>${title}</title>
6311
+ <title>${title}</title>${preloadTags}
6296
6312
  <style data-project-css>
6297
6313
  ${inlineCss}
6298
6314
  </style>
@@ -6324,6 +6340,7 @@ async function renderChapters(inputDir, opts = {}) {
6324
6340
  files,
6325
6341
  readText: (relPath) => readFile18(join2(inputDir, canonicalChapterId(relPath)), "utf-8"),
6326
6342
  projectCss: inlined.css,
6343
+ preloadImages: inlined.copies.map((c) => c.to),
6327
6344
  title: opts.title,
6328
6345
  plugins: opts.plugins,
6329
6346
  pluginCss: opts.pluginCss,
@@ -7521,7 +7538,7 @@ class PdfOutput {
7521
7538
  const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
7522
7539
  await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
7523
7540
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
7524
- const { buildNativePdf } = await import("./engine-57vw8zpd.js");
7541
+ const { buildNativePdf } = await import("./engine-36jts441.js");
7525
7542
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
7526
7543
  title: config.title,
7527
7544
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./cli-4n1p25df.js";
3
+ } from "./cli-daahjr6z.js";
4
4
  import {
5
5
  execCapture,
6
6
  findTool,
package/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  previewArgs
4
- } from "./cli-yzf38679.js";
4
+ } from "./cli-pfpb1t3h.js";
5
5
  import {
6
6
  UsageError,
7
7
  package_default,
8
8
  rejectUnknownFlags
9
- } from "./cli-4n1p25df.js";
9
+ } from "./cli-daahjr6z.js";
10
10
  import {
11
11
  EXIT_CODES
12
12
  } from "./cli-46ycxe6r.js";
@@ -18,16 +18,16 @@ import {
18
18
  import { defineCommand, parseArgs, runMain } from "citty";
19
19
  import { statSync } from "node:fs";
20
20
  var SUBCOMMANDS = {
21
- new: () => import("./new-6gz60bbx.js").then((m) => m.default),
22
- preview: () => import("./preview-7a35z1v1.js").then((m) => m.default),
23
- build: () => import("./build-h0x2w036.js").then((m) => m.default),
24
- publish: () => import("./publish-dpx2cebg.js").then((m) => m.default),
25
- lint: () => import("./lint-7wss5p35.js").then((m) => m.default),
26
- validate: () => import("./validate-hbe05b26.js").then((m) => m.default),
27
- audit: () => import("./audit-p4e7v2m7.js").then((m) => m.default),
28
- preflight: () => import("./preflight-3tb7stqb.js").then((m) => m.default),
29
- doctor: () => import("./doctor-y2w1kcqf.js").then((m) => m.default),
30
- plugin: () => import("./plugin-7awkmgq0.js").then((m) => m.default)
21
+ new: () => import("./new-a4052xsg.js").then((m) => m.default),
22
+ preview: () => import("./preview-ez7yfe8p.js").then((m) => m.default),
23
+ build: () => import("./build-41eds5kk.js").then((m) => m.default),
24
+ publish: () => import("./publish-thxa0cz7.js").then((m) => m.default),
25
+ lint: () => import("./lint-67xx8zms.js").then((m) => m.default),
26
+ validate: () => import("./validate-0tbsdxz2.js").then((m) => m.default),
27
+ audit: () => import("./audit-qz3np6k3.js").then((m) => m.default),
28
+ preflight: () => import("./preflight-7201sv7c.js").then((m) => m.default),
29
+ doctor: () => import("./doctor-dqmz9wr7.js").then((m) => m.default),
30
+ plugin: () => import("./plugin-n4b7b0nx.js").then((m) => m.default)
31
31
  };
32
32
  var VERSION = package_default.version;
33
33
  var main = defineCommand({
@@ -52,7 +52,7 @@ async function preflightRequiredInvocations(rawArgs) {
52
52
  const [command, ...commandArgs] = rawArgs;
53
53
  try {
54
54
  if (command === "new") {
55
- const { newArgs } = await import("./new-6gz60bbx.js");
55
+ const { newArgs } = await import("./new-a4052xsg.js");
56
56
  rejectUnknownFlags(commandArgs, newArgs, "new");
57
57
  const parsed2 = parseArgs(commandArgs, {
58
58
  ...newArgs,
@@ -64,7 +64,7 @@ async function preflightRequiredInvocations(rawArgs) {
64
64
  return;
65
65
  }
66
66
  if (command === "preflight") {
67
- const { preflightArgs } = await import("./preflight-3tb7stqb.js");
67
+ const { preflightArgs } = await import("./preflight-7201sv7c.js");
68
68
  rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
69
69
  const parsed2 = parseArgs(commandArgs, {
70
70
  ...preflightArgs,
@@ -88,7 +88,7 @@ async function preflightRequiredInvocations(rawArgs) {
88
88
  if (subcommand !== "add") {
89
89
  throw new UsageError(`gutterpress plugin: unknown command "${subcommand}"`);
90
90
  }
91
- const { pluginAddArgs } = await import("./plugin-7awkmgq0.js");
91
+ const { pluginAddArgs } = await import("./plugin-n4b7b0nx.js");
92
92
  rejectUnknownFlags(subcommandArgs, pluginAddArgs, "plugin add");
93
93
  const parsed = parseArgs(subcommandArgs, {
94
94
  ...pluginAddArgs,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  getSystemDiagnostics,
3
3
  log
4
- } from "./cli-n79ghfhs.js";
4
+ } from "./cli-wef7qh62.js";
5
5
  import {
6
6
  UsageError,
7
7
  rejectExtraPositionals,
8
8
  rejectUnknownFlags
9
- } from "./cli-4n1p25df.js";
9
+ } from "./cli-daahjr6z.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" | "engine.flush.margin-box";
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" | "engine.page-background.unreferenced";
11
11
  export declare const BUILD_DIAGNOSTIC_CODES: readonly BuildDiagnosticCode[];
12
12
  export interface BuildDiagnostic {
13
13
  code: BuildDiagnosticCode;
@@ -2,7 +2,24 @@
2
2
  * Declarations Chromium accepts syntactically in `@page` margin boxes but
3
3
  * silently drops in native print. These were measured with deliberately
4
4
  * visible values (`rotate(-12deg)`, `6px 6px 0 #c00`) and are documented in
5
- * docs/engine-history/ENGINE.md §8.
5
+ * docs/engine/ENGINE.md §8 and docs/known-limitations.md §2.
6
+ *
7
+ * The line is what the property PAINTS, not the property family: everything
8
+ * that establishes a stacking context or paints outside the border box is
9
+ * dropped, everything that paints inside it is honoured. `text-shadow` beside
10
+ * `box-shadow` is the clearest pair. Re-measured on Chrome 151.0.7922.75,
11
+ * 96dpi raster, mean absolute pixel difference against the same box without
12
+ * the declaration (control: removing the box entirely, 2.6485):
13
+ *
14
+ * dropped, all 0.0000 box-shadow · transform (rotate/scale/translate) ·
15
+ * rotate/scale/translate · opacity · outline (and its
16
+ * longhands) · filter · mix-blend-mode ·
17
+ * backdrop-filter · clip-path · perspective
18
+ * honoured text-shadow 0.1375 · border-radius 0.3397 ·
19
+ * background gradient 11.2260 · writing-mode 0.3200 ·
20
+ * padding-left 0.2927 · font-size 0.7864 ·
21
+ * color 0.1063 · letter-spacing 0.2781 ·
22
+ * text-transform 0.1804 · visibility 2.6485
6
23
  *
7
24
  * Keep the viewer and print-safety linter on this one contract: preview must
8
25
  * omit the same effects native PDF omits, while every other declaration is
@@ -4,7 +4,7 @@ import {
4
4
  build,
5
5
  connectChromium,
6
6
  getBrowser
7
- } from "./cli-eewynen9.js";
7
+ } from "./cli-c2yvktan.js";
8
8
  import"./cli-c41yr7he.js";
9
9
  import {
10
10
  BuildError
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  build
3
- } from "./index-p0ebr7dh.js";
3
+ } from "./index-my3v298z.js";
4
4
  import {
5
5
  BuildError,
6
6
  RENDER_TIMEOUT_MS,
7
7
  assertMilestone,
8
8
  connectChromium,
9
9
  getBrowser
10
- } from "./index-7hrvksrb.js";
10
+ } from "./index-wbm6p5j3.js";
11
11
  import"./index-37x76zdn.js";
12
12
 
13
13
  // src/lib/engine.ts
@@ -2012,7 +2012,18 @@
2012
2012
  "rotate",
2013
2013
  "translate",
2014
2014
  "scale",
2015
- "box-shadow"
2015
+ "perspective",
2016
+ "box-shadow",
2017
+ "opacity",
2018
+ "outline",
2019
+ "outline-color",
2020
+ "outline-style",
2021
+ "outline-width",
2022
+ "outline-offset",
2023
+ "filter",
2024
+ "backdrop-filter",
2025
+ "mix-blend-mode",
2026
+ "clip-path"
2016
2027
  ]);
2017
2028
  function isIgnoredMarginBoxProperty(property) {
2018
2029
  return MARGIN_BOX_IGNORED_PROPERTIES.has(property.toLowerCase());