gutterpress 0.10.1-beta.4 → 0.10.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.js +2 -2
- package/dist/{audit-dsj8dhzy.js → audit-e3xvt6vj.js} +4 -4
- package/dist/{build-cf6wwdyz.js → build-epz7zqcc.js} +4 -4
- package/dist/{cli-eewynen9.js → cli-6qhw9v08.js} +85 -4
- package/dist/{cli-5cwyth9m.js → cli-mnekrb8s.js} +1 -1
- package/dist/{cli-r3ec039v.js → cli-nrv3gzz9.js} +1 -1
- package/dist/{cli-nf1y5dc6.js → cli-wrt39m23.js} +34 -17
- package/dist/cli.js +14 -14
- package/dist/{doctor-vq0fcp4g.js → doctor-236cwssq.js} +2 -2
- package/dist/engine/compiler/build.d.ts +1 -1
- package/dist/engine/shared/margin-box-support.d.ts +18 -1
- package/dist/{engine-57vw8zpd.js → engine-9m8k1mfz.js} +1 -1
- package/dist/{engine-8kdwy0jz.js → engine-e1ycm5p9.js} +2 -2
- package/dist/{gutterpress-viewer-tma1qwz2.js → gutterpress-viewer-mfeccbys.js} +12 -1
- package/dist/{index-8v7d5ck1.js → index-ahzftp35.js} +33 -16
- package/dist/{index-p0ebr7dh.js → index-n252zv7d.js} +75 -4
- package/dist/{index-7hrvksrb.js → index-y7jbczsb.js} +13 -2
- package/dist/index.js +3 -3
- package/dist/lib/asset-inline.d.ts +7 -5
- package/dist/lib/markdown/assemble.d.ts +37 -0
- package/dist/lib/static-serve.d.ts +1 -2
- package/dist/{lint-apwxtwqx.js → lint-e36t5jz3.js} +4 -4
- package/dist/{new-vnhyhwx5.js → new-8jhvnqzp.js} +4 -4
- package/dist/{plugin-1j6p6az1.js → plugin-2390rtn1.js} +4 -4
- package/dist/{preflight-z85pkncr.js → preflight-bvtf28e4.js} +4 -4
- package/dist/preview/server-context.d.ts +6 -6
- package/dist/{preview-bp4a7403.js → preview-37g504pr.js} +4 -4
- package/dist/{publish-n5trd3zc.js → publish-v1gc8zm4.js} +4 -4
- package/dist/render.js +3 -1
- package/dist/test-helpers/testkit.d.ts +31 -0
- package/dist/{validate-657saw9j.js → validate-5yzb2qzq.js} +4 -4
- 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-
|
|
98
|
+
} from "../index-ahzftp35.js";
|
|
99
99
|
import {
|
|
100
100
|
BuildError,
|
|
101
101
|
DEFAULT_PRINT_OPTS,
|
|
102
102
|
readyProbeExpr
|
|
103
|
-
} from "../index-
|
|
103
|
+
} from "../index-y7jbczsb.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-
|
|
3
|
+
} from "./cli-wrt39m23.js";
|
|
4
4
|
import {
|
|
5
5
|
log
|
|
6
|
-
} from "./cli-
|
|
6
|
+
} from "./cli-mnekrb8s.js";
|
|
7
7
|
import {
|
|
8
8
|
UsageError,
|
|
9
9
|
rejectExtraPositionals,
|
|
10
10
|
rejectUnknownFlags
|
|
11
|
-
} from "./cli-
|
|
12
|
-
import"./cli-
|
|
11
|
+
} from "./cli-nrv3gzz9.js";
|
|
12
|
+
import"./cli-6qhw9v08.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-
|
|
4
|
+
} from "./cli-wrt39m23.js";
|
|
5
5
|
import {
|
|
6
6
|
log
|
|
7
|
-
} from "./cli-
|
|
7
|
+
} from "./cli-mnekrb8s.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-
|
|
16
|
-
import"./cli-
|
|
15
|
+
} from "./cli-nrv3gzz9.js";
|
|
16
|
+
import"./cli-6qhw9v08.js";
|
|
17
17
|
import"./cli-c41yr7he.js";
|
|
18
18
|
import {
|
|
19
19
|
BuildError
|
|
@@ -936,7 +936,18 @@ var MARGIN_BOX_IGNORED_PROPERTIES = new Set([
|
|
|
936
936
|
"rotate",
|
|
937
937
|
"translate",
|
|
938
938
|
"scale",
|
|
939
|
-
"
|
|
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());
|
|
@@ -1434,7 +1445,7 @@ var preview_shell_default = "./preview-shell-kyjq59d9.js";
|
|
|
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-
|
|
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.
|
|
7
|
+
version: "0.10.2-alpha.1",
|
|
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",
|
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
stampCreator,
|
|
14
14
|
stripAnnotations,
|
|
15
15
|
warn
|
|
16
|
-
} from "./cli-
|
|
16
|
+
} from "./cli-mnekrb8s.js";
|
|
17
17
|
import {
|
|
18
18
|
DEBOUNCE,
|
|
19
19
|
UsageError,
|
|
20
20
|
resolvePort
|
|
21
|
-
} from "./cli-
|
|
21
|
+
} from "./cli-nrv3gzz9.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-
|
|
30
|
+
} from "./cli-6qhw9v08.js";
|
|
31
31
|
import {
|
|
32
32
|
INSTALL_HINTS,
|
|
33
33
|
execCapture,
|
|
@@ -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
|
|
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 (
|
|
1262
|
+
} else if (isDroppedPageGradient(decl)) {
|
|
1244
1263
|
warnings.push({
|
|
1245
1264
|
rule: ruleRiskyProps,
|
|
1246
1265
|
severity: "warning",
|
|
1247
|
-
message: `
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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-
|
|
7541
|
+
const { buildNativePdf } = await import("./engine-9m8k1mfz.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,
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
UsageError,
|
|
7
7
|
package_default,
|
|
8
8
|
rejectUnknownFlags
|
|
9
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-nrv3gzz9.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-
|
|
22
|
-
preview: () => import("./preview-
|
|
23
|
-
build: () => import("./build-
|
|
24
|
-
publish: () => import("./publish-
|
|
25
|
-
lint: () => import("./lint-
|
|
26
|
-
validate: () => import("./validate-
|
|
27
|
-
audit: () => import("./audit-
|
|
28
|
-
preflight: () => import("./preflight-
|
|
29
|
-
doctor: () => import("./doctor-
|
|
30
|
-
plugin: () => import("./plugin-
|
|
21
|
+
new: () => import("./new-8jhvnqzp.js").then((m) => m.default),
|
|
22
|
+
preview: () => import("./preview-37g504pr.js").then((m) => m.default),
|
|
23
|
+
build: () => import("./build-epz7zqcc.js").then((m) => m.default),
|
|
24
|
+
publish: () => import("./publish-v1gc8zm4.js").then((m) => m.default),
|
|
25
|
+
lint: () => import("./lint-e36t5jz3.js").then((m) => m.default),
|
|
26
|
+
validate: () => import("./validate-5yzb2qzq.js").then((m) => m.default),
|
|
27
|
+
audit: () => import("./audit-e3xvt6vj.js").then((m) => m.default),
|
|
28
|
+
preflight: () => import("./preflight-bvtf28e4.js").then((m) => m.default),
|
|
29
|
+
doctor: () => import("./doctor-236cwssq.js").then((m) => m.default),
|
|
30
|
+
plugin: () => import("./plugin-2390rtn1.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-
|
|
55
|
+
const { newArgs } = await import("./new-8jhvnqzp.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-
|
|
67
|
+
const { preflightArgs } = await import("./preflight-bvtf28e4.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-
|
|
91
|
+
const { pluginAddArgs } = await import("./plugin-2390rtn1.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-
|
|
4
|
+
} from "./cli-mnekrb8s.js";
|
|
5
5
|
import {
|
|
6
6
|
UsageError,
|
|
7
7
|
rejectExtraPositionals,
|
|
8
8
|
rejectUnknownFlags
|
|
9
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-nrv3gzz9.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-history/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
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
build
|
|
3
|
-
} from "./index-
|
|
3
|
+
} from "./index-n252zv7d.js";
|
|
4
4
|
import {
|
|
5
5
|
BuildError,
|
|
6
6
|
RENDER_TIMEOUT_MS,
|
|
7
7
|
assertMilestone,
|
|
8
8
|
connectChromium,
|
|
9
9
|
getBrowser
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-y7jbczsb.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
|
-
"
|
|
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());
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
resolveChromiumExecutable,
|
|
18
18
|
run,
|
|
19
19
|
spawnCapture
|
|
20
|
-
} from "./index-
|
|
20
|
+
} from "./index-y7jbczsb.js";
|
|
21
21
|
import {
|
|
22
22
|
gitFs,
|
|
23
23
|
gitScopeFor,
|
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
3981
|
+
version: "0.10.2-alpha.1",
|
|
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
|
|
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 (
|
|
4436
|
+
} else if (isDroppedPageGradient(decl)) {
|
|
4420
4437
|
warnings.push({
|
|
4421
4438
|
rule: ruleRiskyProps,
|
|
4422
4439
|
severity: "warning",
|
|
4423
|
-
message: `
|
|
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-
|
|
8036
|
+
const { buildNativePdf } = await import("./engine-e1ycm5p9.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-
|
|
7
|
+
} from "./index-y7jbczsb.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);
|
|
@@ -608,7 +608,7 @@ var preview_shell_default = "./preview-shell-kyjq59d9.js";
|
|
|
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-
|
|
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
|
-
"
|
|
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-
|
|
116
|
+
} from "./index-ahzftp35.js";
|
|
117
117
|
import {
|
|
118
118
|
BUILD_DIAGNOSTIC_CODES
|
|
119
|
-
} from "./index-
|
|
119
|
+
} from "./index-n252zv7d.js";
|
|
120
120
|
import {
|
|
121
121
|
BuildError,
|
|
122
122
|
DEFAULT_PRINT_OPTS,
|
|
123
123
|
readyProbeExpr
|
|
124
|
-
} from "./index-
|
|
124
|
+
} from "./index-y7jbczsb.js";
|
|
125
125
|
import {
|
|
126
126
|
AUTO_SNAPSHOT_MESSAGE,
|
|
127
127
|
HISTORY_PAGE_LIMIT,
|
|
@@ -6,7 +6,8 @@
|
|
|
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.
|
|
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
13
|
* a 404 during pagination that Paged.js parsed as CSS or silently replaced with
|
|
@@ -14,8 +15,6 @@
|
|
|
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.
|
|
96
|
-
* their authored relative path, so the author's folder layout is what
|
|
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,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;
|
|
@@ -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
|
|
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-
|
|
4
|
+
} from "./cli-wrt39m23.js";
|
|
5
5
|
import {
|
|
6
6
|
log
|
|
7
|
-
} from "./cli-
|
|
7
|
+
} from "./cli-mnekrb8s.js";
|
|
8
8
|
import {
|
|
9
9
|
UsageError,
|
|
10
10
|
rejectExtraPositionals,
|
|
11
11
|
rejectUnknownFlags
|
|
12
|
-
} from "./cli-
|
|
13
|
-
import"./cli-
|
|
12
|
+
} from "./cli-nrv3gzz9.js";
|
|
13
|
+
import"./cli-6qhw9v08.js";
|
|
14
14
|
import"./cli-c41yr7he.js";
|
|
15
15
|
import {
|
|
16
16
|
EXIT_CODES
|
|
@@ -5,16 +5,16 @@ import {
|
|
|
5
5
|
TARGETS,
|
|
6
6
|
TARGET_IDS,
|
|
7
7
|
scaffoldProject
|
|
8
|
-
} from "./cli-
|
|
8
|
+
} from "./cli-wrt39m23.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveGhostscript
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-mnekrb8s.js";
|
|
12
12
|
import {
|
|
13
13
|
UsageError,
|
|
14
14
|
rejectExtraPositionals,
|
|
15
15
|
rejectUnknownFlags
|
|
16
|
-
} from "./cli-
|
|
17
|
-
import"./cli-
|
|
16
|
+
} from "./cli-nrv3gzz9.js";
|
|
17
|
+
import"./cli-6qhw9v08.js";
|
|
18
18
|
import {
|
|
19
19
|
isToolAvailable
|
|
20
20
|
} from "./cli-c41yr7he.js";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
addNpmPlugin,
|
|
3
3
|
parseNpmPluginSpec
|
|
4
|
-
} from "./cli-
|
|
5
|
-
import"./cli-
|
|
4
|
+
} from "./cli-wrt39m23.js";
|
|
5
|
+
import"./cli-mnekrb8s.js";
|
|
6
6
|
import {
|
|
7
7
|
UsageError,
|
|
8
8
|
rejectExtraPositionals,
|
|
9
9
|
rejectUnknownFlags
|
|
10
|
-
} from "./cli-
|
|
11
|
-
import"./cli-
|
|
10
|
+
} from "./cli-nrv3gzz9.js";
|
|
11
|
+
import"./cli-6qhw9v08.js";
|
|
12
12
|
import"./cli-c41yr7he.js";
|
|
13
13
|
import {
|
|
14
14
|
EXIT_CODES
|
|
@@ -2,16 +2,16 @@ import {
|
|
|
2
2
|
executeValidation,
|
|
3
3
|
publishTargetFor,
|
|
4
4
|
reportMissingTools
|
|
5
|
-
} from "./cli-
|
|
5
|
+
} from "./cli-wrt39m23.js";
|
|
6
6
|
import {
|
|
7
7
|
log
|
|
8
|
-
} from "./cli-
|
|
8
|
+
} from "./cli-mnekrb8s.js";
|
|
9
9
|
import {
|
|
10
10
|
UsageError,
|
|
11
11
|
rejectExtraPositionals,
|
|
12
12
|
rejectUnknownFlags
|
|
13
|
-
} from "./cli-
|
|
14
|
-
import"./cli-
|
|
13
|
+
} from "./cli-nrv3gzz9.js";
|
|
14
|
+
import"./cli-6qhw9v08.js";
|
|
15
15
|
import"./cli-c41yr7he.js";
|
|
16
16
|
import {
|
|
17
17
|
EXIT_CODES
|
|
@@ -35,12 +35,12 @@ export interface ServerState {
|
|
|
35
35
|
* Files the inlined CSS references but could not embed, as
|
|
36
36
|
* `book.html`-relative URL path → absolute source path.
|
|
37
37
|
*
|
|
38
|
-
* `asset-inline.ts` embeds fonts
|
|
39
|
-
* `
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
38
|
+
* `asset-inline.ts` embeds fonts; every image becomes
|
|
39
|
+
* `assets/<contentHash><ext>`, wherever it lives — in the book, or in a
|
|
40
|
+
* repo-root shared stylesheet's art folder (the normative multi-book
|
|
41
|
+
* layout). The inliner returns a COPY PLAN, which the build executes into
|
|
42
|
+
* its output dir. Content-addressing is what keeps a CSS image's URL
|
|
43
|
+
* distinct from a prose image's; see `inlineOne`.
|
|
44
44
|
*
|
|
45
45
|
* The preview serves the project in place and has no output dir, so a
|
|
46
46
|
* rewritten `assets/<hash>` URL had nothing behind it and shared art
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
runBuild,
|
|
7
7
|
splitOutPath,
|
|
8
8
|
startPreviewServer
|
|
9
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-wrt39m23.js";
|
|
10
10
|
import {
|
|
11
11
|
log
|
|
12
|
-
} from "./cli-
|
|
12
|
+
} from "./cli-mnekrb8s.js";
|
|
13
13
|
import {
|
|
14
14
|
UsageError,
|
|
15
15
|
parseEngine,
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
rejectExtraPositionals,
|
|
19
19
|
rejectUnknownFlags,
|
|
20
20
|
resolvePort
|
|
21
|
-
} from "./cli-
|
|
22
|
-
import"./cli-
|
|
21
|
+
} from "./cli-nrv3gzz9.js";
|
|
22
|
+
import"./cli-6qhw9v08.js";
|
|
23
23
|
import"./cli-c41yr7he.js";
|
|
24
24
|
import {
|
|
25
25
|
BuildError
|
|
@@ -7,17 +7,17 @@ import {
|
|
|
7
7
|
publishCredentialKey,
|
|
8
8
|
publishProviderFor,
|
|
9
9
|
runPublish
|
|
10
|
-
} from "./cli-
|
|
10
|
+
} from "./cli-wrt39m23.js";
|
|
11
11
|
import {
|
|
12
12
|
FileTokenStore,
|
|
13
13
|
log
|
|
14
|
-
} from "./cli-
|
|
14
|
+
} from "./cli-mnekrb8s.js";
|
|
15
15
|
import {
|
|
16
16
|
UsageError,
|
|
17
17
|
rejectExtraPositionals,
|
|
18
18
|
rejectUnknownFlags
|
|
19
|
-
} from "./cli-
|
|
20
|
-
import"./cli-
|
|
19
|
+
} from "./cli-nrv3gzz9.js";
|
|
20
|
+
import"./cli-6qhw9v08.js";
|
|
21
21
|
import"./cli-c41yr7he.js";
|
|
22
22
|
import {
|
|
23
23
|
EXIT_CODES
|
package/dist/render.js
CHANGED
|
@@ -1389,12 +1389,14 @@ ${projectCss.trim()}` : null
|
|
|
1389
1389
|
].filter(Boolean).join(`
|
|
1390
1390
|
|
|
1391
1391
|
`);
|
|
1392
|
+
const preloadTags = (opts.preloadImages ?? []).map((href) => `
|
|
1393
|
+
<link rel="preload" as="image" href="${href}">`).join("");
|
|
1392
1394
|
return `<!DOCTYPE html>
|
|
1393
1395
|
<html lang="en">
|
|
1394
1396
|
<head>
|
|
1395
1397
|
<meta charset="UTF-8">
|
|
1396
1398
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1397
|
-
<title>${title}</title
|
|
1399
|
+
<title>${title}</title>${preloadTags}
|
|
1398
1400
|
<style data-project-css>
|
|
1399
1401
|
${inlineCss}
|
|
1400
1402
|
</style>
|
|
@@ -89,3 +89,34 @@ export declare class ProcessExitSignal extends Error {
|
|
|
89
89
|
* `build-runner.browser-lifecycle.test.ts`.
|
|
90
90
|
*/
|
|
91
91
|
export declare function stubProcessExit(): ReturnType<typeof spyOn<typeof process, "exit">>;
|
|
92
|
+
/**
|
|
93
|
+
* "Did this declaration paint anything?" is not answerable from a PDF's
|
|
94
|
+
* structure — Chromium fetches an unreferenced `@page` background image and
|
|
95
|
+
* then paints nothing, with no error and no missing object
|
|
96
|
+
* (docs/known-limitations.md §3, #152). The only honest answer is pixels, so
|
|
97
|
+
* these helpers rasterize page 1 and diff it against a control.
|
|
98
|
+
*
|
|
99
|
+
* Ghostscript rather than poppler's `pdftoppm`: `gs` is what CI installs, and
|
|
100
|
+
* one rasterizer keeps the two page-background suites measuring the same way.
|
|
101
|
+
*/
|
|
102
|
+
export interface GrayRaster {
|
|
103
|
+
width: number;
|
|
104
|
+
height: number;
|
|
105
|
+
data: Uint8Array;
|
|
106
|
+
}
|
|
107
|
+
/** Ghostscript's path, or `null`. Callers self-skip, as the PDF/X suites do. */
|
|
108
|
+
export declare function resolveRasterizer(): Promise<string | null>;
|
|
109
|
+
/** Page 1 of `pdfPath` as an 8-bit grayscale raster at 100 dpi. */
|
|
110
|
+
export declare function rasterizePdfPage(gsBin: string, pdfPath: string, outDir: string, tag: string): GrayRaster;
|
|
111
|
+
/**
|
|
112
|
+
* Mean absolute per-pixel difference. `0` means the two prints are identical —
|
|
113
|
+
* i.e. the declaration under test changed nothing at all.
|
|
114
|
+
*/
|
|
115
|
+
export declare function meanAbsDiff(a: GrayRaster, b: GrayRaster): number;
|
|
116
|
+
/**
|
|
117
|
+
* A minimal RGB PNG encoder, so page-background fixtures can generate the
|
|
118
|
+
* exact image they need — a solid tile that is unmistakable when it paints, or
|
|
119
|
+
* an incompressible one that is genuinely large — instead of carrying opaque
|
|
120
|
+
* base64 blobs whose size and contrast nobody can check by reading them.
|
|
121
|
+
*/
|
|
122
|
+
export declare function pngRgb(width: number, height: number, pixel: (x: number, y: number) => [number, number, number]): Buffer;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
executeAndReport
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-wrt39m23.js";
|
|
4
4
|
import {
|
|
5
5
|
log
|
|
6
|
-
} from "./cli-
|
|
6
|
+
} from "./cli-mnekrb8s.js";
|
|
7
7
|
import {
|
|
8
8
|
UsageError,
|
|
9
9
|
rejectExtraPositionals,
|
|
10
10
|
rejectUnknownFlags
|
|
11
|
-
} from "./cli-
|
|
12
|
-
import"./cli-
|
|
11
|
+
} from "./cli-nrv3gzz9.js";
|
|
12
|
+
import"./cli-6qhw9v08.js";
|
|
13
13
|
import"./cli-c41yr7he.js";
|
|
14
14
|
import {
|
|
15
15
|
EXIT_CODES
|
package/package.json
CHANGED