@open-press/core 1.1.3 → 1.1.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/engine/cli.mjs
CHANGED
|
@@ -85,7 +85,7 @@ Commands:
|
|
|
85
85
|
preview --renderer react [--host 127.0.0.1] [--port 5173] [--no-build] [--dry-run]
|
|
86
86
|
dev --renderer react [--host 127.0.0.1] [--port 5173] [--no-build] [--dry-run]
|
|
87
87
|
typecheck
|
|
88
|
-
image [--output <outputDir>] [--no-build] [--dry-run]
|
|
88
|
+
image [--output <outputDir>] [--pages <selector>] [--no-build] [--dry-run]
|
|
89
89
|
pdf [--output <outputDir>/<pdf.filename>] [--no-build] [--dry-run]
|
|
90
90
|
deploy --confirm [--dry-run]
|
|
91
91
|
doctor [--json] [--no-cache] # version + skill staleness check
|
|
@@ -40,6 +40,7 @@ export function parseOptions(argv) {
|
|
|
40
40
|
else if (value === "--scope") options.scope = argv[++i];
|
|
41
41
|
else if (value === "--source") options.source = argv[++i];
|
|
42
42
|
else if (value === "--output") options.output = argv[++i];
|
|
43
|
+
else if (value === "--pages") options.pages = argv[++i];
|
|
43
44
|
else if (value.startsWith("--")) throw new Error(`Unknown option: ${value}`);
|
|
44
45
|
else positional.push(value);
|
|
45
46
|
}
|
|
@@ -145,6 +146,7 @@ export async function buildReactImages({
|
|
|
145
146
|
port = "5186",
|
|
146
147
|
noBuild = false,
|
|
147
148
|
recurse,
|
|
149
|
+
pageSelector = null,
|
|
148
150
|
}) {
|
|
149
151
|
config ??= await loadConfig(root);
|
|
150
152
|
outDir ??= path.join(config.paths.outputDir, "images");
|
|
@@ -162,9 +164,18 @@ export async function buildReactImages({
|
|
|
162
164
|
debuggingPortBase: 9700,
|
|
163
165
|
debuggingPortRange: 600,
|
|
164
166
|
profilePrefix: "chrome-image",
|
|
167
|
+
pageSelector,
|
|
165
168
|
});
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
const label = pageSelector
|
|
170
|
+
? `${result.files.length}/${result.pageCount} OpenPress pages exported to PNG`
|
|
171
|
+
: `${result.files.length} OpenPress pages exported to PNG`;
|
|
172
|
+
console.log(label);
|
|
173
|
+
return {
|
|
174
|
+
outDir,
|
|
175
|
+
files: result.files,
|
|
176
|
+
pageCount: result.pageCount,
|
|
177
|
+
selectedPageNumbers: result.selectedPageNumbers,
|
|
178
|
+
};
|
|
168
179
|
} finally {
|
|
169
180
|
await stopChildProcess(server);
|
|
170
181
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { STATIC_SERVER, buildReactImages, formatNodeScriptCommand, formatOpenPressCommand } from "./_shared.mjs";
|
|
3
|
+
import { parsePageSelector } from "../runtime/page-selector.mjs";
|
|
3
4
|
|
|
4
5
|
export async function run({ root, config, options, recurse }) {
|
|
5
6
|
const outputDir = options.output ? path.resolve(root, options.output) : path.join(config.paths.outputDir, "images");
|
|
6
7
|
const host = options.host ?? "127.0.0.1";
|
|
7
8
|
const port = options.port ?? "5186";
|
|
8
9
|
|
|
10
|
+
const pageSelector = options.pages ? parsePageSelector(options.pages) : null;
|
|
11
|
+
|
|
9
12
|
if (options.dryRun) {
|
|
10
13
|
console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
|
|
11
14
|
console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
|
|
12
15
|
console.log(`Chrome image export URL: http://${host}:${port}/?print=1`);
|
|
16
|
+
if (pageSelector) {
|
|
17
|
+
console.log(`Page selector: ${options.pages} (resolved at capture time against the rendered page count)`);
|
|
18
|
+
}
|
|
13
19
|
console.log(`Output: ${path.relative(root, path.join(outputDir, "page-001.png"))}`);
|
|
14
20
|
return 0;
|
|
15
21
|
}
|
|
@@ -22,8 +28,12 @@ export async function run({ root, config, options, recurse }) {
|
|
|
22
28
|
port,
|
|
23
29
|
noBuild: options.noBuild,
|
|
24
30
|
recurse,
|
|
31
|
+
pageSelector,
|
|
25
32
|
});
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
const suffix = pageSelector
|
|
35
|
+
? ` (${result.files.length}/${result.pageCount} pages)`
|
|
36
|
+
: ` (${result.files.length} pages)`;
|
|
37
|
+
console.log(`OpenPress images: ${path.relative(root, result.outDir)}${suffix}`);
|
|
28
38
|
return 0;
|
|
29
39
|
}
|
|
@@ -2,6 +2,9 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { resolvePageSelector } from "../runtime/page-selector.mjs";
|
|
6
|
+
|
|
7
|
+
const defaultResolveSelector = resolvePageSelector;
|
|
5
8
|
|
|
6
9
|
const CHROME_CANDIDATE_PATHS = {
|
|
7
10
|
darwin: [
|
|
@@ -137,6 +140,7 @@ export async function printUrlToPdf({
|
|
|
137
140
|
await preparePdfPage(client, { viewport });
|
|
138
141
|
await client.send("Page.navigate", { url });
|
|
139
142
|
const readyResult = await waitForReady(client);
|
|
143
|
+
warnAboutOverflowingPages("PDF", readyResult);
|
|
140
144
|
const result = await client.send("Page.printToPDF", {
|
|
141
145
|
...DEFAULT_PRINT_OPTIONS,
|
|
142
146
|
...printOptions,
|
|
@@ -152,6 +156,16 @@ export async function printUrlToPdf({
|
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
158
|
|
|
159
|
+
function warnAboutOverflowingPages(label, readyResult) {
|
|
160
|
+
const overflowing = Array.isArray(readyResult?.overflowingPageNumbers) ? readyResult.overflowingPageNumbers : [];
|
|
161
|
+
if (overflowing.length === 0) return;
|
|
162
|
+
const preview = overflowing.slice(0, 12).join(", ") + (overflowing.length > 12 ? `, … (+${overflowing.length - 12} more)` : "");
|
|
163
|
+
console.warn(
|
|
164
|
+
`OpenPress ${label}: ${overflowing.length} page(s) exceed the page body bounds (pages ${preview}). ` +
|
|
165
|
+
`Output will still be generated but those pages may clip; run \`openpress inspect\` to locate the overflowing elements.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
155
169
|
export async function captureUrlPagesToPng({
|
|
156
170
|
root,
|
|
157
171
|
url,
|
|
@@ -162,6 +176,8 @@ export async function captureUrlPagesToPng({
|
|
|
162
176
|
debuggingPortBase = 9700,
|
|
163
177
|
debuggingPortRange = 300,
|
|
164
178
|
profilePrefix = "chrome-image",
|
|
179
|
+
pageSelector = null,
|
|
180
|
+
resolveSelector = null,
|
|
165
181
|
}) {
|
|
166
182
|
chrome ??= resolveChromePath();
|
|
167
183
|
await fs.mkdir(outDir, { recursive: true });
|
|
@@ -189,14 +205,25 @@ export async function captureUrlPagesToPng({
|
|
|
189
205
|
try {
|
|
190
206
|
await preparePdfPage(client, { viewport });
|
|
191
207
|
await client.send("Page.navigate", { url });
|
|
192
|
-
const
|
|
208
|
+
const readyResult = await waitForReady(client);
|
|
209
|
+
warnAboutOverflowingPages("image", readyResult);
|
|
210
|
+
const pageCount = readyResult?.pageCount ?? 0;
|
|
193
211
|
const rects = await getPrintPageRects(client);
|
|
194
212
|
if (rects.length === 0) throw new Error("No OpenPress pages found for image export.");
|
|
195
213
|
|
|
214
|
+
const selectedPageNumbers = pageSelector
|
|
215
|
+
? (resolveSelector ?? defaultResolveSelector)(pageSelector, rects.length)
|
|
216
|
+
: rects.map((_, index) => index + 1);
|
|
217
|
+
if (selectedPageNumbers.length === 0) {
|
|
218
|
+
throw new Error("Page selector resolved to zero pages; nothing to export.");
|
|
219
|
+
}
|
|
220
|
+
|
|
196
221
|
const padWidth = Math.max(3, String(rects.length).length);
|
|
197
222
|
const files = [];
|
|
198
|
-
for (const
|
|
199
|
-
const
|
|
223
|
+
for (const pageNumber of selectedPageNumbers) {
|
|
224
|
+
const rect = rects[pageNumber - 1];
|
|
225
|
+
if (!rect) continue;
|
|
226
|
+
const filename = `page-${String(pageNumber).padStart(padWidth, "0")}.png`;
|
|
200
227
|
const filePath = path.join(outDir, filename);
|
|
201
228
|
const result = await client.send("Page.captureScreenshot", {
|
|
202
229
|
format: "png",
|
|
@@ -214,7 +241,7 @@ export async function captureUrlPagesToPng({
|
|
|
214
241
|
files.push(filePath);
|
|
215
242
|
}
|
|
216
243
|
|
|
217
|
-
return { pageCount, files };
|
|
244
|
+
return { pageCount, files, selectedPageNumbers };
|
|
218
245
|
} finally {
|
|
219
246
|
client.close();
|
|
220
247
|
}
|
|
@@ -282,59 +309,135 @@ export async function evaluateUrlWithChrome({
|
|
|
282
309
|
}
|
|
283
310
|
}
|
|
284
311
|
|
|
285
|
-
export
|
|
286
|
-
|
|
287
|
-
|
|
312
|
+
export const PRINT_READY_DEFAULTS = Object.freeze({
|
|
313
|
+
totalTimeoutMs: 300_000,
|
|
314
|
+
idleTimeoutMs: 30_000,
|
|
315
|
+
pollIntervalMs: 100,
|
|
316
|
+
stableMs: 300,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
export function resolvePrintReadyTiming(env = process.env) {
|
|
320
|
+
const total = Number(env.OPENPRESS_PRINT_READY_TIMEOUT_MS);
|
|
321
|
+
const idle = Number(env.OPENPRESS_PRINT_READY_IDLE_MS);
|
|
322
|
+
const stable = Number(env.OPENPRESS_PRINT_READY_STABLE_MS);
|
|
323
|
+
return {
|
|
324
|
+
totalTimeoutMs: Number.isFinite(total) && total > 0 ? total : PRINT_READY_DEFAULTS.totalTimeoutMs,
|
|
325
|
+
idleTimeoutMs: Number.isFinite(idle) && idle > 0 ? idle : PRINT_READY_DEFAULTS.idleTimeoutMs,
|
|
326
|
+
stableMs: Number.isFinite(stable) && stable >= 0 ? stable : PRINT_READY_DEFAULTS.stableMs,
|
|
327
|
+
pollIntervalMs: PRINT_READY_DEFAULTS.pollIntervalMs,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function printReadinessExpression() {
|
|
332
|
+
return `Promise.resolve().then(async () => {
|
|
333
|
+
const root = document.querySelector('[data-openpress-print-document="true"]');
|
|
334
|
+
if (!root) return { pageCount: 0, overflowingPages: 0, overflowingPageNumbers: [] };
|
|
335
|
+
const candidates = root.querySelectorAll('.openpress-html-page');
|
|
336
|
+
if (candidates.length === 0) return { pageCount: 0, overflowingPages: 0, overflowingPageNumbers: [] };
|
|
337
|
+
|
|
338
|
+
await document.fonts?.ready;
|
|
339
|
+
await Promise.all(Array.from(document.images).map(async (img) => {
|
|
340
|
+
if (!img.complete) {
|
|
341
|
+
await new Promise((resolve) => {
|
|
342
|
+
const settle = () => {
|
|
343
|
+
img.removeEventListener('load', settle);
|
|
344
|
+
img.removeEventListener('error', settle);
|
|
345
|
+
resolve();
|
|
346
|
+
};
|
|
347
|
+
img.addEventListener('load', settle, { once: true });
|
|
348
|
+
img.addEventListener('error', settle, { once: true });
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
await img.decode?.().catch(() => undefined);
|
|
352
|
+
}));
|
|
353
|
+
|
|
354
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
355
|
+
|
|
356
|
+
const pages = Array.from(document.querySelectorAll('.openpress-public-page > .openpress-html-page'));
|
|
357
|
+
const contentFitsPageBody = (body) => {
|
|
358
|
+
const bodyBottom = body.getBoundingClientRect().bottom;
|
|
359
|
+
const contentBottom = Array.from(body.children).reduce((bottom, child) => {
|
|
360
|
+
if (getComputedStyle(child).display === 'none') return bottom;
|
|
361
|
+
const marginBottom = Number.parseFloat(getComputedStyle(child).marginBottom) || 0;
|
|
362
|
+
return Math.max(bottom, child.getBoundingClientRect().bottom + marginBottom);
|
|
363
|
+
}, body.getBoundingClientRect().top);
|
|
364
|
+
return contentBottom <= bodyBottom + 1;
|
|
365
|
+
};
|
|
366
|
+
const overflowingPageNumbers = pages.reduce((nums, page, index) => {
|
|
367
|
+
const body = page.querySelector('.page-body');
|
|
368
|
+
if (body && !contentFitsPageBody(body)) nums.push(index + 1);
|
|
369
|
+
return nums;
|
|
370
|
+
}, []);
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
pageCount: pages.length,
|
|
374
|
+
overflowingPages: overflowingPageNumbers.length,
|
|
375
|
+
overflowingPageNumbers,
|
|
376
|
+
};
|
|
377
|
+
})`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function formatPrintReadyTimeoutMessage(reason, snapshot, timing, elapsedMs) {
|
|
381
|
+
const seconds = Math.round(elapsedMs / 1000);
|
|
382
|
+
const observed = `(observed ${snapshot.pageCount} page(s), ${snapshot.overflowingPages} overflowing)`;
|
|
383
|
+
if (reason === "idle") {
|
|
384
|
+
return (
|
|
385
|
+
`Timed out waiting for OpenPress pagination before PDF export. ` +
|
|
386
|
+
`No progress for ${seconds}s ${observed}. ` +
|
|
387
|
+
`Raise OPENPRESS_PRINT_READY_IDLE_MS (currently ${timing.idleTimeoutMs}ms) to extend the idle window.`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return (
|
|
391
|
+
`Timed out waiting for OpenPress pagination before PDF export. ` +
|
|
392
|
+
`Total ${seconds}s exceeded ${observed}. ` +
|
|
393
|
+
`Raise OPENPRESS_PRINT_READY_TIMEOUT_MS (currently ${timing.totalTimeoutMs}ms) to extend the hard cap.`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function waitForPrintReady(client, timing = resolvePrintReadyTiming()) {
|
|
398
|
+
const startedAt = Date.now();
|
|
399
|
+
let lastSignature = "";
|
|
400
|
+
let lastProgressAt = startedAt;
|
|
401
|
+
let stableSince = startedAt;
|
|
402
|
+
let lastSnapshot = { pageCount: 0, overflowingPages: 0, overflowingPageNumbers: [] };
|
|
403
|
+
|
|
404
|
+
while (true) {
|
|
405
|
+
const totalElapsed = Date.now() - startedAt;
|
|
406
|
+
if (totalElapsed > timing.totalTimeoutMs) {
|
|
407
|
+
throw new Error(formatPrintReadyTimeoutMessage("total", lastSnapshot, timing, totalElapsed));
|
|
408
|
+
}
|
|
409
|
+
|
|
288
410
|
const result = await client.send("Runtime.evaluate", {
|
|
289
411
|
returnByValue: true,
|
|
290
412
|
awaitPromise: true,
|
|
291
|
-
expression:
|
|
292
|
-
const root = document.querySelector('[data-openpress-print-document="true"]');
|
|
293
|
-
if (!root || root.querySelectorAll('.openpress-html-page').length === 0) return 0;
|
|
294
|
-
|
|
295
|
-
await document.fonts?.ready;
|
|
296
|
-
await Promise.all(Array.from(document.images).map(async (img) => {
|
|
297
|
-
if (!img.complete) {
|
|
298
|
-
await new Promise((resolve) => {
|
|
299
|
-
const settle = () => {
|
|
300
|
-
img.removeEventListener('load', settle);
|
|
301
|
-
img.removeEventListener('error', settle);
|
|
302
|
-
resolve();
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
img.addEventListener('load', settle, { once: true });
|
|
306
|
-
img.addEventListener('error', settle, { once: true });
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
await img.decode?.().catch(() => undefined);
|
|
311
|
-
}));
|
|
312
|
-
|
|
313
|
-
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
314
|
-
|
|
315
|
-
const pages = Array.from(document.querySelectorAll('.openpress-public-page > .openpress-html-page'));
|
|
316
|
-
const contentFitsPageBody = (body) => {
|
|
317
|
-
const bodyBottom = body.getBoundingClientRect().bottom;
|
|
318
|
-
const contentBottom = Array.from(body.children).reduce((bottom, child) => {
|
|
319
|
-
if (getComputedStyle(child).display === 'none') return bottom;
|
|
320
|
-
const marginBottom = Number.parseFloat(getComputedStyle(child).marginBottom) || 0;
|
|
321
|
-
return Math.max(bottom, child.getBoundingClientRect().bottom + marginBottom);
|
|
322
|
-
}, body.getBoundingClientRect().top);
|
|
323
|
-
return contentBottom <= bodyBottom + 1;
|
|
324
|
-
};
|
|
325
|
-
const bodyOverflowSafe = pages.every((page) => {
|
|
326
|
-
const body = page.querySelector('.page-body');
|
|
327
|
-
return !body || contentFitsPageBody(body);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
return pages.length > 0 && bodyOverflowSafe ? pages.length : 0;
|
|
331
|
-
})`,
|
|
413
|
+
expression: printReadinessExpression(),
|
|
332
414
|
});
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
415
|
+
const value = result.result?.value ?? {};
|
|
416
|
+
const pageCount = Number.isFinite(Number(value.pageCount)) ? Number(value.pageCount) : 0;
|
|
417
|
+
const overflowingPages = Number.isFinite(Number(value.overflowingPages)) ? Number(value.overflowingPages) : 0;
|
|
418
|
+
const overflowingPageNumbers = Array.isArray(value.overflowingPageNumbers)
|
|
419
|
+
? value.overflowingPageNumbers.map(Number).filter(Number.isFinite)
|
|
420
|
+
: [];
|
|
421
|
+
lastSnapshot = { pageCount, overflowingPages, overflowingPageNumbers };
|
|
422
|
+
|
|
423
|
+
const signature = `${pageCount}:${overflowingPages}`;
|
|
424
|
+
if (signature !== lastSignature) {
|
|
425
|
+
lastSignature = signature;
|
|
426
|
+
stableSince = Date.now();
|
|
427
|
+
lastProgressAt = Date.now();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (pageCount > 0 && Date.now() - stableSince >= timing.stableMs) {
|
|
431
|
+
return { pageCount, overflowingPageNumbers };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const idleElapsed = Date.now() - lastProgressAt;
|
|
435
|
+
if (idleElapsed > timing.idleTimeoutMs) {
|
|
436
|
+
throw new Error(formatPrintReadyTimeoutMessage("idle", lastSnapshot, timing, idleElapsed));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
await delay(timing.pollIntervalMs);
|
|
336
440
|
}
|
|
337
|
-
throw new Error("Timed out waiting for OpenPress pagination before PDF export.");
|
|
338
441
|
}
|
|
339
442
|
|
|
340
443
|
async function getPrintPageRects(client) {
|
|
@@ -139,9 +139,51 @@ export async function inspectRenderedOverflow({ root, config, host = "127.0.0.1"
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
export
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
export const INSPECTION_READY_DEFAULTS = Object.freeze({
|
|
143
|
+
totalTimeoutMs: 300_000,
|
|
144
|
+
idleTimeoutMs: 30_000,
|
|
145
|
+
pollIntervalMs: 100,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
export function resolveInspectionReadyTiming(env = process.env) {
|
|
149
|
+
const total = Number(env.OPENPRESS_INSPECTION_TIMEOUT_MS);
|
|
150
|
+
const idle = Number(env.OPENPRESS_INSPECTION_IDLE_MS);
|
|
151
|
+
return {
|
|
152
|
+
totalTimeoutMs: Number.isFinite(total) && total > 0 ? total : INSPECTION_READY_DEFAULTS.totalTimeoutMs,
|
|
153
|
+
idleTimeoutMs: Number.isFinite(idle) && idle > 0 ? idle : INSPECTION_READY_DEFAULTS.idleTimeoutMs,
|
|
154
|
+
pollIntervalMs: INSPECTION_READY_DEFAULTS.pollIntervalMs,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatInspectionTimeoutMessage(reason, snapshot, timing, elapsedMs) {
|
|
159
|
+
const seconds = Math.round(elapsedMs / 1000);
|
|
160
|
+
const observed = `(observed ${snapshot.pageCount} page(s))`;
|
|
161
|
+
if (reason === "idle") {
|
|
162
|
+
return (
|
|
163
|
+
`Timed out waiting for OpenPress pagination before inspection. ` +
|
|
164
|
+
`No progress for ${seconds}s ${observed}. ` +
|
|
165
|
+
`Raise OPENPRESS_INSPECTION_IDLE_MS (currently ${timing.idleTimeoutMs}ms) to extend the idle window.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
`Timed out waiting for OpenPress pagination before inspection. ` +
|
|
170
|
+
`Total ${seconds}s exceeded ${observed}. ` +
|
|
171
|
+
`Raise OPENPRESS_INSPECTION_TIMEOUT_MS (currently ${timing.totalTimeoutMs}ms) to extend the hard cap.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function waitForInspectionReady(client, timing = resolveInspectionReadyTiming()) {
|
|
176
|
+
const startedAt = Date.now();
|
|
177
|
+
let lastSignature = "";
|
|
178
|
+
let lastProgressAt = startedAt;
|
|
179
|
+
let lastSnapshot = { pageCount: 0 };
|
|
180
|
+
|
|
181
|
+
while (true) {
|
|
182
|
+
const totalElapsed = Date.now() - startedAt;
|
|
183
|
+
if (totalElapsed > timing.totalTimeoutMs) {
|
|
184
|
+
throw new Error(formatInspectionTimeoutMessage("total", lastSnapshot, timing, totalElapsed));
|
|
185
|
+
}
|
|
186
|
+
|
|
145
187
|
const result = await client.send("Runtime.evaluate", {
|
|
146
188
|
returnByValue: true,
|
|
147
189
|
awaitPromise: true,
|
|
@@ -149,9 +191,23 @@ export async function waitForInspectionReady(client) {
|
|
|
149
191
|
});
|
|
150
192
|
const value = result.result?.value;
|
|
151
193
|
if (Array.isArray(value)) return value;
|
|
152
|
-
|
|
194
|
+
|
|
195
|
+
const pageCount = Number.isFinite(Number(value?.pageCount)) ? Number(value.pageCount) : lastSnapshot.pageCount;
|
|
196
|
+
lastSnapshot = { pageCount };
|
|
197
|
+
|
|
198
|
+
const signature = String(pageCount);
|
|
199
|
+
if (signature !== lastSignature) {
|
|
200
|
+
lastSignature = signature;
|
|
201
|
+
lastProgressAt = Date.now();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const idleElapsed = Date.now() - lastProgressAt;
|
|
205
|
+
if (idleElapsed > timing.idleTimeoutMs) {
|
|
206
|
+
throw new Error(formatInspectionTimeoutMessage("idle", lastSnapshot, timing, idleElapsed));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await delay(timing.pollIntervalMs);
|
|
153
210
|
}
|
|
154
|
-
throw new Error("Timed out waiting for OpenPress pagination before inspection.");
|
|
155
211
|
}
|
|
156
212
|
|
|
157
213
|
export function overflowIssuesFromMeasurements(measurements) {
|
|
@@ -241,7 +297,9 @@ function humanOverflowTarget(code) {
|
|
|
241
297
|
function inspectionExpression() {
|
|
242
298
|
return `Promise.resolve().then(async () => {
|
|
243
299
|
const root = document.querySelector('[data-openpress-print-document="true"]');
|
|
244
|
-
if (!root
|
|
300
|
+
if (!root) return { pending: true, pageCount: 0 };
|
|
301
|
+
const candidates = root.querySelectorAll('.openpress-html-page');
|
|
302
|
+
if (candidates.length === 0) return { pending: true, pageCount: 0 };
|
|
245
303
|
|
|
246
304
|
await document.fonts?.ready;
|
|
247
305
|
await Promise.all(Array.from(document.images).map(async (img) => {
|
|
@@ -290,7 +348,7 @@ function inspectionExpression() {
|
|
|
290
348
|
};
|
|
291
349
|
|
|
292
350
|
const wrappers = Array.from(document.querySelectorAll('.openpress-public-page > .openpress-html-page'));
|
|
293
|
-
if (wrappers.length === 0) return
|
|
351
|
+
if (wrappers.length === 0) return { pending: true, pageCount: candidates.length };
|
|
294
352
|
return wrappers.map((wrapper, index) => {
|
|
295
353
|
const page = wrapper.querySelector('.reader-page') || wrapper;
|
|
296
354
|
const frame = page.querySelector('.page-frame') || page;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const TOKEN_PATTERN = /^\s*(-?\d*)\s*(?:-\s*(\d*))?\s*$/;
|
|
2
|
+
|
|
3
|
+
export function parsePageSelector(input) {
|
|
4
|
+
if (typeof input !== "string") {
|
|
5
|
+
throw new TypeError("Page selector must be a string");
|
|
6
|
+
}
|
|
7
|
+
const trimmed = input.trim();
|
|
8
|
+
if (trimmed === "") {
|
|
9
|
+
throw new Error("Page selector is empty; expected something like '3', '3,5-7', or '12-'.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const segments = trimmed.split(",").map((part) => part.trim()).filter(Boolean);
|
|
13
|
+
if (segments.length === 0) {
|
|
14
|
+
throw new Error(`Page selector "${input}" has no usable segments.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return segments.map((segment) => parseSegment(segment, input));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseSegment(segment, original) {
|
|
21
|
+
if (!segment.includes("-")) {
|
|
22
|
+
const value = toPositiveInteger(segment, original);
|
|
23
|
+
return { kind: "single", value };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (segment === "-") {
|
|
27
|
+
throw new Error(`Page selector "${original}" contains a bare "-"; ranges need at least one bound.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dashIndex = segment.indexOf("-");
|
|
31
|
+
if (segment.indexOf("-", dashIndex + 1) !== -1) {
|
|
32
|
+
throw new Error(`Page selector segment "${segment}" has too many dashes.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const leftRaw = segment.slice(0, dashIndex);
|
|
36
|
+
const rightRaw = segment.slice(dashIndex + 1);
|
|
37
|
+
const from = leftRaw.trim() === "" ? null : toPositiveInteger(leftRaw, original);
|
|
38
|
+
const to = rightRaw.trim() === "" ? null : toPositiveInteger(rightRaw, original);
|
|
39
|
+
|
|
40
|
+
if (from != null && to != null && from > to) {
|
|
41
|
+
throw new Error(`Page selector range "${segment}" goes backwards (${from} > ${to}).`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { kind: "range", from, to };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toPositiveInteger(raw, original) {
|
|
48
|
+
const trimmed = raw.trim();
|
|
49
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
50
|
+
throw new Error(`Page selector "${original}" contains non-integer token "${raw}".`);
|
|
51
|
+
}
|
|
52
|
+
const value = Number(trimmed);
|
|
53
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
54
|
+
throw new Error(`Page selector "${original}" contains out-of-range page number "${raw}"; pages start at 1.`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolvePageSelector(spec, totalPages) {
|
|
60
|
+
if (!Array.isArray(spec)) {
|
|
61
|
+
throw new TypeError("resolvePageSelector expects a parsed selector array");
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isInteger(totalPages) || totalPages < 0) {
|
|
64
|
+
throw new TypeError("resolvePageSelector expects a non-negative integer totalPages");
|
|
65
|
+
}
|
|
66
|
+
if (totalPages === 0) return [];
|
|
67
|
+
|
|
68
|
+
const selected = new Set();
|
|
69
|
+
for (const segment of spec) {
|
|
70
|
+
if (segment.kind === "single") {
|
|
71
|
+
if (segment.value > totalPages) {
|
|
72
|
+
throw new Error(`Page ${segment.value} is out of range; document has ${totalPages} page(s).`);
|
|
73
|
+
}
|
|
74
|
+
selected.add(segment.value);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const from = segment.from ?? 1;
|
|
78
|
+
const to = segment.to ?? totalPages;
|
|
79
|
+
if (from > totalPages) {
|
|
80
|
+
throw new Error(`Range start ${from} is out of range; document has ${totalPages} page(s).`);
|
|
81
|
+
}
|
|
82
|
+
const upper = Math.min(to, totalPages);
|
|
83
|
+
for (let i = from; i <= upper; i += 1) selected.add(i);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Array.from(selected).sort((a, b) => a - b);
|
|
87
|
+
}
|