gutterpress 0.9.0-alpha.2 → 0.10.0-alpha.4
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/README.md +18 -4
- package/dist/api/index.d.ts +11 -5
- package/dist/api/index.js +20 -15
- package/dist/assets/preview/scripts/preview-bridge.d.ts +1 -0
- package/dist/assets/preview/scripts/preview-interface.d.ts +1 -0
- package/dist/{audit-nhn2pjz3.js → audit-k1vnfwvc.js} +10 -7
- package/dist/{build-san7fv2z.js → build-hqmwgvdw.js} +14 -8
- package/dist/checks/source/index.d.ts +1 -0
- package/dist/checks/source/layout-markers.d.ts +3 -0
- package/dist/checks/source/local-ref-parser.d.ts +28 -0
- package/dist/{cli-e5zhb0xs.js → cli-0r0tq16s.js} +17 -24
- package/dist/cli-46ycxe6r.js +18 -0
- package/dist/cli-c41yr7he.js +241 -0
- package/dist/cli-hp9r2pzt.js +2343 -0
- package/dist/{index-yzrh708h.js → cli-k1065rkg.js} +2 -5
- package/dist/{cli-k4bd06sd.js → cli-n25qycwz.js} +4986 -5889
- package/dist/{cli-najycadg.js → cli-ra0ed2xt.js} +69 -253
- package/dist/{cli-506tg37g.js → cli-revgt4pr.js} +2 -1
- package/dist/{cli-wchtvxvw.js → cli-v5mp7a6q.js} +14 -1
- package/dist/cli.js +20 -18
- package/dist/{doctor-hrk0kxxz.js → doctor-dxms7ehm.js} +4 -2
- package/dist/engine/compiler/build.d.ts +139 -0
- package/dist/engine/compiler/postprocess.d.ts +23 -0
- package/dist/engine/compiler/tier2.d.ts +78 -0
- package/dist/engine/shared/cdp.d.ts +104 -0
- package/dist/engine/shared/content-value.d.ts +64 -0
- package/dist/engine/shared/gcpm-extract.d.ts +119 -0
- package/dist/engine/shared/margin-box-support.d.ts +12 -0
- package/dist/engine/shared/pdf-inspect.d.ts +24 -0
- package/dist/engine/shared/synthesis.d.ts +155 -0
- package/dist/engine-wa7y9av9.js +41 -0
- package/dist/engine-z4p9sr4h.js +40 -0
- package/dist/gutterpress-agent-1ctgfz92.js +576 -0
- package/dist/gutterpress-viewer-cem7dmr5.js +2349 -0
- package/dist/{index-bynn850m.js → index-05y3dnxq.js} +3049 -4015
- package/dist/index-9tyq9kks.js +708 -0
- package/dist/{cli-yzrh708h.js → index-mdefp0y5.js} +1 -1
- package/dist/{index-wchtvxvw.js → index-v5mp7a6q.js} +14 -1
- package/dist/index-xxg4zfrg.js +1907 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +24 -17
- package/dist/lib/asset-inline.d.ts +36 -0
- package/dist/lib/browser-pool.d.ts +18 -0
- package/dist/lib/build-error.d.ts +1 -1
- package/dist/lib/build-preflight.d.ts +35 -2
- package/dist/lib/build-runner.d.ts +45 -17
- package/dist/lib/build-staging.d.ts +9 -55
- package/dist/lib/cli-args.d.ts +8 -0
- package/dist/lib/desktop.d.ts +2 -2
- package/dist/lib/embedded-assets.d.ts +1 -1
- package/dist/lib/engine.d.ts +33 -0
- package/dist/lib/ghostscript.d.ts +46 -1
- package/dist/lib/markdown/assemble.d.ts +18 -10
- package/dist/lib/markdown/gp-pin-scope.d.ts +1 -0
- package/dist/lib/markdown/gutterpress-css.d.ts +125 -0
- package/dist/lib/markdown/images.d.ts +26 -0
- package/dist/lib/markdown/index.d.ts +4 -2
- package/dist/lib/markdown/inline-source.d.ts +10 -0
- package/dist/lib/markdown/markers.d.ts +32 -0
- package/dist/lib/markdown/renderer.d.ts +8 -3
- package/dist/lib/markdown/source-range.d.ts +61 -0
- package/dist/lib/missing-asset-placeholder.d.ts +52 -0
- package/dist/lib/presets.d.ts +1 -1
- package/dist/lib/printsafe.d.ts +2 -3
- package/dist/lib/remote-auth/converge-merge.d.ts +48 -0
- package/dist/lib/remote-auth/image-clash.d.ts +17 -0
- package/dist/lib/remote-auth/recovery/classify.d.ts +55 -67
- package/dist/lib/remote-auth/recovery/inspect.d.ts +14 -13
- package/dist/lib/remote-auth/recovery/locks.d.ts +20 -0
- package/dist/lib/remote-auth/recovery/repair.d.ts +29 -0
- package/dist/lib/remote-auth/recovery/types.d.ts +8 -204
- package/dist/lib/remote-auth/sync-messages.d.ts +2 -3
- package/dist/lib/remote-auth/sync-types.d.ts +35 -63
- package/dist/lib/remote-auth/sync.d.ts +11 -18
- package/dist/lib/remote-auth/transport.d.ts +14 -7
- package/dist/lib/theme-import.d.ts +4 -5
- package/dist/{lint-96j9hrj4.js → lint-xjwm5ep8.js} +10 -7
- package/dist/{manifest.schema-z61rzw44.json → manifest.schema-zxgxnbg7.json} +21 -0
- package/dist/{new-8p38wavc.js → new-kwdwpf0j.js} +12 -8
- package/dist/{plugin-ees6nhkc.js → plugin-rg4tnn96.js} +10 -7
- package/dist/{preflight-1q6c2edh.js → preflight-3127y25z.js} +10 -7
- package/dist/preview/file-watcher.d.ts +13 -17
- package/dist/preview/lifecycle.d.ts +1 -1
- package/dist/{pagedjs-bridge-vn4hk9fx.js → preview-bridge-fz7vpk8m.js} +8 -0
- package/dist/preview-interface-435cczt5.js +1059 -0
- package/dist/{preview-y5a2zen1.js → preview-ncgfhqmw.js} +16 -9
- package/dist/preview-shell-c5mfa3q0.js +346 -0
- package/dist/{project-source-p0gn1wd5.js → project-source-ekcyp63q.js} +1 -1
- package/dist/{publish-rm9yb3wh.js → publish-pr0rwh6p.js} +10 -7
- package/dist/render.d.ts +3 -4
- package/dist/render.js +728 -64
- package/dist/{repair-zgq7q2g6.js → repair-8270smfw.js} +45 -79
- package/dist/schema/manifest.types.d.ts +38 -0
- package/dist/{source-provider-c1rjm2c0.js → source-provider-3tcj6qg2.js} +2 -2
- package/dist/source-provider-vanafrt9.js +40 -0
- package/dist/{theme-zz2ktzqs.css → theme-h5recz6c.css} +8 -7
- package/dist/{theme-570zmh2t.css → theme-j2bagrfx.css} +8 -7
- package/dist/{theme-nya4nqh6.css → theme-nn6d53zy.css} +8 -7
- package/dist/types.d.ts +7 -0
- package/dist/{validate-nr0xa6sa.js → validate-54e17rae.js} +10 -7
- package/package.json +6 -6
- package/dist/cli-yja077f6.js +0 -92
- package/dist/git-http-yrb4ag6z.js +0 -17
- package/dist/index-yja077f6.js +0 -92
- package/dist/lib/markdown/markdown-it-paged.d.ts +0 -30
- package/dist/lib/pagedjs-marker.d.ts +0 -42
- package/dist/lib/pagedjs.d.ts +0 -26
- package/dist/lib/pagination.d.ts +0 -149
- package/dist/lib/remote-auth/conflict-resolution.d.ts +0 -29
- package/dist/lib/remote-auth/recovery/abort-interrupted-operation.d.ts +0 -103
- package/dist/lib/remote-auth/recovery/backup.d.ts +0 -113
- package/dist/lib/remote-auth/recovery/context.d.ts +0 -47
- package/dist/lib/remote-auth/recovery/dispatch.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/failsafe.d.ts +0 -33
- package/dist/lib/remote-auth/recovery/manual-guidance.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/outcome-mapping.d.ts +0 -59
- package/dist/lib/remote-auth/recovery/policy.d.ts +0 -47
- package/dist/lib/remote-auth/recovery/recover-auth.d.ts +0 -42
- package/dist/lib/remote-auth/recovery/recover-binary-conflict.d.ts +0 -37
- package/dist/lib/remote-auth/recovery/recover-corrupt-index.d.ts +0 -40
- package/dist/lib/remote-auth/recovery/recover-detached-head.d.ts +0 -70
- package/dist/lib/remote-auth/recovery/recover-interrupted-cherry-pick.d.ts +0 -23
- package/dist/lib/remote-auth/recovery/recover-interrupted-merge.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/recover-interrupted-rebase.d.ts +0 -39
- package/dist/lib/remote-auth/recovery/recover-merge-conflict.d.ts +0 -34
- package/dist/lib/remote-auth/recovery/recover-missing-git-dir.d.ts +0 -37
- package/dist/lib/remote-auth/recovery/recover-missing-objects.d.ts +0 -56
- package/dist/lib/remote-auth/recovery/recover-network.d.ts +0 -34
- package/dist/lib/remote-auth/recovery/recover-non-fast-forward.d.ts +0 -27
- package/dist/lib/remote-auth/recovery/recover-stale-lock.d.ts +0 -68
- package/dist/lib/remote-auth/recovery/recover-unrelated-histories.d.ts +0 -44
- package/dist/lib/remote-auth/recovery/recover-wrong-remote.d.ts +0 -35
- package/dist/lib/remote-auth/resolution-plan.d.ts +0 -64
- package/dist/paged.polyfill-n95pbxfn.js +0 -33288
- package/dist/pagedjs-interface-qxvzgwd7.js +0 -557
- package/dist/preview-shell-6dqexx1m.js +0 -581
- /package/dist/assets/{preview/scripts/pagedjs-bridge.d.ts → engine/gutterpress-agent.d.ts} +0 -0
- /package/dist/assets/{preview/scripts/pagedjs-interface.d.ts → engine/gutterpress-viewer.d.ts} +0 -0
|
@@ -0,0 +1,2343 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requireChromiumExecutable
|
|
3
|
+
} from "./cli-c41yr7he.js";
|
|
4
|
+
import {
|
|
5
|
+
__require
|
|
6
|
+
} from "./cli-37x76zdn.js";
|
|
7
|
+
|
|
8
|
+
// src/lib/browser-pool.ts
|
|
9
|
+
var RENDER_TIMEOUT_MS = 60 * 60 * 1000;
|
|
10
|
+
var browserPromise = null;
|
|
11
|
+
async function launchBrowser(timeoutMs) {
|
|
12
|
+
const executablePath = await requireChromiumExecutable();
|
|
13
|
+
const puppeteer = (await import("puppeteer-core")).default;
|
|
14
|
+
const extraChromiumArgs = (process.env.GUTTERPRESS_CHROMIUM_ARGS ?? "").split(/\s+/).filter(Boolean);
|
|
15
|
+
return puppeteer.launch({
|
|
16
|
+
headless: true,
|
|
17
|
+
executablePath,
|
|
18
|
+
args: extraChromiumArgs,
|
|
19
|
+
protocolTimeout: timeoutMs
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function prewarmBrowser(timeoutMs) {
|
|
23
|
+
if (browserPromise)
|
|
24
|
+
return;
|
|
25
|
+
const p = launchBrowser(timeoutMs);
|
|
26
|
+
browserPromise = p;
|
|
27
|
+
p.then((browser) => {
|
|
28
|
+
browser.on("disconnected", () => {
|
|
29
|
+
if (browserPromise === p)
|
|
30
|
+
browserPromise = null;
|
|
31
|
+
});
|
|
32
|
+
}).catch(() => {
|
|
33
|
+
if (browserPromise === p)
|
|
34
|
+
browserPromise = null;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function getBrowser(timeoutMs) {
|
|
38
|
+
if (!browserPromise)
|
|
39
|
+
prewarmBrowser(timeoutMs);
|
|
40
|
+
return browserPromise;
|
|
41
|
+
}
|
|
42
|
+
async function closeBrowser() {
|
|
43
|
+
const p = browserPromise;
|
|
44
|
+
browserPromise = null;
|
|
45
|
+
if (!p)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
const b = await p;
|
|
49
|
+
await b.close();
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/engine/shared/cdp.ts
|
|
54
|
+
import { spawn } from "node:child_process";
|
|
55
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
56
|
+
import { tmpdir } from "node:os";
|
|
57
|
+
import { join } from "node:path";
|
|
58
|
+
import WebSocket from "ws";
|
|
59
|
+
var CANDIDATES = [
|
|
60
|
+
process.env.GUTTERPRESS_CHROMIUM,
|
|
61
|
+
process.env.PUPPETEER_EXECUTABLE_PATH,
|
|
62
|
+
"/opt/pw-browsers/chromium",
|
|
63
|
+
"/usr/bin/chromium",
|
|
64
|
+
"/usr/bin/chromium-browser",
|
|
65
|
+
"/usr/bin/google-chrome",
|
|
66
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
67
|
+
].filter(Boolean);
|
|
68
|
+
function findChromium() {
|
|
69
|
+
for (const c of CANDIDATES)
|
|
70
|
+
if (existsSync(c))
|
|
71
|
+
return c;
|
|
72
|
+
throw new Error(`No Chromium found. Set GUTTERPRESS_CHROMIUM. Looked in:
|
|
73
|
+
${CANDIDATES.join(`
|
|
74
|
+
`)}`);
|
|
75
|
+
}
|
|
76
|
+
var REQUIRED_MILESTONE = 148;
|
|
77
|
+
function assertMilestone(product, origin, hint = "") {
|
|
78
|
+
const milestone = Number(/Chrome\/(\d+)/.exec(product)?.[1] ?? 0);
|
|
79
|
+
if (milestone < REQUIRED_MILESTONE) {
|
|
80
|
+
throw new Error(`The Gutterpress engine requires Chromium ${REQUIRED_MILESTONE}+; found ${product} ${origin}.` + (hint ? `
|
|
81
|
+
${hint}` : ""));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readyProbeExpr(timeoutMs) {
|
|
85
|
+
return `(async () => {
|
|
86
|
+
await document.fonts.ready;
|
|
87
|
+
if (window.__gpReadyPending) {
|
|
88
|
+
await new Promise((res) => {
|
|
89
|
+
const t = setTimeout(res, ${timeoutMs});
|
|
90
|
+
document.addEventListener('gp:ready', () => { clearTimeout(t); res(); }, { once: true });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// two rAFs: let layout settle after fonts swap
|
|
94
|
+
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
95
|
+
return true;
|
|
96
|
+
})()`;
|
|
97
|
+
}
|
|
98
|
+
var DEFAULT_PRINT_OPTS = {
|
|
99
|
+
printBackground: true,
|
|
100
|
+
preferCSSPageSize: true,
|
|
101
|
+
generateTaggedPDF: true,
|
|
102
|
+
generateDocumentOutline: true
|
|
103
|
+
};
|
|
104
|
+
async function launchChromium(opts = {}) {
|
|
105
|
+
const bin = findChromium();
|
|
106
|
+
const userDataDir = mkdtempSync(join(tmpdir(), "gp-cdp-"));
|
|
107
|
+
const args = [
|
|
108
|
+
"--remote-debugging-port=0",
|
|
109
|
+
`--user-data-dir=${userDataDir}`,
|
|
110
|
+
"--no-first-run",
|
|
111
|
+
"--no-default-browser-check",
|
|
112
|
+
"--disable-gpu",
|
|
113
|
+
"--no-sandbox",
|
|
114
|
+
"--hide-scrollbars",
|
|
115
|
+
"--allow-file-access-from-files",
|
|
116
|
+
"--font-render-hinting=none",
|
|
117
|
+
...opts.headless === false ? [] : ["--headless=new"],
|
|
118
|
+
...opts.args ?? [],
|
|
119
|
+
"about:blank"
|
|
120
|
+
];
|
|
121
|
+
const proc = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
122
|
+
const wsUrl = await readDevToolsUrl(proc);
|
|
123
|
+
const conn = await Connection.open(wsUrl);
|
|
124
|
+
const teardown = async () => {
|
|
125
|
+
const exited = new Promise((r) => proc.once("exit", () => r()));
|
|
126
|
+
try {
|
|
127
|
+
await Promise.race([
|
|
128
|
+
conn.send("Browser.close").then(() => exited),
|
|
129
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("Browser.close timed out")), 5000))
|
|
130
|
+
]);
|
|
131
|
+
} catch {
|
|
132
|
+
proc.kill("SIGKILL");
|
|
133
|
+
await exited;
|
|
134
|
+
}
|
|
135
|
+
conn.close();
|
|
136
|
+
rmSync(userDataDir, { recursive: true, force: true });
|
|
137
|
+
};
|
|
138
|
+
return checkMilestoneAndWrap(conn, wsUrl, `at ${bin}`, teardown);
|
|
139
|
+
}
|
|
140
|
+
async function connectChromium(wsUrl) {
|
|
141
|
+
const conn = await Connection.open(wsUrl);
|
|
142
|
+
return checkMilestoneAndWrap(conn, wsUrl, "via connected browser", async () => {
|
|
143
|
+
conn.close();
|
|
144
|
+
}, `Set CHROMIUM_PATH (or PUPPETEER_EXECUTABLE_PATH) to a ${REQUIRED_MILESTONE}+ binary.`);
|
|
145
|
+
}
|
|
146
|
+
async function checkMilestoneAndWrap(conn, wsUrl, origin, teardown, overrideHint = `Set GUTTERPRESS_CHROMIUM to a ${REQUIRED_MILESTONE}+ binary.`) {
|
|
147
|
+
const version = await conn.send("Browser.getVersion", {});
|
|
148
|
+
try {
|
|
149
|
+
assertMilestone(version.product, origin, overrideHint);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
await teardown();
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
const milestone = Number(/Chrome\/(\d+)/.exec(version.product)?.[1] ?? 0);
|
|
155
|
+
return {
|
|
156
|
+
wsUrl,
|
|
157
|
+
version: version.product,
|
|
158
|
+
milestone,
|
|
159
|
+
async newPage() {
|
|
160
|
+
const { targetId } = await conn.send("Target.createTarget", { url: "about:blank" });
|
|
161
|
+
const { sessionId } = await conn.send("Target.attachToTarget", { targetId, flatten: true });
|
|
162
|
+
const s = new SessionImpl(conn, sessionId, targetId);
|
|
163
|
+
await s.send("Page.enable");
|
|
164
|
+
await s.send("Runtime.enable");
|
|
165
|
+
return s;
|
|
166
|
+
},
|
|
167
|
+
close: teardown
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function readDevToolsUrl(proc) {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
let buf = "";
|
|
173
|
+
const timer = setTimeout(() => reject(new Error(`Chromium did not report a DevTools URL:
|
|
174
|
+
${buf}`)), 20000);
|
|
175
|
+
proc.stderr.on("data", (d) => {
|
|
176
|
+
buf += String(d);
|
|
177
|
+
const m = /ws:\/\/[^\s]+/.exec(buf);
|
|
178
|
+
if (m) {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
resolve(m[0]);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
proc.once("exit", (code) => {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
reject(new Error(`Chromium exited (${code}) before listening:
|
|
186
|
+
${buf}`));
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class Connection {
|
|
192
|
+
ws;
|
|
193
|
+
id = 0;
|
|
194
|
+
pending = new Map;
|
|
195
|
+
listeners = new Map;
|
|
196
|
+
constructor(ws) {
|
|
197
|
+
this.ws = ws;
|
|
198
|
+
ws.on("message", (raw) => this.dispatch(JSON.parse(String(raw))));
|
|
199
|
+
}
|
|
200
|
+
static async open(url) {
|
|
201
|
+
const ws = new WebSocket(url, { maxPayload: 512 * 1024 * 1024 });
|
|
202
|
+
await new Promise((res, rej) => {
|
|
203
|
+
ws.once("open", () => res());
|
|
204
|
+
ws.once("error", rej);
|
|
205
|
+
});
|
|
206
|
+
return new Connection(ws);
|
|
207
|
+
}
|
|
208
|
+
dispatch(msg) {
|
|
209
|
+
if (msg.id !== undefined) {
|
|
210
|
+
const p = this.pending.get(msg.id);
|
|
211
|
+
if (!p)
|
|
212
|
+
return;
|
|
213
|
+
this.pending.delete(msg.id);
|
|
214
|
+
if (msg.error)
|
|
215
|
+
p.reject(new Error(`${msg.error.message} (${msg.error.code})`));
|
|
216
|
+
else
|
|
217
|
+
p.resolve(msg.result);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const key = msg.sessionId ? `${msg.sessionId}:${msg.method}` : msg.method;
|
|
221
|
+
for (const fn of this.listeners.get(key) ?? [])
|
|
222
|
+
fn(msg.params);
|
|
223
|
+
for (const fn of this.listeners.get(msg.method) ?? [])
|
|
224
|
+
fn(msg.params);
|
|
225
|
+
}
|
|
226
|
+
send(method, params = {}, sessionId) {
|
|
227
|
+
const id = ++this.id;
|
|
228
|
+
const payload = { id, method, params };
|
|
229
|
+
if (sessionId)
|
|
230
|
+
payload.sessionId = sessionId;
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
this.pending.set(id, { resolve, reject });
|
|
233
|
+
this.ws.send(JSON.stringify(payload));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
on(key, fn) {
|
|
237
|
+
let set = this.listeners.get(key);
|
|
238
|
+
if (!set)
|
|
239
|
+
this.listeners.set(key, set = new Set);
|
|
240
|
+
set.add(fn);
|
|
241
|
+
return () => set.delete(fn);
|
|
242
|
+
}
|
|
243
|
+
close() {
|
|
244
|
+
this.ws.close();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
class SessionImpl {
|
|
249
|
+
conn;
|
|
250
|
+
sessionId;
|
|
251
|
+
targetId;
|
|
252
|
+
constructor(conn, sessionId, targetId) {
|
|
253
|
+
this.conn = conn;
|
|
254
|
+
this.sessionId = sessionId;
|
|
255
|
+
this.targetId = targetId;
|
|
256
|
+
}
|
|
257
|
+
send(method, params = {}) {
|
|
258
|
+
return this.conn.send(method, params, this.sessionId);
|
|
259
|
+
}
|
|
260
|
+
on(event, fn) {
|
|
261
|
+
return this.conn.on(`${this.sessionId}:${event}`, fn);
|
|
262
|
+
}
|
|
263
|
+
async evaluate(expression) {
|
|
264
|
+
const res = await this.send("Runtime.evaluate", {
|
|
265
|
+
expression,
|
|
266
|
+
returnByValue: true,
|
|
267
|
+
awaitPromise: true
|
|
268
|
+
});
|
|
269
|
+
if (res.exceptionDetails) {
|
|
270
|
+
const e = res.exceptionDetails;
|
|
271
|
+
throw new Error(`evaluate failed: ${e.exception?.description ?? e.text}`);
|
|
272
|
+
}
|
|
273
|
+
return res.result.value;
|
|
274
|
+
}
|
|
275
|
+
async waitForLoad(fn) {
|
|
276
|
+
const done = new Promise((resolve) => {
|
|
277
|
+
const off = this.on("Page.loadEventFired", () => {
|
|
278
|
+
off();
|
|
279
|
+
resolve();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
await fn();
|
|
283
|
+
await done;
|
|
284
|
+
}
|
|
285
|
+
async setContent(html, baseUrl = "http://gutterpress.spike/") {
|
|
286
|
+
await this.waitForLoad(async () => {
|
|
287
|
+
const { frameTree } = await this.send("Page.getFrameTree");
|
|
288
|
+
await this.send("Page.setDocumentContent", {
|
|
289
|
+
frameId: frameTree.frame.id,
|
|
290
|
+
html
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async navigate(url) {
|
|
295
|
+
await this.waitForLoad(async () => {
|
|
296
|
+
await this.send("Page.navigate", { url });
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async waitForReady(timeoutMs = 15000) {
|
|
300
|
+
await this.evaluate(readyProbeExpr(timeoutMs));
|
|
301
|
+
}
|
|
302
|
+
async printToPDF(opts = {}) {
|
|
303
|
+
const res = await this.send("Page.printToPDF", {
|
|
304
|
+
...DEFAULT_PRINT_OPTS,
|
|
305
|
+
transferMode: "ReturnAsStream",
|
|
306
|
+
...opts
|
|
307
|
+
});
|
|
308
|
+
if (!res.stream)
|
|
309
|
+
return Uint8Array.from(Buffer.from(res.data, "base64"));
|
|
310
|
+
const chunks = [];
|
|
311
|
+
try {
|
|
312
|
+
for (;; ) {
|
|
313
|
+
const c = await this.send("IO.read", { handle: res.stream, size: 8 * 1024 * 1024 });
|
|
314
|
+
if (c.data)
|
|
315
|
+
chunks.push(Buffer.from(c.data, c.base64Encoded ? "base64" : "binary"));
|
|
316
|
+
if (c.eof)
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
} finally {
|
|
320
|
+
await this.send("IO.close", { handle: res.stream }).catch(() => {});
|
|
321
|
+
}
|
|
322
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
323
|
+
}
|
|
324
|
+
async close() {
|
|
325
|
+
await this.conn.send("Target.closeTarget", { targetId: this.targetId });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/engine/compiler/build.ts
|
|
330
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
331
|
+
import { resolve as resolve2 } from "node:path";
|
|
332
|
+
import { pathToFileURL } from "node:url";
|
|
333
|
+
|
|
334
|
+
// src/engine/shared/gcpm-extract.ts
|
|
335
|
+
var NESTED_AT_RULES = /^@(media|supports|layer|scope|container|document)\b/i;
|
|
336
|
+
function scanRules(css) {
|
|
337
|
+
const out = [];
|
|
338
|
+
let i = 0;
|
|
339
|
+
let start = 0;
|
|
340
|
+
let depth = 0;
|
|
341
|
+
let bodyStart = -1;
|
|
342
|
+
while (i < css.length) {
|
|
343
|
+
const c = css[i];
|
|
344
|
+
if (c === "/" && css[i + 1] === "*") {
|
|
345
|
+
const end = css.indexOf("*/", i + 2);
|
|
346
|
+
i = end === -1 ? css.length : end + 2;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (c === '"' || c === "'") {
|
|
350
|
+
i = skipString(css, i);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (c === "{") {
|
|
354
|
+
if (depth === 0)
|
|
355
|
+
bodyStart = i;
|
|
356
|
+
depth++;
|
|
357
|
+
i++;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (c === "}") {
|
|
361
|
+
depth--;
|
|
362
|
+
if (depth === 0) {
|
|
363
|
+
out.push({
|
|
364
|
+
prelude: stripComments(css.slice(start, bodyStart)).trim(),
|
|
365
|
+
body: css.slice(bodyStart + 1, i)
|
|
366
|
+
});
|
|
367
|
+
start = i + 1;
|
|
368
|
+
}
|
|
369
|
+
i++;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (c === ";" && depth === 0) {
|
|
373
|
+
const stmt = stripComments(css.slice(start, i)).trim();
|
|
374
|
+
if (stmt)
|
|
375
|
+
out.push({ statement: stmt });
|
|
376
|
+
start = i + 1;
|
|
377
|
+
i++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
i++;
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
function stripComments(s) {
|
|
385
|
+
let out = "";
|
|
386
|
+
let i = 0;
|
|
387
|
+
while (i < s.length) {
|
|
388
|
+
if (s[i] === "/" && s[i + 1] === "*") {
|
|
389
|
+
const end = s.indexOf("*/", i + 2);
|
|
390
|
+
i = end === -1 ? s.length : end + 2;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (s[i] === '"' || s[i] === "'") {
|
|
394
|
+
const end = skipString(s, i);
|
|
395
|
+
out += s.slice(i, end);
|
|
396
|
+
i = end;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
out += s[i++];
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
function skipString(css, i) {
|
|
404
|
+
const quote = css[i];
|
|
405
|
+
i++;
|
|
406
|
+
while (i < css.length) {
|
|
407
|
+
if (css[i] === "\\")
|
|
408
|
+
i += 2;
|
|
409
|
+
else if (css[i] === quote)
|
|
410
|
+
return i + 1;
|
|
411
|
+
else
|
|
412
|
+
i++;
|
|
413
|
+
}
|
|
414
|
+
return i;
|
|
415
|
+
}
|
|
416
|
+
function parseDeclarations(body) {
|
|
417
|
+
const decls = {};
|
|
418
|
+
let i = 0;
|
|
419
|
+
let start = 0;
|
|
420
|
+
let depth = 0;
|
|
421
|
+
const push = (chunk) => {
|
|
422
|
+
const s = stripComments(chunk).trim();
|
|
423
|
+
if (!s)
|
|
424
|
+
return;
|
|
425
|
+
const colon = indexOfTopLevel(s, ":");
|
|
426
|
+
if (colon <= 0)
|
|
427
|
+
return;
|
|
428
|
+
const rawProp = s.slice(0, colon).trim();
|
|
429
|
+
const prop = rawProp.startsWith("--") ? rawProp : rawProp.toLowerCase();
|
|
430
|
+
const value = s.slice(colon + 1).trim().replace(/\s*!important$/i, "");
|
|
431
|
+
if (prop)
|
|
432
|
+
decls[prop] = value;
|
|
433
|
+
};
|
|
434
|
+
while (i < body.length) {
|
|
435
|
+
const c = body[i];
|
|
436
|
+
if (c === "/" && body[i + 1] === "*") {
|
|
437
|
+
const end = body.indexOf("*/", i + 2);
|
|
438
|
+
i = end === -1 ? body.length : end + 2;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (c === '"' || c === "'") {
|
|
442
|
+
i = skipString(body, i);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (c === "(" || c === "{" || c === "[")
|
|
446
|
+
depth++;
|
|
447
|
+
else if (c === ")" || c === "}" || c === "]")
|
|
448
|
+
depth--;
|
|
449
|
+
else if (c === ";" && depth === 0) {
|
|
450
|
+
push(body.slice(start, i));
|
|
451
|
+
start = i + 1;
|
|
452
|
+
}
|
|
453
|
+
i++;
|
|
454
|
+
}
|
|
455
|
+
push(body.slice(start));
|
|
456
|
+
return decls;
|
|
457
|
+
}
|
|
458
|
+
function indexOfTopLevel(s, ch) {
|
|
459
|
+
let depth = 0;
|
|
460
|
+
for (let i = 0;i < s.length; i++) {
|
|
461
|
+
const c = s[i];
|
|
462
|
+
if (c === '"' || c === "'") {
|
|
463
|
+
i = skipString(s, i) - 1;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (c === "(")
|
|
467
|
+
depth++;
|
|
468
|
+
else if (c === ")")
|
|
469
|
+
depth--;
|
|
470
|
+
else if (c === ch && depth === 0)
|
|
471
|
+
return i;
|
|
472
|
+
}
|
|
473
|
+
return -1;
|
|
474
|
+
}
|
|
475
|
+
function extract(css) {
|
|
476
|
+
const model = {
|
|
477
|
+
pageRules: [],
|
|
478
|
+
stringSets: [],
|
|
479
|
+
pageAssignments: [],
|
|
480
|
+
breaks: [],
|
|
481
|
+
xrefs: [],
|
|
482
|
+
counterResets: [],
|
|
483
|
+
pageNames: [],
|
|
484
|
+
warnings: []
|
|
485
|
+
};
|
|
486
|
+
walk(css, model);
|
|
487
|
+
resolveGeometryVars(model, collectRootCustomProperties(css));
|
|
488
|
+
const names = new Set;
|
|
489
|
+
for (const r of model.pageRules)
|
|
490
|
+
if (r.name)
|
|
491
|
+
names.add(r.name);
|
|
492
|
+
for (const a of model.pageAssignments)
|
|
493
|
+
names.add(a.page);
|
|
494
|
+
model.pageNames = [...names];
|
|
495
|
+
return model;
|
|
496
|
+
}
|
|
497
|
+
var GEOMETRY_PROPS = [
|
|
498
|
+
"size",
|
|
499
|
+
"margin",
|
|
500
|
+
"margin-top",
|
|
501
|
+
"margin-right",
|
|
502
|
+
"margin-bottom",
|
|
503
|
+
"margin-left",
|
|
504
|
+
"bleed",
|
|
505
|
+
"marks"
|
|
506
|
+
];
|
|
507
|
+
function collectRootCustomProperties(css) {
|
|
508
|
+
const props = new Map;
|
|
509
|
+
const walkForRoot = (body) => {
|
|
510
|
+
for (const rule of scanRules(body)) {
|
|
511
|
+
if ("statement" in rule)
|
|
512
|
+
continue;
|
|
513
|
+
const { prelude, body: ruleBody } = rule;
|
|
514
|
+
if (NESTED_AT_RULES.test(prelude)) {
|
|
515
|
+
walkForRoot(ruleBody);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (!prelude.startsWith("@")) {
|
|
519
|
+
const selectors = splitTopLevel(prelude, ",");
|
|
520
|
+
if (selectors.some((s) => s.trim() === ":root")) {
|
|
521
|
+
const decls = parseDeclarations(ruleBody);
|
|
522
|
+
for (const [k, v] of Object.entries(decls)) {
|
|
523
|
+
if (k.startsWith("--"))
|
|
524
|
+
props.set(k, v);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
walkForRoot(css);
|
|
531
|
+
return props;
|
|
532
|
+
}
|
|
533
|
+
function matchParen(s, open) {
|
|
534
|
+
let depth = 0;
|
|
535
|
+
let i = open;
|
|
536
|
+
while (i < s.length) {
|
|
537
|
+
const c = s[i];
|
|
538
|
+
if (c === '"' || c === "'") {
|
|
539
|
+
i = skipString(s, i);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (c === "(")
|
|
543
|
+
depth++;
|
|
544
|
+
else if (c === ")") {
|
|
545
|
+
depth--;
|
|
546
|
+
if (depth === 0)
|
|
547
|
+
return i;
|
|
548
|
+
}
|
|
549
|
+
i++;
|
|
550
|
+
}
|
|
551
|
+
return -1;
|
|
552
|
+
}
|
|
553
|
+
function resolveVarsInValue(value, customProps, stack = new Set) {
|
|
554
|
+
let out = "";
|
|
555
|
+
let i = 0;
|
|
556
|
+
while (i < value.length) {
|
|
557
|
+
const isVarStart = /^var\(/i.test(value.slice(i, i + 4)) && (i === 0 || !/[\w-]/.test(value[i - 1]));
|
|
558
|
+
if (!isVarStart) {
|
|
559
|
+
out += value[i];
|
|
560
|
+
i++;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const open = i + 3;
|
|
564
|
+
const close = matchParen(value, open);
|
|
565
|
+
if (close === -1) {
|
|
566
|
+
out += value.slice(i);
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
const inner = value.slice(open + 1, close);
|
|
570
|
+
const commaIdx = indexOfTopLevel(inner, ",");
|
|
571
|
+
const name = (commaIdx === -1 ? inner : inner.slice(0, commaIdx)).trim();
|
|
572
|
+
const fallback = commaIdx === -1 ? undefined : inner.slice(commaIdx + 1).trim();
|
|
573
|
+
if (!/^--[\w-]+$/.test(name)) {
|
|
574
|
+
return { text: out, unresolved: `var(${inner}) is not a valid custom property reference` };
|
|
575
|
+
}
|
|
576
|
+
if (stack.has(name)) {
|
|
577
|
+
return { text: out, unresolved: `var(${name}) is circular` };
|
|
578
|
+
}
|
|
579
|
+
const rootValue = customProps.get(name);
|
|
580
|
+
if (rootValue !== undefined) {
|
|
581
|
+
const nested = resolveVarsInValue(rootValue, customProps, new Set(stack).add(name));
|
|
582
|
+
if (nested.unresolved)
|
|
583
|
+
return { text: out, unresolved: nested.unresolved };
|
|
584
|
+
out += nested.text;
|
|
585
|
+
} else if (fallback !== undefined) {
|
|
586
|
+
if (/\bvar\(/i.test(fallback)) {
|
|
587
|
+
return {
|
|
588
|
+
text: out,
|
|
589
|
+
unresolved: `var(${name}, ${fallback}) — a fallback containing another var() is not resolved`
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
out += fallback;
|
|
593
|
+
} else {
|
|
594
|
+
return {
|
|
595
|
+
text: out,
|
|
596
|
+
unresolved: `${name} is not defined at :root and var() has no fallback`
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
i = close + 1;
|
|
600
|
+
}
|
|
601
|
+
return { text: out };
|
|
602
|
+
}
|
|
603
|
+
function resolveGeometryVars(model, customProps) {
|
|
604
|
+
for (const rule of model.pageRules) {
|
|
605
|
+
for (const prop of GEOMETRY_PROPS) {
|
|
606
|
+
const value = rule.decls[prop];
|
|
607
|
+
if (!value || !/\bvar\(/i.test(value))
|
|
608
|
+
continue;
|
|
609
|
+
const { text, unresolved } = resolveVarsInValue(value, customProps);
|
|
610
|
+
if (unresolved) {
|
|
611
|
+
throw new Error(`${rule.raw} { ${prop}: ${value} } — cannot resolve ${unresolved}. ` + `Define the custom property at :root, add a literal var(--x, fallback) fallback, or use a literal value.`);
|
|
612
|
+
}
|
|
613
|
+
const unparsable = unparsableGeometry(prop, text);
|
|
614
|
+
if (unparsable) {
|
|
615
|
+
throw new Error(`${rule.raw} { ${prop}: ${value} } — resolves to \`${text.trim()}\`, ${unparsable}. ` + `Use a value this engine can read (a length like 0.75in/12pt/10mm, or a named page size for \`size\`).`);
|
|
616
|
+
}
|
|
617
|
+
rule.decls[prop] = text;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function unparsableGeometry(prop, text) {
|
|
622
|
+
const t = text.trim();
|
|
623
|
+
if (prop === "size") {
|
|
624
|
+
if (!t)
|
|
625
|
+
return "which is empty";
|
|
626
|
+
return parseSize(t) ? undefined : "which is not a page size";
|
|
627
|
+
}
|
|
628
|
+
if (prop === "margin" || prop.startsWith("margin-")) {
|
|
629
|
+
if (!t)
|
|
630
|
+
return "which is empty";
|
|
631
|
+
const parts = t.split(/\s+/);
|
|
632
|
+
if (prop !== "margin" && parts.length !== 1)
|
|
633
|
+
return "which is not a single length";
|
|
634
|
+
if (parts.length > 4)
|
|
635
|
+
return "which is not a valid margin";
|
|
636
|
+
const bad = parts.filter((p) => toPt(p) === undefined);
|
|
637
|
+
if (bad.length)
|
|
638
|
+
return `which this engine cannot read as a length (\`${bad[0]}\`)`;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (prop === "bleed") {
|
|
642
|
+
if (!t)
|
|
643
|
+
return "which is empty";
|
|
644
|
+
if (/^(auto|none)$/i.test(t))
|
|
645
|
+
return;
|
|
646
|
+
return toPt(t) === undefined ? "which this engine cannot read as a length" : undefined;
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
function walk(css, model) {
|
|
651
|
+
for (const rule of scanRules(css)) {
|
|
652
|
+
if ("statement" in rule)
|
|
653
|
+
continue;
|
|
654
|
+
const { prelude, body } = rule;
|
|
655
|
+
if (NESTED_AT_RULES.test(prelude)) {
|
|
656
|
+
walk(body, model);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (/^@page\b/i.test(prelude)) {
|
|
660
|
+
model.pageRules.push(parsePageRule(prelude, body));
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (prelude.startsWith("@"))
|
|
664
|
+
continue;
|
|
665
|
+
parseQualifiedRule(prelude, body, model);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function parsePageRule(prelude, body) {
|
|
669
|
+
const sel = prelude.replace(/^@page\s*/i, "").trim();
|
|
670
|
+
const nameMatch = /^([A-Za-z_][\w-]*)/.exec(sel);
|
|
671
|
+
const pseudos = [...sel.matchAll(/:([A-Za-z-]+)(\([^)]*\))?/g)].map((m) => m[1] + (m[2] ?? ""));
|
|
672
|
+
const rule = {
|
|
673
|
+
name: nameMatch?.[1],
|
|
674
|
+
pseudos,
|
|
675
|
+
decls: {},
|
|
676
|
+
marginBoxes: {},
|
|
677
|
+
raw: `@page ${sel}`.trim()
|
|
678
|
+
};
|
|
679
|
+
let rest = "";
|
|
680
|
+
let i = 0;
|
|
681
|
+
while (i < body.length) {
|
|
682
|
+
const at = findNextAtRule(body, i);
|
|
683
|
+
if (at === -1) {
|
|
684
|
+
rest += body.slice(i);
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
rest += body.slice(i, at);
|
|
688
|
+
const open = body.indexOf("{", at);
|
|
689
|
+
if (open === -1) {
|
|
690
|
+
rest += body.slice(at);
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
const name = body.slice(at + 1, open).trim().toLowerCase();
|
|
694
|
+
let depth = 1;
|
|
695
|
+
let j = open + 1;
|
|
696
|
+
while (j < body.length && depth > 0) {
|
|
697
|
+
const c = body[j];
|
|
698
|
+
if (c === "/" && body[j + 1] === "*") {
|
|
699
|
+
const end = body.indexOf("*/", j + 2);
|
|
700
|
+
j = end === -1 ? body.length : end + 2;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (c === '"' || c === "'") {
|
|
704
|
+
j = skipString(body, j);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (c === "{")
|
|
708
|
+
depth++;
|
|
709
|
+
else if (c === "}")
|
|
710
|
+
depth--;
|
|
711
|
+
j++;
|
|
712
|
+
}
|
|
713
|
+
rule.marginBoxes[`@${name}`] = parseDeclarations(body.slice(open + 1, j - 1));
|
|
714
|
+
i = j;
|
|
715
|
+
}
|
|
716
|
+
rule.decls = parseDeclarations(rest);
|
|
717
|
+
return rule;
|
|
718
|
+
}
|
|
719
|
+
function findNextAtRule(body, start) {
|
|
720
|
+
let i = start;
|
|
721
|
+
while (i < body.length) {
|
|
722
|
+
const c = body[i];
|
|
723
|
+
if (c === "/" && body[i + 1] === "*") {
|
|
724
|
+
const end = body.indexOf("*/", i + 2);
|
|
725
|
+
i = end === -1 ? body.length : end + 2;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (c === '"' || c === "'") {
|
|
729
|
+
i = skipString(body, i);
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (c === "@")
|
|
733
|
+
return i;
|
|
734
|
+
i++;
|
|
735
|
+
}
|
|
736
|
+
return -1;
|
|
737
|
+
}
|
|
738
|
+
function parseQualifiedRule(selector, body, model) {
|
|
739
|
+
const decls = parseDeclarations(body);
|
|
740
|
+
for (const [prop, value] of Object.entries(decls)) {
|
|
741
|
+
if (prop === "string-set") {
|
|
742
|
+
for (const entry of splitTopLevel(value, ",")) {
|
|
743
|
+
const m = /^\s*([A-Za-z_][\w-]*)\s+(.+)$/.exec(entry);
|
|
744
|
+
if (m && m[1] !== undefined && m[2] !== undefined)
|
|
745
|
+
model.stringSets.push({ selector, name: m[1], value: m[2].trim() });
|
|
746
|
+
}
|
|
747
|
+
} else if (prop === "page") {
|
|
748
|
+
if (value && value !== "auto")
|
|
749
|
+
model.pageAssignments.push({ selector, page: value.trim() });
|
|
750
|
+
} else if (prop === "break-before" || prop === "break-after" || prop === "break-inside") {
|
|
751
|
+
model.breaks.push({ selector, prop, value });
|
|
752
|
+
} else if (prop === "counter-reset") {
|
|
753
|
+
const m = /\bpage\s+(-?\d+)/.exec(value);
|
|
754
|
+
if (m)
|
|
755
|
+
model.counterResets.push({ selector, start: Number(m[1]) });
|
|
756
|
+
} else if (prop === "content") {
|
|
757
|
+
for (const fn of ["target-counter", "target-text", "leader", "string"]) {
|
|
758
|
+
if (new RegExp(`\\b${fn}\\s*\\(`).test(value)) {
|
|
759
|
+
model.xrefs.push({ selector, content: value, fn });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function splitTopLevel(s, sep) {
|
|
766
|
+
const out = [];
|
|
767
|
+
let depth = 0;
|
|
768
|
+
let start = 0;
|
|
769
|
+
for (let i = 0;i < s.length; i++) {
|
|
770
|
+
const c = s[i];
|
|
771
|
+
if (c === '"' || c === "'") {
|
|
772
|
+
i = skipString(s, i) - 1;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (c === "(")
|
|
776
|
+
depth++;
|
|
777
|
+
else if (c === ")")
|
|
778
|
+
depth--;
|
|
779
|
+
else if (c === sep && depth === 0) {
|
|
780
|
+
out.push(s.slice(start, i));
|
|
781
|
+
start = i + 1;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
out.push(s.slice(start));
|
|
785
|
+
return out.map((x) => x.trim()).filter(Boolean);
|
|
786
|
+
}
|
|
787
|
+
var UNITS_PER_PT = {
|
|
788
|
+
pt: 1,
|
|
789
|
+
px: 0.75,
|
|
790
|
+
in: 72,
|
|
791
|
+
pc: 12,
|
|
792
|
+
cm: 72 / 2.54,
|
|
793
|
+
mm: 72 / 25.4,
|
|
794
|
+
q: 72 / 101.6
|
|
795
|
+
};
|
|
796
|
+
function toPt(value) {
|
|
797
|
+
const m = /^(-?[\d.]+)([a-z%]*)$/i.exec(value.trim());
|
|
798
|
+
if (!m)
|
|
799
|
+
return;
|
|
800
|
+
const n = Number(m[1]);
|
|
801
|
+
const unit = (m[2] || "px").toLowerCase();
|
|
802
|
+
const factor = UNITS_PER_PT[unit];
|
|
803
|
+
return factor === undefined ? undefined : n * factor;
|
|
804
|
+
}
|
|
805
|
+
var PAGE_SIZES = {
|
|
806
|
+
a5: [419.53, 595.28],
|
|
807
|
+
a4: [595.28, 841.89],
|
|
808
|
+
a3: [841.89, 1190.55],
|
|
809
|
+
b5: [498.9, 708.66],
|
|
810
|
+
b4: [708.66, 1000.63],
|
|
811
|
+
"jis-b5": [515.91, 728.5],
|
|
812
|
+
"jis-b4": [728.5, 1031.81],
|
|
813
|
+
letter: [612, 792],
|
|
814
|
+
legal: [612, 1008],
|
|
815
|
+
ledger: [1224, 792]
|
|
816
|
+
};
|
|
817
|
+
function parseSize(value) {
|
|
818
|
+
const parts = value.trim().split(/\s+/);
|
|
819
|
+
let landscape = false;
|
|
820
|
+
const lens = [];
|
|
821
|
+
let named;
|
|
822
|
+
for (const p of parts) {
|
|
823
|
+
const key = p.toLowerCase();
|
|
824
|
+
if (key === "landscape")
|
|
825
|
+
landscape = true;
|
|
826
|
+
else if (key === "portrait")
|
|
827
|
+
continue;
|
|
828
|
+
else if (PAGE_SIZES[key])
|
|
829
|
+
named = PAGE_SIZES[key];
|
|
830
|
+
else {
|
|
831
|
+
const pt = toPt(p);
|
|
832
|
+
if (pt !== undefined)
|
|
833
|
+
lens.push(pt);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
let w, h;
|
|
837
|
+
if (named)
|
|
838
|
+
[w, h] = named;
|
|
839
|
+
else if (lens.length === 1)
|
|
840
|
+
[w, h] = [lens[0], lens[0]];
|
|
841
|
+
else if (lens.length >= 2)
|
|
842
|
+
[w, h] = [lens[0], lens[1]];
|
|
843
|
+
else
|
|
844
|
+
return;
|
|
845
|
+
if (landscape && h > w)
|
|
846
|
+
[w, h] = [h, w];
|
|
847
|
+
return { width: w, height: h };
|
|
848
|
+
}
|
|
849
|
+
function parseMargin(value) {
|
|
850
|
+
const parts = value.trim().split(/\s+/).map((p) => toPt(p) ?? 0);
|
|
851
|
+
const a = parts[0] ?? 0;
|
|
852
|
+
const b = parts[1] ?? a;
|
|
853
|
+
const c = parts[2] ?? a;
|
|
854
|
+
const d = parts[3] ?? b;
|
|
855
|
+
return { top: a, right: b, bottom: c, left: d };
|
|
856
|
+
}
|
|
857
|
+
function resolvePage(model, ctx = {}) {
|
|
858
|
+
const wanted = new Set(ctx.pseudos ?? []);
|
|
859
|
+
const applicable = model.pageRules.filter((r) => r.name ? r.name === ctx.name : true).filter((r) => r.pseudos.every((p) => wanted.has(p))).sort((a, b) => specificity(a) - specificity(b));
|
|
860
|
+
const decls = {};
|
|
861
|
+
const marginBoxes = {};
|
|
862
|
+
const margin = { top: 72, right: 72, bottom: 72, left: 72 };
|
|
863
|
+
for (const rule of applicable) {
|
|
864
|
+
for (const [prop, value] of Object.entries(rule.decls)) {
|
|
865
|
+
if (prop === "margin")
|
|
866
|
+
Object.assign(margin, parseMargin(value));
|
|
867
|
+
else if (prop === "margin-top" || prop === "margin-right" || prop === "margin-bottom" || prop === "margin-left") {
|
|
868
|
+
const side = prop.slice(7);
|
|
869
|
+
margin[side] = toPt(value) ?? margin[side];
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
Object.assign(decls, rule.decls);
|
|
873
|
+
for (const [box, d] of Object.entries(rule.marginBoxes)) {
|
|
874
|
+
marginBoxes[box] = { ...marginBoxes[box] ?? {}, ...d };
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const size = parseSize(decls.size ?? "letter") ?? { width: 612, height: 792 };
|
|
878
|
+
return {
|
|
879
|
+
decls,
|
|
880
|
+
marginBoxes,
|
|
881
|
+
geometry: {
|
|
882
|
+
width: size.width,
|
|
883
|
+
height: size.height,
|
|
884
|
+
margin,
|
|
885
|
+
bleed: decls.bleed ? toPt(decls.bleed) ?? 0 : 0,
|
|
886
|
+
marks: (decls.marks ?? "none").split(/\s+/).filter((m) => m && m !== "none")
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function specificity(r) {
|
|
891
|
+
return (r.name ? 2 : 0) + r.pseudos.length;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/engine/shared/synthesis.ts
|
|
895
|
+
var RECTO_VERSO_VALUES = /^(right|recto|left|verso)$/;
|
|
896
|
+
function isRectoVersoBreak(decl) {
|
|
897
|
+
return decl.prop === "break-before" && RECTO_VERSO_VALUES.test(decl.value.trim());
|
|
898
|
+
}
|
|
899
|
+
function wantsRecto(value) {
|
|
900
|
+
return /^(right|recto)$/.test(value.trim());
|
|
901
|
+
}
|
|
902
|
+
function planRectoBlanks(sites) {
|
|
903
|
+
let shift = 0;
|
|
904
|
+
return sites.map((site) => {
|
|
905
|
+
if (site.page <= 0)
|
|
906
|
+
return false;
|
|
907
|
+
const effective = site.page + shift;
|
|
908
|
+
const onRecto = effective % 2 === 1;
|
|
909
|
+
const wrong = site.wantsRecto ? !onRecto : onRecto;
|
|
910
|
+
if (wrong)
|
|
911
|
+
shift++;
|
|
912
|
+
return wrong;
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
var WHICH_VALUES = new Set(["first", "start", "last", "first-except"]);
|
|
916
|
+
function parseWhich(raw) {
|
|
917
|
+
const w = (raw ?? "").trim();
|
|
918
|
+
return WHICH_VALUES.has(w) ? w : "first";
|
|
919
|
+
}
|
|
920
|
+
function stringValueAt(entries, page, which = "first") {
|
|
921
|
+
let entry = "";
|
|
922
|
+
const onPage = [];
|
|
923
|
+
for (const e of entries) {
|
|
924
|
+
if (e.page < page)
|
|
925
|
+
entry = e.value;
|
|
926
|
+
else if (e.page === page)
|
|
927
|
+
onPage.push(e.value);
|
|
928
|
+
else
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
switch (which) {
|
|
932
|
+
case "start":
|
|
933
|
+
return entry;
|
|
934
|
+
case "last":
|
|
935
|
+
return onPage.length ? onPage[onPage.length - 1] : entry;
|
|
936
|
+
case "first-except":
|
|
937
|
+
return onPage.length ? "" : entry;
|
|
938
|
+
case "first":
|
|
939
|
+
return onPage.length ? onPage[0] : entry;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function stringSymbols(entries, pageCount, which = "first") {
|
|
943
|
+
const symbols = [];
|
|
944
|
+
for (let p = 1;p <= pageCount; p++)
|
|
945
|
+
symbols.push(stringValueAt(entries, p, which));
|
|
946
|
+
return symbols;
|
|
947
|
+
}
|
|
948
|
+
function cssQuote(s) {
|
|
949
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
950
|
+
}
|
|
951
|
+
function counterStyleName(name, which) {
|
|
952
|
+
return which === "first" ? `gp-${name}` : `gp-${name}--${which}`;
|
|
953
|
+
}
|
|
954
|
+
function pageCounterValues(resets, pageCount) {
|
|
955
|
+
const byPage = new Map;
|
|
956
|
+
for (const r of resets)
|
|
957
|
+
if (r.page >= 1)
|
|
958
|
+
byPage.set(r.page, r.start);
|
|
959
|
+
const values = [];
|
|
960
|
+
let value = 0;
|
|
961
|
+
for (let p = 1;p <= pageCount; p++) {
|
|
962
|
+
if (byPage.has(p))
|
|
963
|
+
value = byPage.get(p) - 1;
|
|
964
|
+
value++;
|
|
965
|
+
values.push(value);
|
|
966
|
+
}
|
|
967
|
+
return values;
|
|
968
|
+
}
|
|
969
|
+
function restartedPageValues(resetSites, pageMap, pageCount) {
|
|
970
|
+
const resets = resetSites.map((s) => ({ page: pageMap[s.id] ?? 0, start: s.start })).filter((r) => r.page > 0);
|
|
971
|
+
return resets.length ? pageCounterValues(resets, pageCount) : null;
|
|
972
|
+
}
|
|
973
|
+
function toFolioPage(physicalPage, pageValues) {
|
|
974
|
+
if (!pageValues)
|
|
975
|
+
return physicalPage;
|
|
976
|
+
return pageValues[physicalPage - 1] ?? physicalPage;
|
|
977
|
+
}
|
|
978
|
+
var LEADER_START = "";
|
|
979
|
+
var LEADER_END = "";
|
|
980
|
+
function leaderMarker(glue) {
|
|
981
|
+
return `${LEADER_START}${glue}${LEADER_END}`;
|
|
982
|
+
}
|
|
983
|
+
function generatedContentCss(selectors) {
|
|
984
|
+
const rules = new Set;
|
|
985
|
+
for (const raw of selectors) {
|
|
986
|
+
for (const one of raw.split(",")) {
|
|
987
|
+
const selector = one.trim();
|
|
988
|
+
if (!selector)
|
|
989
|
+
continue;
|
|
990
|
+
const m = /^(.*?)(::?)(after|before)\s*$/i.exec(selector);
|
|
991
|
+
if (!m)
|
|
992
|
+
continue;
|
|
993
|
+
const [, base, colons, pseudo] = m;
|
|
994
|
+
if (base === undefined || colons === undefined || pseudo === undefined)
|
|
995
|
+
continue;
|
|
996
|
+
const where = pseudo.toLowerCase();
|
|
997
|
+
rules.add(`${base.trim()}[data-gp-${where}]${colons}${where} { content: attr(data-gp-${where}); }`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
rules.add(`[data-gp-after]::after { content: attr(data-gp-after); }`);
|
|
1001
|
+
rules.add(`[data-gp-before]::before { content: attr(data-gp-before); }`);
|
|
1002
|
+
return [...rules].join(`
|
|
1003
|
+
`);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/engine/shared/content-value.ts
|
|
1007
|
+
var FUNC = /^([a-z-]+)\(/i;
|
|
1008
|
+
function parseContent(value) {
|
|
1009
|
+
const parts = [];
|
|
1010
|
+
let i = 0;
|
|
1011
|
+
const s = value.trim();
|
|
1012
|
+
while (i < s.length) {
|
|
1013
|
+
const c = s[i];
|
|
1014
|
+
if (/\s/.test(c)) {
|
|
1015
|
+
i++;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (c === '"' || c === "'") {
|
|
1019
|
+
const end = closeString(s, i);
|
|
1020
|
+
parts.push({ type: "literal", value: unquote(s.slice(i, end)) });
|
|
1021
|
+
i = end;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const rest = s.slice(i);
|
|
1025
|
+
const fn = FUNC.exec(rest);
|
|
1026
|
+
if (fn && fn[1] !== undefined) {
|
|
1027
|
+
const open = i + fn[0].length - 1;
|
|
1028
|
+
const close = matchParen2(s, open);
|
|
1029
|
+
const args = splitTopLevel(s.slice(open + 1, close), ",");
|
|
1030
|
+
parts.push(toPart(fn[1].toLowerCase(), args));
|
|
1031
|
+
i = close + 1;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const word = /^[^\s"']+/.exec(rest)[0];
|
|
1035
|
+
parts.push({ type: "keyword", value: word });
|
|
1036
|
+
i += word.length;
|
|
1037
|
+
}
|
|
1038
|
+
return parts;
|
|
1039
|
+
}
|
|
1040
|
+
function toPart(name, args) {
|
|
1041
|
+
switch (name) {
|
|
1042
|
+
case "counter":
|
|
1043
|
+
return { type: "counter", name: args[0] ?? "page", style: args[1] ?? "decimal" };
|
|
1044
|
+
case "string":
|
|
1045
|
+
return { type: "string", name: args[0] ?? "", which: args[1] ?? "first" };
|
|
1046
|
+
case "target-counter":
|
|
1047
|
+
return {
|
|
1048
|
+
type: "target-counter",
|
|
1049
|
+
url: args[0] ?? "",
|
|
1050
|
+
counter: args[1] ?? "page",
|
|
1051
|
+
style: args[2] ?? "decimal"
|
|
1052
|
+
};
|
|
1053
|
+
case "target-text":
|
|
1054
|
+
return { type: "target-text", url: args[0] ?? "", which: args[1] ?? "content" };
|
|
1055
|
+
case "leader":
|
|
1056
|
+
return { type: "leader", glue: unquote(args[0] ?? '"."') };
|
|
1057
|
+
case "attr": {
|
|
1058
|
+
const [a, as] = (args[0] ?? "").split(/\s+/);
|
|
1059
|
+
return { type: "attr", name: a ?? "", as };
|
|
1060
|
+
}
|
|
1061
|
+
case "content":
|
|
1062
|
+
return { type: "content", which: args[0] ?? "text" };
|
|
1063
|
+
default:
|
|
1064
|
+
return { type: "keyword", value: `${name}(${args.join(",")})` };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function closeString(s, i) {
|
|
1068
|
+
const q = s[i++];
|
|
1069
|
+
while (i < s.length) {
|
|
1070
|
+
if (s[i] === "\\")
|
|
1071
|
+
i += 2;
|
|
1072
|
+
else if (s[i] === q)
|
|
1073
|
+
return i + 1;
|
|
1074
|
+
else
|
|
1075
|
+
i++;
|
|
1076
|
+
}
|
|
1077
|
+
return i;
|
|
1078
|
+
}
|
|
1079
|
+
function matchParen2(s, open) {
|
|
1080
|
+
let depth = 0;
|
|
1081
|
+
for (let i = open;i < s.length; i++) {
|
|
1082
|
+
const c = s[i];
|
|
1083
|
+
if (c === '"' || c === "'") {
|
|
1084
|
+
i = closeString(s, i) - 1;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (c === "(")
|
|
1088
|
+
depth++;
|
|
1089
|
+
else if (c === ")" && --depth === 0)
|
|
1090
|
+
return i;
|
|
1091
|
+
}
|
|
1092
|
+
return s.length - 1;
|
|
1093
|
+
}
|
|
1094
|
+
function unquote(s) {
|
|
1095
|
+
const t = s.trim();
|
|
1096
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'"))
|
|
1097
|
+
return t.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
1098
|
+
return t;
|
|
1099
|
+
}
|
|
1100
|
+
function resolveUrlArg(arg, ctx) {
|
|
1101
|
+
const m = /^attr\(\s*([\w-]+)(?:\s+url)?\s*\)$/i.exec(arg.trim());
|
|
1102
|
+
if (m && m[1] !== undefined)
|
|
1103
|
+
return ctx.attr?.(m[1]) ?? "";
|
|
1104
|
+
return unquote(arg);
|
|
1105
|
+
}
|
|
1106
|
+
var ROMAN = [
|
|
1107
|
+
[1000, "m"],
|
|
1108
|
+
[900, "cm"],
|
|
1109
|
+
[500, "d"],
|
|
1110
|
+
[400, "cd"],
|
|
1111
|
+
[100, "c"],
|
|
1112
|
+
[90, "xc"],
|
|
1113
|
+
[50, "l"],
|
|
1114
|
+
[40, "xl"],
|
|
1115
|
+
[10, "x"],
|
|
1116
|
+
[9, "ix"],
|
|
1117
|
+
[5, "v"],
|
|
1118
|
+
[4, "iv"],
|
|
1119
|
+
[1, "i"]
|
|
1120
|
+
];
|
|
1121
|
+
function formatCounter(n, style = "decimal") {
|
|
1122
|
+
switch (style.trim()) {
|
|
1123
|
+
case "decimal-leading-zero":
|
|
1124
|
+
return n < 10 ? `0${n}` : String(n);
|
|
1125
|
+
case "lower-roman":
|
|
1126
|
+
case "upper-roman": {
|
|
1127
|
+
let v = n;
|
|
1128
|
+
let out = "";
|
|
1129
|
+
for (const [num, sym] of ROMAN)
|
|
1130
|
+
while (v >= num)
|
|
1131
|
+
out += sym, v -= num;
|
|
1132
|
+
return style === "upper-roman" ? out.toUpperCase() : out;
|
|
1133
|
+
}
|
|
1134
|
+
case "lower-alpha":
|
|
1135
|
+
case "upper-alpha": {
|
|
1136
|
+
let v = n;
|
|
1137
|
+
let out = "";
|
|
1138
|
+
while (v > 0) {
|
|
1139
|
+
const r = (v - 1) % 26;
|
|
1140
|
+
out = String.fromCharCode(97 + r) + out;
|
|
1141
|
+
v = Math.floor((v - 1) / 26);
|
|
1142
|
+
}
|
|
1143
|
+
return style === "upper-alpha" ? out.toUpperCase() : out;
|
|
1144
|
+
}
|
|
1145
|
+
case "none":
|
|
1146
|
+
return "";
|
|
1147
|
+
default:
|
|
1148
|
+
return String(n);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function evaluateContent(parts, ctx) {
|
|
1152
|
+
let out = "";
|
|
1153
|
+
for (const p of parts) {
|
|
1154
|
+
switch (p.type) {
|
|
1155
|
+
case "literal":
|
|
1156
|
+
out += p.value;
|
|
1157
|
+
break;
|
|
1158
|
+
case "counter":
|
|
1159
|
+
out += formatCounter(p.name === "pages" ? ctx.pages ?? 0 : ctx.page ?? 0, p.style);
|
|
1160
|
+
break;
|
|
1161
|
+
case "string":
|
|
1162
|
+
out += ctx.strings?.(p.name, p.which) ?? "";
|
|
1163
|
+
break;
|
|
1164
|
+
case "target-counter": {
|
|
1165
|
+
const url = resolveUrlArg(p.url, ctx);
|
|
1166
|
+
const page = ctx.targetPage?.(url);
|
|
1167
|
+
out += page === undefined ? "?" : formatCounter(page, p.style);
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
case "target-text": {
|
|
1171
|
+
const url = resolveUrlArg(p.url, ctx);
|
|
1172
|
+
out += ctx.targetText?.(url, p.which) ?? "";
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
case "attr":
|
|
1176
|
+
out += ctx.attr?.(p.name) ?? "";
|
|
1177
|
+
break;
|
|
1178
|
+
case "content":
|
|
1179
|
+
out += ctx.text ?? "";
|
|
1180
|
+
break;
|
|
1181
|
+
case "leader":
|
|
1182
|
+
out += ctx.leader?.(p.glue) ?? "";
|
|
1183
|
+
break;
|
|
1184
|
+
case "keyword":
|
|
1185
|
+
if (p.value === "normal" || p.value === "none")
|
|
1186
|
+
break;
|
|
1187
|
+
out += p.value;
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
function evaluate(value, ctx) {
|
|
1194
|
+
return evaluateContent(parseContent(value), ctx);
|
|
1195
|
+
}
|
|
1196
|
+
function needsMeasurement(value) {
|
|
1197
|
+
return parseContent(value).some((p) => p.type === "target-counter" || p.type === "target-text" || p.type === "leader");
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/engine/shared/pdf-inspect.ts
|
|
1201
|
+
import {
|
|
1202
|
+
PDFArray,
|
|
1203
|
+
PDFDict,
|
|
1204
|
+
PDFDocument,
|
|
1205
|
+
PDFHexString,
|
|
1206
|
+
PDFName,
|
|
1207
|
+
PDFNumber,
|
|
1208
|
+
PDFRef,
|
|
1209
|
+
PDFString
|
|
1210
|
+
} from "pdf-lib";
|
|
1211
|
+
function boxOf(page, name) {
|
|
1212
|
+
const arr = page.node.get(PDFName.of(name));
|
|
1213
|
+
if (!(arr instanceof PDFArray))
|
|
1214
|
+
return;
|
|
1215
|
+
return arr.asArray().map((n) => n.asNumber());
|
|
1216
|
+
}
|
|
1217
|
+
async function inspectPdf(bytes) {
|
|
1218
|
+
const doc = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
1219
|
+
const pages = doc.getPages();
|
|
1220
|
+
const pageRefIndex = new Map;
|
|
1221
|
+
pages.forEach((p, i) => pageRefIndex.set(p.ref.toString(), i));
|
|
1222
|
+
const boxes = pages.map((p) => ({
|
|
1223
|
+
media: boxOf(p, "MediaBox") ?? [],
|
|
1224
|
+
crop: boxOf(p, "CropBox"),
|
|
1225
|
+
bleed: boxOf(p, "BleedBox"),
|
|
1226
|
+
trim: boxOf(p, "TrimBox")
|
|
1227
|
+
}));
|
|
1228
|
+
const resolvePageIndex = (dest) => {
|
|
1229
|
+
let d = dest;
|
|
1230
|
+
if (d instanceof PDFDict)
|
|
1231
|
+
d = d.get(PDFName.of("D")) ?? d;
|
|
1232
|
+
if (!(d instanceof PDFArray))
|
|
1233
|
+
return;
|
|
1234
|
+
const first = d.get(0);
|
|
1235
|
+
if (first instanceof PDFRef)
|
|
1236
|
+
return pageRefIndex.get(first.toString());
|
|
1237
|
+
if (first instanceof PDFNumber)
|
|
1238
|
+
return first.asNumber();
|
|
1239
|
+
return;
|
|
1240
|
+
};
|
|
1241
|
+
const namedDests = {};
|
|
1242
|
+
const catalog = doc.catalog;
|
|
1243
|
+
const walkNameTree = (node) => {
|
|
1244
|
+
if (!node)
|
|
1245
|
+
return;
|
|
1246
|
+
const names = node.lookupMaybe(PDFName.of("Names"), PDFArray);
|
|
1247
|
+
if (names) {
|
|
1248
|
+
for (let i = 0;i + 1 < names.size(); i += 2) {
|
|
1249
|
+
const key = names.lookup(i);
|
|
1250
|
+
const val = names.lookup(i + 1);
|
|
1251
|
+
const idx = resolvePageIndex(val);
|
|
1252
|
+
if (idx !== undefined)
|
|
1253
|
+
namedDests[String(key).replace(/^\((.*)\)$/, "$1")] = idx;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const kids = node.lookupMaybe(PDFName.of("Kids"), PDFArray);
|
|
1257
|
+
if (kids)
|
|
1258
|
+
for (let i = 0;i < kids.size(); i++)
|
|
1259
|
+
walkNameTree(kids.lookup(i, PDFDict));
|
|
1260
|
+
};
|
|
1261
|
+
const namesDict = catalog.lookupMaybe(PDFName.of("Names"), PDFDict);
|
|
1262
|
+
walkNameTree(namesDict?.lookupMaybe(PDFName.of("Dests"), PDFDict));
|
|
1263
|
+
const legacyDests = catalog.lookupMaybe(PDFName.of("Dests"), PDFDict);
|
|
1264
|
+
if (legacyDests) {
|
|
1265
|
+
for (const [k, v] of legacyDests.entries()) {
|
|
1266
|
+
const idx = resolvePageIndex(doc.context.lookup(v));
|
|
1267
|
+
if (idx !== undefined)
|
|
1268
|
+
namedDests[k.asString().replace(/^\//, "")] = idx;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const linkTargets = {};
|
|
1272
|
+
for (const page of pages) {
|
|
1273
|
+
const annots = page.node.lookup(PDFName.of("Annots"));
|
|
1274
|
+
if (!(annots instanceof PDFArray))
|
|
1275
|
+
continue;
|
|
1276
|
+
for (let i = 0;i < annots.size(); i++) {
|
|
1277
|
+
const a = annots.lookup(i, PDFDict);
|
|
1278
|
+
if (!a)
|
|
1279
|
+
continue;
|
|
1280
|
+
if (a.get(PDFName.of("Subtype"))?.toString() !== "/Link")
|
|
1281
|
+
continue;
|
|
1282
|
+
let dest = a.get(PDFName.of("Dest"));
|
|
1283
|
+
if (!dest) {
|
|
1284
|
+
const action = a.lookup(PDFName.of("A"));
|
|
1285
|
+
if (action instanceof PDFDict)
|
|
1286
|
+
dest = action.get(PDFName.of("D"));
|
|
1287
|
+
}
|
|
1288
|
+
if (dest instanceof PDFRef)
|
|
1289
|
+
dest = doc.context.lookup(dest);
|
|
1290
|
+
let target;
|
|
1291
|
+
if (dest instanceof PDFName || dest instanceof PDFString || dest instanceof PDFHexString) {
|
|
1292
|
+
const key2 = String(dest).replace(/^[/(]|\)$/g, "");
|
|
1293
|
+
target = namedDests[key2];
|
|
1294
|
+
} else {
|
|
1295
|
+
target = resolvePageIndex(dest);
|
|
1296
|
+
}
|
|
1297
|
+
if (target === undefined)
|
|
1298
|
+
continue;
|
|
1299
|
+
const rect = a.lookupMaybe(PDFName.of("Rect"), PDFArray);
|
|
1300
|
+
const key = rect ? rect.asArray().map((n) => Math.round(n.asNumber() * 100) / 100).join(",") : `annot-${Object.keys(linkTargets).length}`;
|
|
1301
|
+
linkTargets[key] = target;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const outline = [];
|
|
1305
|
+
const outlines = catalog.lookupMaybe(PDFName.of("Outlines"), PDFDict);
|
|
1306
|
+
const walkOutline = (node) => {
|
|
1307
|
+
let cur = node?.lookupMaybe(PDFName.of("First"), PDFDict);
|
|
1308
|
+
while (cur) {
|
|
1309
|
+
const titleObj = cur.get(PDFName.of("Title"));
|
|
1310
|
+
const title = titleObj ? String(titleObj).replace(/^\((.*)\)$/, "$1").replace(/^/, "") : "";
|
|
1311
|
+
let dest = cur.get(PDFName.of("Dest"));
|
|
1312
|
+
if (!dest) {
|
|
1313
|
+
const action = cur.lookupMaybe(PDFName.of("A"), PDFDict);
|
|
1314
|
+
dest = action?.get(PDFName.of("D"));
|
|
1315
|
+
}
|
|
1316
|
+
if (dest instanceof PDFRef)
|
|
1317
|
+
dest = doc.context.lookup(dest);
|
|
1318
|
+
const page = resolvePageIndex(dest);
|
|
1319
|
+
if (page !== undefined)
|
|
1320
|
+
outline.push({ title, page });
|
|
1321
|
+
walkOutline(cur.lookupMaybe(PDFName.of("First"), PDFDict) ? cur : undefined);
|
|
1322
|
+
cur = cur.lookupMaybe(PDFName.of("Next"), PDFDict);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
walkOutline(outlines);
|
|
1326
|
+
return { pageCount: pages.length, boxes, linkTargets, namedDests, outline };
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/lib/embedded-assets.ts
|
|
1330
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1331
|
+
import { mkdtemp, mkdir, writeFile, readFile } from "node:fs/promises";
|
|
1332
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1333
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
1334
|
+
import { fileURLToPath } from "node:url";
|
|
1335
|
+
|
|
1336
|
+
// src/assets/favicon.ico
|
|
1337
|
+
var favicon_default = "./favicon-wkbm9cjn.ico";
|
|
1338
|
+
|
|
1339
|
+
// src/assets/manifest.schema.json
|
|
1340
|
+
var manifest_schema_default = "./manifest.schema-zxgxnbg7.json";
|
|
1341
|
+
|
|
1342
|
+
// src/assets/preview/scripts/preview-interface.js
|
|
1343
|
+
var preview_interface_default = "./preview-interface-435cczt5.js";
|
|
1344
|
+
|
|
1345
|
+
// src/assets/preview/scripts/preview-bridge.js
|
|
1346
|
+
var preview_bridge_default = "./preview-bridge-fz7vpk8m.js";
|
|
1347
|
+
|
|
1348
|
+
// src/assets/preview/scripts/preview-shell.js
|
|
1349
|
+
var preview_shell_default = "./preview-shell-c5mfa3q0.js";
|
|
1350
|
+
|
|
1351
|
+
// profiles/CGATS21_CRPC1.icc
|
|
1352
|
+
var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
|
|
1353
|
+
|
|
1354
|
+
// src/assets/engine/gutterpress-viewer.js
|
|
1355
|
+
var gutterpress_viewer_default = "./gutterpress-viewer-cem7dmr5.js";
|
|
1356
|
+
|
|
1357
|
+
// src/assets/engine/gutterpress-agent.js
|
|
1358
|
+
var gutterpress_agent_default = "./gutterpress-agent-1ctgfz92.js";
|
|
1359
|
+
|
|
1360
|
+
// src/assets/templates/book/manifest.yaml
|
|
1361
|
+
var manifest_default = "./manifest-n1gh3qw5.yaml";
|
|
1362
|
+
|
|
1363
|
+
// src/assets/templates/book/chapter-01.md
|
|
1364
|
+
var chapter_01_default = "./chapter-01-rkpyvjna.md";
|
|
1365
|
+
|
|
1366
|
+
// src/assets/templates/zine/manifest.yaml
|
|
1367
|
+
var manifest_default2 = "./manifest-n1gh3qw5.yaml";
|
|
1368
|
+
|
|
1369
|
+
// src/assets/templates/zine/chapter-01.md
|
|
1370
|
+
var chapter_01_default2 = "./chapter-01-7jwqg1ct.md";
|
|
1371
|
+
|
|
1372
|
+
// src/assets/templates/technical/manifest.yaml
|
|
1373
|
+
var manifest_default3 = "./manifest-n1gh3qw5.yaml";
|
|
1374
|
+
|
|
1375
|
+
// src/assets/templates/technical/chapter-01.md
|
|
1376
|
+
var chapter_01_default3 = "./chapter-01-6fn9pvd0.md";
|
|
1377
|
+
|
|
1378
|
+
// src/assets/themes/clean-book/theme.css
|
|
1379
|
+
var theme_default = "./theme-j2bagrfx.css";
|
|
1380
|
+
|
|
1381
|
+
// src/assets/themes/clean-book/theme.json
|
|
1382
|
+
var theme_default2 = "./theme-xt0hjb1b.json";
|
|
1383
|
+
|
|
1384
|
+
// src/assets/themes/zine/theme.css
|
|
1385
|
+
var theme_default3 = "./theme-h5recz6c.css";
|
|
1386
|
+
|
|
1387
|
+
// src/assets/themes/zine/theme.json
|
|
1388
|
+
var theme_default4 = "./theme-cd6efzjw.json";
|
|
1389
|
+
|
|
1390
|
+
// src/assets/themes/technical-doc/theme.css
|
|
1391
|
+
var theme_default5 = "./theme-nn6d53zy.css";
|
|
1392
|
+
|
|
1393
|
+
// src/assets/themes/technical-doc/theme.json
|
|
1394
|
+
var theme_default6 = "./theme-tqfypc1w.json";
|
|
1395
|
+
|
|
1396
|
+
// src/lib/embedded-assets.ts
|
|
1397
|
+
var __libdir = dirname(fileURLToPath(import.meta.url));
|
|
1398
|
+
var abs = (p) => resolve(__libdir, p);
|
|
1399
|
+
var filePath = (v) => v;
|
|
1400
|
+
var EMBEDDED_ASSETS = {
|
|
1401
|
+
"favicon.ico": abs(favicon_default),
|
|
1402
|
+
"manifest.schema.json": abs(filePath(manifest_schema_default)),
|
|
1403
|
+
"preview/scripts/preview-interface.js": abs(filePath(preview_interface_default)),
|
|
1404
|
+
"preview/scripts/preview-bridge.js": abs(filePath(preview_bridge_default)),
|
|
1405
|
+
"preview/scripts/preview-shell.js": abs(filePath(preview_shell_default)),
|
|
1406
|
+
"profiles/CGATS21_CRPC1.icc": abs(filePath(CGATS21_CRPC1_default)),
|
|
1407
|
+
"engine/gutterpress-viewer.js": abs(filePath(gutterpress_viewer_default)),
|
|
1408
|
+
"engine/gutterpress-agent.js": abs(filePath(gutterpress_agent_default)),
|
|
1409
|
+
"templates/book/manifest.yaml": abs(filePath(manifest_default)),
|
|
1410
|
+
"templates/book/chapter-01.md": abs(filePath(chapter_01_default)),
|
|
1411
|
+
"templates/zine/manifest.yaml": abs(filePath(manifest_default2)),
|
|
1412
|
+
"templates/zine/chapter-01.md": abs(filePath(chapter_01_default2)),
|
|
1413
|
+
"templates/technical/manifest.yaml": abs(filePath(manifest_default3)),
|
|
1414
|
+
"templates/technical/chapter-01.md": abs(filePath(chapter_01_default3)),
|
|
1415
|
+
"themes/clean-book/theme.css": abs(filePath(theme_default)),
|
|
1416
|
+
"themes/clean-book/theme.json": abs(filePath(theme_default2)),
|
|
1417
|
+
"themes/zine/theme.css": abs(filePath(theme_default3)),
|
|
1418
|
+
"themes/zine/theme.json": abs(filePath(theme_default4)),
|
|
1419
|
+
"themes/technical-doc/theme.css": abs(filePath(theme_default5)),
|
|
1420
|
+
"themes/technical-doc/theme.json": abs(filePath(theme_default6))
|
|
1421
|
+
};
|
|
1422
|
+
var extractPromise = null;
|
|
1423
|
+
async function extractAssets() {
|
|
1424
|
+
const root = await mkdtemp(join2(tmpdir2(), "gutterpress-assets-"));
|
|
1425
|
+
for (const [relPath, srcPath] of Object.entries(EMBEDDED_ASSETS)) {
|
|
1426
|
+
const dest = join2(root, relPath);
|
|
1427
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
1428
|
+
const bytes = await readFile(srcPath);
|
|
1429
|
+
await writeFile(dest, bytes);
|
|
1430
|
+
}
|
|
1431
|
+
return root;
|
|
1432
|
+
}
|
|
1433
|
+
var SENTINEL_ASSET = "engine/gutterpress-viewer.js";
|
|
1434
|
+
async function getAssetsDir() {
|
|
1435
|
+
if (extractPromise) {
|
|
1436
|
+
try {
|
|
1437
|
+
const root = await extractPromise;
|
|
1438
|
+
if (existsSync2(join2(root, SENTINEL_ASSET))) {
|
|
1439
|
+
return root;
|
|
1440
|
+
}
|
|
1441
|
+
} catch {}
|
|
1442
|
+
extractPromise = null;
|
|
1443
|
+
}
|
|
1444
|
+
extractPromise = extractAssets();
|
|
1445
|
+
return extractPromise;
|
|
1446
|
+
}
|
|
1447
|
+
async function getAssetPath(relPath) {
|
|
1448
|
+
const root = await getAssetsDir();
|
|
1449
|
+
return join2(root, relPath);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/engine/compiler/tier2.ts
|
|
1453
|
+
var DEFAULT_SLUG_PT = 18;
|
|
1454
|
+
function classify(model) {
|
|
1455
|
+
const tier2Strings = [];
|
|
1456
|
+
const tier3Strings = [];
|
|
1457
|
+
const tier3Reasons = [];
|
|
1458
|
+
const consumed = consumedStrings(model);
|
|
1459
|
+
for (const decl of model.stringSets) {
|
|
1460
|
+
if (!consumed.has(decl.name))
|
|
1461
|
+
continue;
|
|
1462
|
+
if (startsRun(model, decl.selector))
|
|
1463
|
+
tier2Strings.push(decl.name);
|
|
1464
|
+
else {
|
|
1465
|
+
tier3Strings.push(decl.name);
|
|
1466
|
+
tier3Reasons.push(`string "${decl.name}" is set by \`${decl.selector}\` outside a named-page run — page granularity requires measurement`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
for (const x of model.xrefs) {
|
|
1470
|
+
if (needsMeasurement(x.content))
|
|
1471
|
+
tier3Reasons.push(`\`${x.selector}\` uses ${x.fn}() — requires measurement`);
|
|
1472
|
+
}
|
|
1473
|
+
return { tier2Strings, tier3Strings, tier3Reasons };
|
|
1474
|
+
}
|
|
1475
|
+
function pseudoVariants(model) {
|
|
1476
|
+
const set = new Set;
|
|
1477
|
+
for (const rule of model.pageRules)
|
|
1478
|
+
for (const p of rule.pseudos)
|
|
1479
|
+
set.add(p);
|
|
1480
|
+
return [...set];
|
|
1481
|
+
}
|
|
1482
|
+
function consumedStrings(model) {
|
|
1483
|
+
const names = new Set;
|
|
1484
|
+
for (const rule of model.pageRules)
|
|
1485
|
+
for (const decls of Object.values(rule.marginBoxes))
|
|
1486
|
+
if (decls.content) {
|
|
1487
|
+
for (const part of parseContent(decls.content))
|
|
1488
|
+
if (part.type === "string")
|
|
1489
|
+
names.add(part.name);
|
|
1490
|
+
}
|
|
1491
|
+
return names;
|
|
1492
|
+
}
|
|
1493
|
+
function startsRun(model, selector) {
|
|
1494
|
+
const sel = selector.trim();
|
|
1495
|
+
if (model.pageAssignments.some((a) => a.selector.trim() === sel))
|
|
1496
|
+
return true;
|
|
1497
|
+
return model.breaks.some((b) => b.selector.trim() === sel && b.prop === "break-before" && /^(page|always|left|right|recto|verso)$/.test(b.value.trim()));
|
|
1498
|
+
}
|
|
1499
|
+
function pageGeometry(model, opts) {
|
|
1500
|
+
const base = resolvePage(model);
|
|
1501
|
+
const trim = { width: base.geometry.width, height: base.geometry.height };
|
|
1502
|
+
const bleed = opts.bleedPt ?? base.geometry.bleed;
|
|
1503
|
+
const wantsMarks = base.geometry.marks.length > 0 || opts.marks === true;
|
|
1504
|
+
const slug = wantsMarks ? opts.slugPt ?? DEFAULT_SLUG_PT : 0;
|
|
1505
|
+
return {
|
|
1506
|
+
trim,
|
|
1507
|
+
bleed,
|
|
1508
|
+
slug,
|
|
1509
|
+
marks: base.geometry.marks.length ? base.geometry.marks : wantsMarks ? ["crop"] : [],
|
|
1510
|
+
media: {
|
|
1511
|
+
width: trim.width + 2 * (bleed + slug),
|
|
1512
|
+
height: trim.height + 2 * (bleed + slug)
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
var declsToCss = (d, indent = " ") => Object.entries(d).map(([k, v]) => `${indent}${k}: ${v};`).join(`
|
|
1517
|
+
`);
|
|
1518
|
+
function bleedMargin(authored, g) {
|
|
1519
|
+
return authored === 0 ? g.slug : authored + g.bleed + g.slug;
|
|
1520
|
+
}
|
|
1521
|
+
function marginBoxInset(name, inset) {
|
|
1522
|
+
if (!inset)
|
|
1523
|
+
return {};
|
|
1524
|
+
if (name.startsWith("@top"))
|
|
1525
|
+
return { "padding-top": `${inset}pt` };
|
|
1526
|
+
if (name.startsWith("@bottom"))
|
|
1527
|
+
return { "padding-bottom": `${inset}pt` };
|
|
1528
|
+
if (name.startsWith("@left"))
|
|
1529
|
+
return { "padding-left": `${inset}pt` };
|
|
1530
|
+
if (name.startsWith("@right"))
|
|
1531
|
+
return { "padding-right": `${inset}pt` };
|
|
1532
|
+
return {};
|
|
1533
|
+
}
|
|
1534
|
+
function synthesize(input) {
|
|
1535
|
+
const { model } = input;
|
|
1536
|
+
const geometry = pageGeometry(model, input);
|
|
1537
|
+
const inset = geometry.bleed + geometry.slug;
|
|
1538
|
+
const { tier3Reasons } = classify(model);
|
|
1539
|
+
const notes = [];
|
|
1540
|
+
const out = [
|
|
1541
|
+
"/* gp.gen.css — generated by the Gutterpress compiler (Tier 2).",
|
|
1542
|
+
" Derived from the author's own @page/GCPM declarations; the author's",
|
|
1543
|
+
" files are never modified. Inspect freely, do not edit. */",
|
|
1544
|
+
""
|
|
1545
|
+
];
|
|
1546
|
+
if (inset > 0) {
|
|
1547
|
+
const names = [undefined, ...model.pageNames];
|
|
1548
|
+
const variants = [[], ...pseudoVariants(model).map((p) => [p])];
|
|
1549
|
+
for (const name of names) {
|
|
1550
|
+
for (const pseudos of variants) {
|
|
1551
|
+
const isBase = name === undefined && pseudos.length === 0;
|
|
1552
|
+
const authorWroteIt = model.pageRules.some((r) => r.name === name && r.pseudos.length === pseudos.length && r.pseudos.every((p) => pseudos.includes(p)));
|
|
1553
|
+
if (!isBase && !authorWroteIt)
|
|
1554
|
+
continue;
|
|
1555
|
+
const resolved = resolvePage(model, { name, pseudos });
|
|
1556
|
+
const pseudo = pseudos.length ? `:${pseudos.join(":")}` : "";
|
|
1557
|
+
const lines = [
|
|
1558
|
+
` size: ${round(geometry.media.width)}pt ${round(geometry.media.height)}pt;`
|
|
1559
|
+
];
|
|
1560
|
+
for (const side of ["top", "right", "bottom", "left"]) {
|
|
1561
|
+
lines.push(` margin-${side}: ${round(bleedMargin(resolved.geometry.margin[side], geometry))}pt;`);
|
|
1562
|
+
}
|
|
1563
|
+
out.push(`@page ${name ?? ""}${pseudo} {`, ...lines, `}`.replace("@page ", "@page "));
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
const seen = new Set;
|
|
1567
|
+
for (const rule of model.pageRules)
|
|
1568
|
+
for (const name of Object.keys(rule.marginBoxes))
|
|
1569
|
+
seen.add(name);
|
|
1570
|
+
for (const name of seen) {
|
|
1571
|
+
const d = marginBoxInset(name, inset);
|
|
1572
|
+
if (!Object.keys(d).length)
|
|
1573
|
+
continue;
|
|
1574
|
+
out.push(`@page {`, ` ${name} {`, declsToCss(d, " "), ` }`, `}`);
|
|
1575
|
+
}
|
|
1576
|
+
notes.push(`bleed ${round(geometry.bleed)}pt + slug ${round(geometry.slug)}pt: media ${round(geometry.media.width)}×${round(geometry.media.height)}pt, trim ${round(geometry.trim.width)}×${round(geometry.trim.height)}pt`);
|
|
1577
|
+
if (geometry.bleed > 0)
|
|
1578
|
+
notes.push("Chromium clips page content to the content box, so art can only reach the bleed area on " + "pages whose authored margin is 0 (covers, plates) — those keep a slug-only margin so the " + "content box IS the bleed box. On pages with real margins, bleed is geometry only.");
|
|
1579
|
+
}
|
|
1580
|
+
return {
|
|
1581
|
+
css: out.join(`
|
|
1582
|
+
`) + `
|
|
1583
|
+
`,
|
|
1584
|
+
geometry,
|
|
1585
|
+
needsTier3: tier3Reasons.length > 0,
|
|
1586
|
+
notes: [...notes, ...tier3Reasons]
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function round(n) {
|
|
1590
|
+
return Math.round(n * 1000) / 1000;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/engine/compiler/postprocess.ts
|
|
1594
|
+
import { PDFDocument as PDFDocument2, PDFName as PDFName2, PDFNumber as PDFNumber2, rgb } from "pdf-lib";
|
|
1595
|
+
async function postprocess(input, opts) {
|
|
1596
|
+
const doc = await PDFDocument2.load(input, { updateMetadata: false });
|
|
1597
|
+
const g = opts.geometry;
|
|
1598
|
+
const inset = g.bleed + g.slug;
|
|
1599
|
+
const media = [0, 0, g.media.width, g.media.height];
|
|
1600
|
+
const bleedBox = [
|
|
1601
|
+
g.slug,
|
|
1602
|
+
g.slug,
|
|
1603
|
+
g.media.width - g.slug,
|
|
1604
|
+
g.media.height - g.slug
|
|
1605
|
+
];
|
|
1606
|
+
const trimBox = [
|
|
1607
|
+
inset,
|
|
1608
|
+
inset,
|
|
1609
|
+
g.media.width - inset,
|
|
1610
|
+
g.media.height - inset
|
|
1611
|
+
];
|
|
1612
|
+
let padded = 0;
|
|
1613
|
+
if (opts.signature && opts.signature > 1) {
|
|
1614
|
+
const remainder = doc.getPageCount() % opts.signature;
|
|
1615
|
+
if (remainder !== 0) {
|
|
1616
|
+
padded = opts.signature - remainder;
|
|
1617
|
+
for (let i = 0;i < padded; i++)
|
|
1618
|
+
doc.addPage([g.media.width, g.media.height]);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
const wantMarks = opts.marks || g.marks.includes("crop") || g.marks.includes("cross");
|
|
1622
|
+
for (const page of doc.getPages()) {
|
|
1623
|
+
const node = page.node;
|
|
1624
|
+
const set = (name, box) => node.set(PDFName2.of(name), doc.context.obj(box.map((v) => PDFNumber2.of(round2(v)))));
|
|
1625
|
+
set("MediaBox", media);
|
|
1626
|
+
set("CropBox", media);
|
|
1627
|
+
if (g.bleed > 0 || g.slug > 0) {
|
|
1628
|
+
set("BleedBox", bleedBox);
|
|
1629
|
+
set("TrimBox", trimBox);
|
|
1630
|
+
} else {
|
|
1631
|
+
set("TrimBox", media);
|
|
1632
|
+
}
|
|
1633
|
+
if (wantMarks && g.slug > 0)
|
|
1634
|
+
drawCropMarks(page, g);
|
|
1635
|
+
}
|
|
1636
|
+
if (opts.title)
|
|
1637
|
+
doc.setTitle(opts.title);
|
|
1638
|
+
if (opts.author)
|
|
1639
|
+
doc.setAuthor(opts.author);
|
|
1640
|
+
if (opts.subject)
|
|
1641
|
+
doc.setSubject(opts.subject);
|
|
1642
|
+
if (opts.keywords?.length)
|
|
1643
|
+
doc.setKeywords(opts.keywords);
|
|
1644
|
+
const bytes = await doc.save({ useObjectStreams: true });
|
|
1645
|
+
return {
|
|
1646
|
+
bytes,
|
|
1647
|
+
pageCount: doc.getPageCount(),
|
|
1648
|
+
padded,
|
|
1649
|
+
boxes: { media, bleed: bleedBox, trim: trimBox }
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
function drawCropMarks(page, g) {
|
|
1653
|
+
const inset = g.bleed + g.slug;
|
|
1654
|
+
const len = Math.min(g.slug, 18);
|
|
1655
|
+
const gap = g.bleed;
|
|
1656
|
+
const w = g.media.width;
|
|
1657
|
+
const h = g.media.height;
|
|
1658
|
+
const color = rgb(0, 0, 0);
|
|
1659
|
+
const thickness = 0.25;
|
|
1660
|
+
const line = (x, y, dx, dy) => page.drawLine({
|
|
1661
|
+
start: { x, y },
|
|
1662
|
+
end: { x: x + dx, y: y + dy },
|
|
1663
|
+
thickness,
|
|
1664
|
+
color
|
|
1665
|
+
});
|
|
1666
|
+
const xs = [inset, w - inset];
|
|
1667
|
+
const ys = [inset, h - inset];
|
|
1668
|
+
for (const x of xs) {
|
|
1669
|
+
for (const y of ys) {
|
|
1670
|
+
const sx = x === inset ? -1 : 1;
|
|
1671
|
+
const sy = y === inset ? -1 : 1;
|
|
1672
|
+
line(x + sx * gap, y, sx * len, 0);
|
|
1673
|
+
line(x, y + sy * gap, 0, sy * len);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function round2(n) {
|
|
1678
|
+
return Math.round(n * 1000) / 1000;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/engine/compiler/build.ts
|
|
1682
|
+
var BLANK_PAGE = "gp--blank";
|
|
1683
|
+
var DESC_JS = `(el) => {
|
|
1684
|
+
const cls = typeof el.className === "string" && el.className
|
|
1685
|
+
? "." + el.className.trim().split(/\\s+/).join(".") : "";
|
|
1686
|
+
const src = el.tagName === "IMG" ? " src=" + (el.getAttribute("src") || "").slice(-40) : "";
|
|
1687
|
+
return el.tagName.toLowerCase() + cls + src;
|
|
1688
|
+
}`;
|
|
1689
|
+
function pagePseudoContexts(model) {
|
|
1690
|
+
const used = new Set(pseudoVariants(model));
|
|
1691
|
+
const standard = new Set(["left", "right", "recto", "verso", "first", "blank"]);
|
|
1692
|
+
const choices = (a, b) => [
|
|
1693
|
+
undefined,
|
|
1694
|
+
...used.has(a) ? [a] : [],
|
|
1695
|
+
...used.has(b) ? [b] : []
|
|
1696
|
+
];
|
|
1697
|
+
const physical = choices("left", "right");
|
|
1698
|
+
const progression = choices("recto", "verso");
|
|
1699
|
+
const first = used.has("first") ? [false, true] : [false];
|
|
1700
|
+
const blank = used.has("blank") ? [false, true] : [false];
|
|
1701
|
+
const standardContexts = [];
|
|
1702
|
+
for (const p of physical)
|
|
1703
|
+
for (const r of progression)
|
|
1704
|
+
for (const f of first)
|
|
1705
|
+
for (const b of blank)
|
|
1706
|
+
standardContexts.push([
|
|
1707
|
+
...p ? [p] : [],
|
|
1708
|
+
...r ? [r] : [],
|
|
1709
|
+
...f ? ["first"] : [],
|
|
1710
|
+
...b ? ["blank"] : []
|
|
1711
|
+
]);
|
|
1712
|
+
const authoredExactSets = [
|
|
1713
|
+
[],
|
|
1714
|
+
...model.pageRules.map((rule) => rule.pseudos.filter((pseudo) => !standard.has(pseudo)))
|
|
1715
|
+
];
|
|
1716
|
+
const contexts = new Map;
|
|
1717
|
+
for (const exact of authoredExactSets) {
|
|
1718
|
+
for (const common of standardContexts) {
|
|
1719
|
+
const context = [...new Set([...exact, ...common])].sort();
|
|
1720
|
+
contexts.set(context.join("\x00"), context);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return [...contexts.values()];
|
|
1724
|
+
}
|
|
1725
|
+
async function build(opts) {
|
|
1726
|
+
const log = opts.onProgress ?? (() => {});
|
|
1727
|
+
const AGENT = await readFile2(await getAssetPath("engine/gutterpress-agent.js"), "utf8");
|
|
1728
|
+
const VIEWER = await readFile2(await getAssetPath("engine/gutterpress-viewer.js"), "utf8");
|
|
1729
|
+
const browser = opts.browser ?? await launchChromium();
|
|
1730
|
+
const ownsBrowser = !opts.browser;
|
|
1731
|
+
const page = await browser.newPage();
|
|
1732
|
+
const notes = [];
|
|
1733
|
+
const diagnostics = [];
|
|
1734
|
+
const diagnose = (code, message, severity = "warning") => diagnostics.push({ code, severity, message });
|
|
1735
|
+
let prints = 0;
|
|
1736
|
+
try {
|
|
1737
|
+
const url = /^(https?|file):\/\//.test(opts.input) ? opts.input : pathToFileURL(resolve2(opts.input)).href;
|
|
1738
|
+
await page.navigate(url);
|
|
1739
|
+
await page.evaluate(AGENT);
|
|
1740
|
+
await page.waitForReady();
|
|
1741
|
+
const cssText = await page.evaluate(`window.__gp.collectCss()`);
|
|
1742
|
+
const model = extract(cssText);
|
|
1743
|
+
const { tier3Reasons } = classify(model);
|
|
1744
|
+
const baseGeom = resolvePage(model).geometry;
|
|
1745
|
+
const sheetViewport = {
|
|
1746
|
+
width: Math.max(1, Math.round(baseGeom.width * 96 / 72)),
|
|
1747
|
+
height: Math.max(1, Math.round(baseGeom.height * 96 / 72)),
|
|
1748
|
+
deviceScaleFactor: 1,
|
|
1749
|
+
mobile: false
|
|
1750
|
+
};
|
|
1751
|
+
await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
|
|
1752
|
+
await page.send("Emulation.setEmulatedMedia", { media: "print" });
|
|
1753
|
+
log(`print media emulated for audits and measurement`);
|
|
1754
|
+
const tier2 = synthesize({
|
|
1755
|
+
model,
|
|
1756
|
+
marks: opts.marks,
|
|
1757
|
+
slugPt: opts.slugPt,
|
|
1758
|
+
bleedPt: opts.bleedPt
|
|
1759
|
+
});
|
|
1760
|
+
notes.push(...tier2.notes);
|
|
1761
|
+
let genCss = tier2.css;
|
|
1762
|
+
if (genCss.trim().length > 200) {
|
|
1763
|
+
await page.evaluate(`window.__gp.addCss("gp-gen-css", ${JSON.stringify(genCss)})`);
|
|
1764
|
+
log(`tier 2: bleed/marks geometry`);
|
|
1765
|
+
}
|
|
1766
|
+
const widthOffenders = await findWidthOffenders(page, model, 2 * (tier2.geometry.bleed + tier2.geometry.slug), sheetViewport);
|
|
1767
|
+
const describe = (list) => list.map((o) => {
|
|
1768
|
+
const fix = o.left !== undefined && o.left < -1 ? "keep it inside the page content box" : "give it an explicit width";
|
|
1769
|
+
return ` ${o.desc} — ${Math.round(o.px)}px > ${Math.round(widthOffenders.limitPx)}px content box (${fix})`;
|
|
1770
|
+
}).join(`
|
|
1771
|
+
`);
|
|
1772
|
+
if (widthOffenders.boxes.length) {
|
|
1773
|
+
const msg = `content wider than the page content box triggers Chromium print ` + `shrink-to-fit (the WHOLE book scales down, silently):
|
|
1774
|
+
${describe(widthOffenders.boxes)}`;
|
|
1775
|
+
if (opts.allowShrink) {
|
|
1776
|
+
for (const o of widthOffenders.boxes)
|
|
1777
|
+
diagnose("engine.width.overflow", `${o.desc} is wider than the page — Chromium shrinks the WHOLE book to fit it. ${o.left < -1 ? "Keep it inside the page content box." : "Give it an explicit width that fits."}`);
|
|
1778
|
+
log(`WARNING: ${msg}`);
|
|
1779
|
+
} else {
|
|
1780
|
+
throw new Error(`${msg}
|
|
1781
|
+
Fix the offending widths, or pass allowShrink to build anyway.`);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (widthOffenders.intrinsics.length) {
|
|
1785
|
+
const msg = `${widthOffenders.intrinsics.length} auto-width image(s) with an intrinsic width past the ` + `page content box — if the output prints smaller than the CSS declares, give these an ` + `explicit width (e.g. width: 100%):
|
|
1786
|
+
${describe(widthOffenders.intrinsics)}`;
|
|
1787
|
+
for (const o of widthOffenders.intrinsics)
|
|
1788
|
+
diagnose("engine.width.intrinsic", `${o.desc} has no width set and is naturally wider than the page — if the printed output comes out smaller than your CSS says, give it an explicit width (e.g. width: 100%).`);
|
|
1789
|
+
log(`WARNING: ${msg}`);
|
|
1790
|
+
}
|
|
1791
|
+
const rectoDecls = model.breaks.filter(isRectoVersoBreak);
|
|
1792
|
+
const needsMeasure = tier3Reasons.length > 0 || consumedStrings(model).size > 0 || rectoDecls.length > 0 || model.counterResets.length > 0;
|
|
1793
|
+
let tier = tier2.geometry.bleed > 0 || tier2.geometry.slug > 0 ? 2 : 1;
|
|
1794
|
+
let passes = 1;
|
|
1795
|
+
let pageMap = {};
|
|
1796
|
+
let converged = true;
|
|
1797
|
+
let bytes;
|
|
1798
|
+
let predictedForResult = null;
|
|
1799
|
+
let resetSitesForResult = [];
|
|
1800
|
+
if (needsMeasure) {
|
|
1801
|
+
tier = 3;
|
|
1802
|
+
const maxPasses = opts.maxPasses ?? 4;
|
|
1803
|
+
const sources = await page.evaluate(`window.__gp.stringSources(${JSON.stringify(model.stringSets.map((s) => ({ selector: s.selector, name: s.name, value: s.value })))})`);
|
|
1804
|
+
const rectoSites = rectoDecls.length ? await page.evaluate(`window.__gp.forcedBreakSites(${JSON.stringify(rectoDecls)})`) : [];
|
|
1805
|
+
const hasBlankSites = rectoSites.length > 0;
|
|
1806
|
+
const xrefSelectors = model.xrefs.filter((x) => needsMeasurement(x.content)).map((x) => x.selector);
|
|
1807
|
+
const sites = await page.evaluate(`window.__gp.xrefSites(${JSON.stringify(xrefSelectors)})`);
|
|
1808
|
+
const resetSites = model.counterResets.length ? await page.evaluate(`window.__gp.counterResetSites(${JSON.stringify(model.counterResets)})`) : [];
|
|
1809
|
+
resetSitesForResult = resetSites;
|
|
1810
|
+
const targets = new Set;
|
|
1811
|
+
for (const s of sources)
|
|
1812
|
+
targets.add(s.id);
|
|
1813
|
+
for (const s of sites)
|
|
1814
|
+
if (s.href.startsWith("#"))
|
|
1815
|
+
targets.add(s.href.slice(1));
|
|
1816
|
+
for (const s of rectoSites)
|
|
1817
|
+
targets.add(s.id);
|
|
1818
|
+
for (const s of resetSites)
|
|
1819
|
+
targets.add(s.id);
|
|
1820
|
+
await page.evaluate(`window.__gp.instrument(${JSON.stringify([...targets])})`);
|
|
1821
|
+
const targetText = await page.evaluate(`window.__gp.targetTexts(${JSON.stringify([...targets])})`);
|
|
1822
|
+
const brokenXrefs = findBrokenXrefRefs(sites, targetText);
|
|
1823
|
+
for (const href of brokenXrefs)
|
|
1824
|
+
diagnose("engine.xref.broken", `The link "${href}" doesn't point at anything in this book, so its page number will print blank. Check the spelling, or add that id to the heading you meant.`);
|
|
1825
|
+
if (brokenXrefs.length)
|
|
1826
|
+
log(`WARNING: ${brokenXrefs.length} broken cross-reference href(s): ${brokenXrefs.join(", ")}`);
|
|
1827
|
+
if (rectoSites.length) {
|
|
1828
|
+
const measure = async () => {
|
|
1829
|
+
prints++;
|
|
1830
|
+
const facts = await inspectPdf(await printPdf(page));
|
|
1831
|
+
return Object.fromEntries(Object.entries(facts.namedDests).map(([k, v]) => [k, v + 1]));
|
|
1832
|
+
};
|
|
1833
|
+
let map = await measure();
|
|
1834
|
+
const wrong = (site, p) => wantsRecto(site.value) ? p % 2 === 0 : p % 2 === 1;
|
|
1835
|
+
const plan = planRectoBlanks(rectoSites.map((site) => ({
|
|
1836
|
+
page: map[site.id] ?? 0,
|
|
1837
|
+
wantsRecto: wantsRecto(site.value)
|
|
1838
|
+
})));
|
|
1839
|
+
const planned = rectoSites.filter((_, i) => plan[i]).map((site) => site.id);
|
|
1840
|
+
if (planned.length) {
|
|
1841
|
+
const earlyBlankCss = counterStyleCss(model, [], {}, 0, [], true);
|
|
1842
|
+
if (earlyBlankCss) {
|
|
1843
|
+
await page.evaluate(`window.__gp.addCss("gp-gen-strings", ${JSON.stringify(earlyBlankCss)})`);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (planned.length) {
|
|
1847
|
+
await page.evaluate(`window.__gp.applyRectoSpacers(${JSON.stringify(planned)}, ${JSON.stringify(BLANK_PAGE)})`);
|
|
1848
|
+
log(`tier 3: ${planned.length} blank page(s) so forced breaks land on the right side`);
|
|
1849
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
1850
|
+
map = await measure();
|
|
1851
|
+
const bad = rectoSites.filter((site) => {
|
|
1852
|
+
const p = map[site.id];
|
|
1853
|
+
return p ? wrong(site, p) : false;
|
|
1854
|
+
});
|
|
1855
|
+
if (!bad.length)
|
|
1856
|
+
break;
|
|
1857
|
+
for (const site of bad) {
|
|
1858
|
+
const i = planned.indexOf(site.id);
|
|
1859
|
+
if (i >= 0)
|
|
1860
|
+
planned.splice(i, 1);
|
|
1861
|
+
else
|
|
1862
|
+
planned.push(site.id);
|
|
1863
|
+
}
|
|
1864
|
+
await page.evaluate(`window.__gp.applyRectoSpacers(${JSON.stringify(planned)}, ${JSON.stringify(BLANK_PAGE)})`);
|
|
1865
|
+
if (attempt === 2)
|
|
1866
|
+
notes.push(`Could not place ${bad.length} recto/verso break(s) after 3 attempts.`);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
const applySynthesis = async (map, pageCount) => {
|
|
1871
|
+
const pageValues = restartedPageValues(resetSites, map, pageCount);
|
|
1872
|
+
const generated = [];
|
|
1873
|
+
for (const site of sites) {
|
|
1874
|
+
const decl = model.xrefs.find((x) => x.selector === site.selector);
|
|
1875
|
+
if (!decl)
|
|
1876
|
+
continue;
|
|
1877
|
+
const where = /::?before$/.test(site.selector) ? "before" : "after";
|
|
1878
|
+
const text = evaluate(decl.content, {
|
|
1879
|
+
attr: (n) => n === "href" ? site.href : undefined,
|
|
1880
|
+
targetPage: (u) => {
|
|
1881
|
+
const physical = map[u.replace(/^#/, "")];
|
|
1882
|
+
return physical === undefined ? undefined : toFolioPage(physical, pageValues);
|
|
1883
|
+
},
|
|
1884
|
+
targetText: (u) => targetText[u.replace(/^#/, "")],
|
|
1885
|
+
leader: leaderMarker
|
|
1886
|
+
});
|
|
1887
|
+
generated.push({ id: site.id, where, text });
|
|
1888
|
+
}
|
|
1889
|
+
if (generated.length) {
|
|
1890
|
+
const contentCss = generatedContentCss(model.xrefs.map((x) => x.selector));
|
|
1891
|
+
await page.evaluate(`window.__gp.setGenerated(${JSON.stringify(generated)}, ${JSON.stringify(contentCss)})`);
|
|
1892
|
+
const g = tier2.geometry;
|
|
1893
|
+
const contentWidthPx = (g.trim.width - resolvePage(model).geometry.margin.left - resolvePage(model).geometry.margin.right) * 96 / 72;
|
|
1894
|
+
await page.evaluate(`window.__gp.fillLeaders(${contentWidthPx})`);
|
|
1895
|
+
}
|
|
1896
|
+
const mapCss = counterStyleCss(model, sources, map, pageCount, resetSites, hasBlankSites);
|
|
1897
|
+
if (mapCss) {
|
|
1898
|
+
await page.evaluate(`window.__gp.addCss("gp-gen-strings", ${JSON.stringify(mapCss)})`);
|
|
1899
|
+
genCss = genCss.split(`
|
|
1900
|
+
/* Tier 3 */`)[0] + `
|
|
1901
|
+
/* Tier 3 */
|
|
1902
|
+
${mapCss}`;
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
const predicted = await predictPageMap(browser, url, AGENT, VIEWER, {
|
|
1906
|
+
stringSets: model.stringSets.map((s) => ({ selector: s.selector, name: s.name, value: s.value })),
|
|
1907
|
+
rectoDecls,
|
|
1908
|
+
xrefSelectors,
|
|
1909
|
+
resets: model.counterResets,
|
|
1910
|
+
targets: [...targets]
|
|
1911
|
+
}, sheetViewport);
|
|
1912
|
+
if (predicted)
|
|
1913
|
+
predictedForResult = { pageMap: predicted.pageMap, pageCount: predicted.pageCount };
|
|
1914
|
+
let previous = "";
|
|
1915
|
+
if (predicted) {
|
|
1916
|
+
log(`tier 3: predicted page map from the viewer in ${predicted.ms.toFixed(0)}ms ` + `(${predicted.pageCount}pp, ${Object.keys(predicted.pageMap).length}/${targets.size} targets resolved)`);
|
|
1917
|
+
await applySynthesis(predicted.pageMap, predicted.pageCount);
|
|
1918
|
+
previous = mapSignature(predicted.pageMap, predicted.pageCount);
|
|
1919
|
+
}
|
|
1920
|
+
converged = false;
|
|
1921
|
+
for (let pass = 1;pass <= maxPasses; pass++) {
|
|
1922
|
+
passes = pass;
|
|
1923
|
+
prints++;
|
|
1924
|
+
bytes = await printPdf(page);
|
|
1925
|
+
const facts = await inspectPdf(bytes);
|
|
1926
|
+
pageMap = Object.fromEntries(Object.entries(facts.namedDests).filter(([k]) => targets.has(k)).map(([k, v]) => [k, v + 1]));
|
|
1927
|
+
log(`tier 3: pass ${pass} measured ${Object.keys(pageMap).length}/${targets.size} targets (${facts.pageCount}pp)`);
|
|
1928
|
+
const signature = mapSignature(pageMap, facts.pageCount);
|
|
1929
|
+
if (signature === previous) {
|
|
1930
|
+
converged = true;
|
|
1931
|
+
log(`tier 3: fixpoint after ${pass} pass${pass === 1 ? "" : "es"}` + (predicted && pass === 1 ? " — the predicted print shipped, no reprint" : ""));
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
previous = signature;
|
|
1935
|
+
await applySynthesis(pageMap, facts.pageCount);
|
|
1936
|
+
}
|
|
1937
|
+
if (!converged) {
|
|
1938
|
+
notes.push(`Tier 3 did not reach a fixpoint in ${passes} passes — page numbers in generated text may be one pass stale.`);
|
|
1939
|
+
log(`tier 3: NOT converged after ${passes} passes`);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
const pseudoContexts = pagePseudoContexts(model);
|
|
1943
|
+
const contentHeightPt = (name) => Math.min(...pseudoContexts.map((pseudos) => {
|
|
1944
|
+
const geometry = resolvePage(model, { name, pseudos }).geometry;
|
|
1945
|
+
return geometry.height - geometry.margin.top - geometry.margin.bottom;
|
|
1946
|
+
}));
|
|
1947
|
+
const contentHeightPx = contentHeightPt() * 96 / 72;
|
|
1948
|
+
const namedContentHeightsPx = Object.fromEntries(model.pageNames.map((name) => [name, contentHeightPt(name) * 96 / 72]));
|
|
1949
|
+
const minContentHeightPx = Math.min(contentHeightPt(), ...model.pageNames.map((name) => contentHeightPt(name))) * 96 / 72;
|
|
1950
|
+
{
|
|
1951
|
+
const audit = await page.evaluate(`window.__gp.auditContent(${JSON.stringify({
|
|
1952
|
+
default: contentHeightPx,
|
|
1953
|
+
named: namedContentHeightsPx
|
|
1954
|
+
})}, ${opts.dpiFloor ?? 300})`);
|
|
1955
|
+
for (const w of audit) {
|
|
1956
|
+
if (w.kind === "overheight")
|
|
1957
|
+
diagnose("engine.content.overheight", `${w.what} is taller than one page (${w.detail}): print splits it across pages while the on-screen preview clips it, so the two will not agree here.`);
|
|
1958
|
+
else
|
|
1959
|
+
diagnose("engine.image.low-dpi", `${w.what} is below the ${opts.dpiFloor ?? 300} DPI print bar (${w.detail}) — it may look soft or pixelated in print.`);
|
|
1960
|
+
}
|
|
1961
|
+
if (audit.length)
|
|
1962
|
+
log(`audit: ${audit.length} print-quality warning(s)`);
|
|
1963
|
+
}
|
|
1964
|
+
{
|
|
1965
|
+
const { leaks, multicol, layerTraps } = JSON.parse(await page.evaluate(`(() => {
|
|
1966
|
+
const desc = ${DESC_JS};
|
|
1967
|
+
const leaks = [];
|
|
1968
|
+
const multicol = [];
|
|
1969
|
+
const layerTraps = [];
|
|
1970
|
+
const seenLayerTraps = new Set();
|
|
1971
|
+
|
|
1972
|
+
const stackingReasons = (el, cs) => {
|
|
1973
|
+
const reasons = [];
|
|
1974
|
+
const positioned = cs.position !== "static";
|
|
1975
|
+
const parentDisplay = el.parentElement
|
|
1976
|
+
? getComputedStyle(el.parentElement).display
|
|
1977
|
+
: "";
|
|
1978
|
+
const flexOrGridItem = /^(inline-)?(flex|grid)$/.test(parentDisplay);
|
|
1979
|
+
if (cs.isolation === "isolate") reasons.push("isolation: isolate");
|
|
1980
|
+
if (Number.parseFloat(cs.opacity) < 1) reasons.push("opacity: " + cs.opacity);
|
|
1981
|
+
if (cs.mixBlendMode !== "normal") reasons.push("mix-blend-mode: " + cs.mixBlendMode);
|
|
1982
|
+
if (cs.position === "fixed" || cs.position === "sticky")
|
|
1983
|
+
reasons.push("position: " + cs.position);
|
|
1984
|
+
if (cs.zIndex !== "auto" && (positioned || flexOrGridItem))
|
|
1985
|
+
reasons.push("z-index: " + cs.zIndex);
|
|
1986
|
+
for (const prop of [
|
|
1987
|
+
"transform", "scale", "rotate", "translate", "filter",
|
|
1988
|
+
"backdropFilter", "perspective", "clipPath", "mask", "maskImage",
|
|
1989
|
+
]) {
|
|
1990
|
+
const value = cs[prop];
|
|
1991
|
+
if (value && value !== "none")
|
|
1992
|
+
reasons.push(prop.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()) + ": " + value);
|
|
1993
|
+
}
|
|
1994
|
+
if (/\\b(layout|paint|strict|content)\\b/.test(cs.contain))
|
|
1995
|
+
reasons.push("contain: " + cs.contain);
|
|
1996
|
+
if (cs.containerType && cs.containerType !== "normal")
|
|
1997
|
+
reasons.push("container-type: " + cs.containerType);
|
|
1998
|
+
if (/\\b(transform|opacity|filter|perspective|clip-path|mask)\\b/.test(cs.willChange))
|
|
1999
|
+
reasons.push("will-change: " + cs.willChange);
|
|
2000
|
+
return reasons;
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
for (const el of document.querySelectorAll("*")) {
|
|
2004
|
+
const cs = getComputedStyle(el);
|
|
2005
|
+
if (leaks.length < 20) {
|
|
2006
|
+
const tag = el.tagName;
|
|
2007
|
+
const id = el.id || "";
|
|
2008
|
+
const cls = typeof el.className === "string" ? el.className : "";
|
|
2009
|
+
if (
|
|
2010
|
+
tag !== "GP-ANCHOR" &&
|
|
2011
|
+
!id.startsWith("gp-") &&
|
|
2012
|
+
!id.startsWith("__gp") &&
|
|
2013
|
+
// gp-* CLASSES are author-facing vocabulary (MARKER_CSS's
|
|
2014
|
+
// .gp-pin is abspos by design and must stay visible to this
|
|
2015
|
+
// leak check); only the engine's own print-document class is
|
|
2016
|
+
// excluded. Engine-internal DOM identifies itself via ids or
|
|
2017
|
+
// __gp classes, never bare gp-* classes.
|
|
2018
|
+
!/(^|\\s)(gp-recto-spacer|__gp)/.test(cls) &&
|
|
2019
|
+
!el.closest("#gp-instrumentation") &&
|
|
2020
|
+
cs.position === "absolute" &&
|
|
2021
|
+
el.getClientRects().length !== 0 // not a display:none subtree
|
|
2022
|
+
) {
|
|
2023
|
+
const op = el.offsetParent;
|
|
2024
|
+
if (op === null || op === document.body) leaks.push(desc(el));
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
if (
|
|
2028
|
+
multicol.length < 20 &&
|
|
2029
|
+
!(cs.columnCount === "auto" && cs.columnWidth === "auto") &&
|
|
2030
|
+
cs.columnFill === "balance" &&
|
|
2031
|
+
el.getBoundingClientRect().height > ${minContentHeightPx} + 1
|
|
2032
|
+
) {
|
|
2033
|
+
multicol.push(desc(el));
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
if (
|
|
2037
|
+
layerTraps.length < 20 &&
|
|
2038
|
+
el.classList.contains("gp-behind") &&
|
|
2039
|
+
el.getClientRects().length !== 0
|
|
2040
|
+
) {
|
|
2041
|
+
const boundary = el.closest(".page, .spread");
|
|
2042
|
+
if (boundary) {
|
|
2043
|
+
for (let ancestor = el.parentElement; ancestor; ancestor = ancestor.parentElement) {
|
|
2044
|
+
const ancestorStyle = getComputedStyle(ancestor);
|
|
2045
|
+
const reasons = stackingReasons(ancestor, ancestorStyle);
|
|
2046
|
+
const clips = ancestorStyle.overflowX !== "visible" ||
|
|
2047
|
+
ancestorStyle.overflowY !== "visible";
|
|
2048
|
+
if ((reasons.length || clips) && !seenLayerTraps.has(ancestor)) {
|
|
2049
|
+
seenLayerTraps.add(ancestor);
|
|
2050
|
+
const effects = [];
|
|
2051
|
+
if (reasons.length)
|
|
2052
|
+
effects.push("creates a stacking context (" + reasons.join(", ") + ")");
|
|
2053
|
+
if (clips)
|
|
2054
|
+
effects.push(
|
|
2055
|
+
"clips descendants (overflow-x: " + ancestorStyle.overflowX +
|
|
2056
|
+
", overflow-y: " + ancestorStyle.overflowY + ")"
|
|
2057
|
+
);
|
|
2058
|
+
layerTraps.push({
|
|
2059
|
+
behind: desc(el),
|
|
2060
|
+
ancestor: desc(ancestor),
|
|
2061
|
+
detail: effects.join(" and "),
|
|
2062
|
+
});
|
|
2063
|
+
if (layerTraps.length >= 20) break;
|
|
2064
|
+
}
|
|
2065
|
+
if (ancestor === boundary) break;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return JSON.stringify({ leaks, multicol, layerTraps });
|
|
2071
|
+
})()`));
|
|
2072
|
+
for (const d of leaks)
|
|
2073
|
+
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.`);
|
|
2074
|
+
if (leaks.length)
|
|
2075
|
+
log(`audit: ${leaks.length} abspos containing-block leak(s)`);
|
|
2076
|
+
for (const trap of layerTraps)
|
|
2077
|
+
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.`);
|
|
2078
|
+
if (layerTraps.length)
|
|
2079
|
+
log(`audit: ${layerTraps.length} trapped layer warning(s)`);
|
|
2080
|
+
for (const d of multicol)
|
|
2081
|
+
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}.`);
|
|
2082
|
+
if (multicol.length)
|
|
2083
|
+
log(`audit: ${multicol.length} fragmenting multicol warning(s)`);
|
|
2084
|
+
}
|
|
2085
|
+
if (bytes === undefined) {
|
|
2086
|
+
prints++;
|
|
2087
|
+
bytes = await printPdf(page);
|
|
2088
|
+
}
|
|
2089
|
+
const post = await postprocess(bytes, {
|
|
2090
|
+
geometry: tier2.geometry,
|
|
2091
|
+
signature: opts.signature,
|
|
2092
|
+
marks: opts.marks,
|
|
2093
|
+
title: opts.title,
|
|
2094
|
+
author: opts.author
|
|
2095
|
+
});
|
|
2096
|
+
log(`postprocess: ${post.pageCount} pages` + (post.padded ? ` (+${post.padded} signature pad)` : ""));
|
|
2097
|
+
return {
|
|
2098
|
+
bytes: post.bytes,
|
|
2099
|
+
tier,
|
|
2100
|
+
passes,
|
|
2101
|
+
pageCount: post.pageCount,
|
|
2102
|
+
genCss,
|
|
2103
|
+
geometry: tier2.geometry,
|
|
2104
|
+
notes,
|
|
2105
|
+
diagnostics,
|
|
2106
|
+
post,
|
|
2107
|
+
pageMap,
|
|
2108
|
+
converged,
|
|
2109
|
+
prints,
|
|
2110
|
+
predicted: predictedForResult,
|
|
2111
|
+
resetSites: resetSitesForResult,
|
|
2112
|
+
viewport: { width: sheetViewport.width, height: sheetViewport.height }
|
|
2113
|
+
};
|
|
2114
|
+
} finally {
|
|
2115
|
+
await page.close();
|
|
2116
|
+
if (ownsBrowser)
|
|
2117
|
+
await browser.close();
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
async function printPdf(page) {
|
|
2121
|
+
await page.waitForReady();
|
|
2122
|
+
return page.printToPDF();
|
|
2123
|
+
}
|
|
2124
|
+
async function findWidthOffenders(page, model, bleedSlugExtensionPt, restoreViewport) {
|
|
2125
|
+
const contexts = [
|
|
2126
|
+
resolvePage(model),
|
|
2127
|
+
...model.pageNames.filter((n) => !n.startsWith("gp-")).map((n) => resolvePage(model, { name: n }))
|
|
2128
|
+
];
|
|
2129
|
+
const maxContentPt = Math.max(...contexts.map((c) => c.geometry.width - c.geometry.margin.left - c.geometry.margin.right)) + bleedSlugExtensionPt;
|
|
2130
|
+
const limitPx = maxContentPt * 96 / 72;
|
|
2131
|
+
await page.evaluate(`Promise.allSettled(
|
|
2132
|
+
[...document.images].map((i) => i.decode().catch(() => {}))
|
|
2133
|
+
)`);
|
|
2134
|
+
try {
|
|
2135
|
+
await page.send("Emulation.setDeviceMetricsOverride", {
|
|
2136
|
+
width: Math.ceil(limitPx),
|
|
2137
|
+
height: 1080,
|
|
2138
|
+
deviceScaleFactor: 1,
|
|
2139
|
+
mobile: false
|
|
2140
|
+
});
|
|
2141
|
+
const boxes = JSON.parse(await page.evaluate(`(() => {
|
|
2142
|
+
const LIMIT = ${limitPx} + 1;
|
|
2143
|
+
const desc = ${DESC_JS};
|
|
2144
|
+
const out = [];
|
|
2145
|
+
for (const el of document.querySelectorAll("*")) {
|
|
2146
|
+
const r = el.getBoundingClientRect();
|
|
2147
|
+
const right = Math.max(r.right, r.left + r.width);
|
|
2148
|
+
// Flag right-edge overflow (fixed-width blocks, negative-margin
|
|
2149
|
+
// pulls) AND left-edge protrusion (negative left with an ordinary
|
|
2150
|
+
// width) — both trigger the same whole-document shrink-to-fit,
|
|
2151
|
+
// measured: a left bleed alone cost 16%.
|
|
2152
|
+
if (right <= LIMIT && r.width <= LIMIT && r.left >= -1) continue;
|
|
2153
|
+
let deepest = true;
|
|
2154
|
+
for (const c of el.children) {
|
|
2155
|
+
if (c.getBoundingClientRect().width >= r.width - 1) { deepest = false; break; }
|
|
2156
|
+
}
|
|
2157
|
+
if (!deepest) continue;
|
|
2158
|
+
out.push({ desc: desc(el), px: Math.max(r.width, right), left: r.left });
|
|
2159
|
+
if (out.length >= 20) break;
|
|
2160
|
+
}
|
|
2161
|
+
return JSON.stringify(out);
|
|
2162
|
+
})()`));
|
|
2163
|
+
const replaced = JSON.parse(await page.evaluate(`(() => {
|
|
2164
|
+
const LIMIT = ${limitPx} + 1;
|
|
2165
|
+
const desc = ${DESC_JS};
|
|
2166
|
+
const out = [];
|
|
2167
|
+
for (const el of document.querySelectorAll("img, canvas, video")) {
|
|
2168
|
+
const intrinsic = el.naturalWidth ?? el.videoWidth ?? el.width ?? 0;
|
|
2169
|
+
if (!intrinsic || intrinsic <= LIMIT) continue;
|
|
2170
|
+
let widthIsAuto = true;
|
|
2171
|
+
try {
|
|
2172
|
+
widthIsAuto = String(el.computedStyleMap().get("width")) === "auto";
|
|
2173
|
+
} catch (_) { /* Typed OM unavailable: keep the conservative flag */ }
|
|
2174
|
+
if (!widthIsAuto) continue;
|
|
2175
|
+
out.push({ desc: desc(el), px: intrinsic });
|
|
2176
|
+
if (out.length >= 20) break;
|
|
2177
|
+
}
|
|
2178
|
+
return JSON.stringify(out);
|
|
2179
|
+
})()`));
|
|
2180
|
+
return { limitPx, boxes, intrinsics: replaced };
|
|
2181
|
+
} finally {
|
|
2182
|
+
await page.send("Emulation.setDeviceMetricsOverride", restoreViewport);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function findBrokenXrefRefs(sites, resolved) {
|
|
2186
|
+
const seen = new Set;
|
|
2187
|
+
const out = [];
|
|
2188
|
+
for (const s of sites) {
|
|
2189
|
+
if (!s.href.startsWith("#"))
|
|
2190
|
+
continue;
|
|
2191
|
+
if (s.href.slice(1) in resolved)
|
|
2192
|
+
continue;
|
|
2193
|
+
if (seen.has(s.href))
|
|
2194
|
+
continue;
|
|
2195
|
+
seen.add(s.href);
|
|
2196
|
+
out.push(s.href);
|
|
2197
|
+
}
|
|
2198
|
+
return out;
|
|
2199
|
+
}
|
|
2200
|
+
function mapSignature(map, pageCount) {
|
|
2201
|
+
return Object.keys(map).sort().map((k) => `${k}=${map[k]}`).join("|") + `|pageCount=${pageCount}`;
|
|
2202
|
+
}
|
|
2203
|
+
async function predictPageMap(browser, url, agentScript, viewerScript, args, sheetViewport) {
|
|
2204
|
+
const t0 = performance.now();
|
|
2205
|
+
let page;
|
|
2206
|
+
try {
|
|
2207
|
+
page = await browser.newPage();
|
|
2208
|
+
await page.send("Emulation.setDeviceMetricsOverride", sheetViewport);
|
|
2209
|
+
await page.navigate(url);
|
|
2210
|
+
await page.evaluate(agentScript);
|
|
2211
|
+
await page.waitForReady();
|
|
2212
|
+
await page.evaluate(`window.__GP_MANUAL__ = true;`);
|
|
2213
|
+
await page.evaluate(viewerScript);
|
|
2214
|
+
await page.evaluate(`window.__gp.stringSources(${JSON.stringify(args.stringSets)})`);
|
|
2215
|
+
if (args.rectoDecls.length)
|
|
2216
|
+
await page.evaluate(`window.__gp.forcedBreakSites(${JSON.stringify(args.rectoDecls)})`);
|
|
2217
|
+
await page.evaluate(`window.__gp.xrefSites(${JSON.stringify(args.xrefSelectors)})`);
|
|
2218
|
+
if (args.resets.length)
|
|
2219
|
+
await page.evaluate(`window.__gp.counterResetSites(${JSON.stringify(args.resets)})`);
|
|
2220
|
+
const result = await page.evaluate(`(async () => {
|
|
2221
|
+
const api = await window.Gutterpress.fragmentDocument({});
|
|
2222
|
+
const pageMap = {};
|
|
2223
|
+
for (const id of ${JSON.stringify(args.targets)}) {
|
|
2224
|
+
const el = document.getElementById(id);
|
|
2225
|
+
if (!el) continue;
|
|
2226
|
+
// an id may name an injected zero-size <gp-anchor>; measure its
|
|
2227
|
+
// host instead (same as the compiler agent's own anchorHost()).
|
|
2228
|
+
const target = el.tagName === "GP-ANCHOR" ? el.parentElement ?? el : el;
|
|
2229
|
+
pageMap[id] = api.pageOf(target) + 1;
|
|
2230
|
+
}
|
|
2231
|
+
return { pageMap, pageCount: api.totalPages };
|
|
2232
|
+
})()`);
|
|
2233
|
+
return { ...result, ms: performance.now() - t0 };
|
|
2234
|
+
} catch {
|
|
2235
|
+
return null;
|
|
2236
|
+
} finally {
|
|
2237
|
+
if (page)
|
|
2238
|
+
await page.close();
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function counterStyleCss(model, sources, pageMap, pageCount, resetSites = [], hasBlank = false) {
|
|
2242
|
+
const consumed = consumedStrings(model);
|
|
2243
|
+
const byName = new Map;
|
|
2244
|
+
for (const s of sources) {
|
|
2245
|
+
const page = pageMap[s.id];
|
|
2246
|
+
if (!page || !consumed.has(s.name))
|
|
2247
|
+
continue;
|
|
2248
|
+
const list = byName.get(s.name) ?? [];
|
|
2249
|
+
list.push({ page, value: s.text });
|
|
2250
|
+
byName.set(s.name, list);
|
|
2251
|
+
}
|
|
2252
|
+
for (const entries of byName.values())
|
|
2253
|
+
entries.sort((a, b) => a.page - b.page);
|
|
2254
|
+
const pageValues = restartedPageValues(resetSites, pageMap, pageCount);
|
|
2255
|
+
if (!consumed.size && !pageValues && !hasBlank)
|
|
2256
|
+
return "";
|
|
2257
|
+
if (!byName.size && !pageValues && !hasBlank)
|
|
2258
|
+
return "";
|
|
2259
|
+
const pairs = new Map;
|
|
2260
|
+
for (const rule of model.pageRules) {
|
|
2261
|
+
for (const decls of Object.values(rule.marginBoxes)) {
|
|
2262
|
+
if (!decls.content)
|
|
2263
|
+
continue;
|
|
2264
|
+
for (const part of parseContent(decls.content)) {
|
|
2265
|
+
if (part.type !== "string" || !byName.has(part.name))
|
|
2266
|
+
continue;
|
|
2267
|
+
const which = parseWhich(part.which);
|
|
2268
|
+
pairs.set(counterStyleName(part.name, which), { name: part.name, which });
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const out = [];
|
|
2273
|
+
for (const [styleName, pair] of pairs) {
|
|
2274
|
+
const symbols = stringSymbols(byName.get(pair.name), pageCount, pair.which);
|
|
2275
|
+
out.push(`@counter-style ${styleName} { system: fixed; suffix: ""; symbols: ${symbols.map(cssQuote).join(" ")}; }`);
|
|
2276
|
+
}
|
|
2277
|
+
const pageCounterStyles = new Map;
|
|
2278
|
+
const pageCounterStyleName = (style) => `gp-page--${style}`;
|
|
2279
|
+
const rewrite = (content) => parseContent(content).map((part) => {
|
|
2280
|
+
if (part.type === "string")
|
|
2281
|
+
return `counter(page, ${counterStyleName(part.name, parseWhich(part.which))})`;
|
|
2282
|
+
if (part.type === "literal")
|
|
2283
|
+
return cssQuote(part.value);
|
|
2284
|
+
if (part.type === "counter") {
|
|
2285
|
+
if (part.name === "page" && pageValues) {
|
|
2286
|
+
const styleName = pageCounterStyleName(part.style);
|
|
2287
|
+
if (!pageCounterStyles.has(styleName))
|
|
2288
|
+
pageCounterStyles.set(styleName, pageValues.map((v) => formatCounter(v, part.style)));
|
|
2289
|
+
return `counter(page, ${styleName})`;
|
|
2290
|
+
}
|
|
2291
|
+
return `counter(${part.name}${part.style !== "decimal" ? `, ${part.style}` : ""})`;
|
|
2292
|
+
}
|
|
2293
|
+
if (part.type === "keyword")
|
|
2294
|
+
return part.value;
|
|
2295
|
+
return "";
|
|
2296
|
+
}).filter(Boolean).join(" ");
|
|
2297
|
+
const names = [undefined, ...model.pageNames];
|
|
2298
|
+
const variants = [[], ...pseudoVariants(model).map((p) => [p])];
|
|
2299
|
+
for (const name of names) {
|
|
2300
|
+
for (const pseudos of variants) {
|
|
2301
|
+
const resolved = resolvePage(model, { name, pseudos });
|
|
2302
|
+
const lines = [];
|
|
2303
|
+
for (const [box, decls] of Object.entries(resolved.marginBoxes)) {
|
|
2304
|
+
if (!decls.content)
|
|
2305
|
+
continue;
|
|
2306
|
+
const hasString = /\bstring\s*\(/.test(decls.content);
|
|
2307
|
+
const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls.content);
|
|
2308
|
+
const needsRewrite = hasString || hasPageCounter;
|
|
2309
|
+
lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls.content) : decls.content}; }`);
|
|
2310
|
+
}
|
|
2311
|
+
if (!lines.length)
|
|
2312
|
+
continue;
|
|
2313
|
+
const pseudo = pseudos.length ? `:${pseudos.join(":")}` : "";
|
|
2314
|
+
out.push(`@page ${name ?? ""}${pseudo} {
|
|
2315
|
+
${lines.join(`
|
|
2316
|
+
`)}
|
|
2317
|
+
}`.replace("@page ", "@page "));
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
if (hasBlank) {
|
|
2321
|
+
const blank = resolvePage(model, { pseudos: ["blank"] });
|
|
2322
|
+
const lines = [];
|
|
2323
|
+
for (const [box, decls] of Object.entries(blank.marginBoxes)) {
|
|
2324
|
+
if (!decls.content)
|
|
2325
|
+
continue;
|
|
2326
|
+
const hasString = /\bstring\s*\(/.test(decls.content);
|
|
2327
|
+
const hasPageCounter = pageValues && /\bcounter\(\s*page\b(?!s)/.test(decls.content);
|
|
2328
|
+
const needsRewrite = hasString || hasPageCounter;
|
|
2329
|
+
lines.push(` ${box} { content: ${needsRewrite ? rewrite(decls.content) : decls.content}; }`);
|
|
2330
|
+
}
|
|
2331
|
+
out.push(`@page ${BLANK_PAGE} {
|
|
2332
|
+
${lines.length ? lines.join(`
|
|
2333
|
+
`) : " /* author declared no @page :blank */"}
|
|
2334
|
+
}`);
|
|
2335
|
+
}
|
|
2336
|
+
for (const [styleName, symbols] of pageCounterStyles) {
|
|
2337
|
+
out.push(`@counter-style ${styleName} { system: fixed; suffix: ""; symbols: ${symbols.map(cssQuote).join(" ")}; }`);
|
|
2338
|
+
}
|
|
2339
|
+
return out.join(`
|
|
2340
|
+
`);
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
export { RENDER_TIMEOUT_MS, prewarmBrowser, getBrowser, closeBrowser, getAssetPath, REQUIRED_MILESTONE, assertMilestone, connectChromium, build };
|