@open-press/core 1.1.4 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/engine/cli.mjs +3 -3
- package/engine/commands/_shared.mjs +89 -13
- package/engine/commands/deploy.mjs +19 -4
- package/engine/commands/image.mjs +9 -3
- package/engine/commands/pdf.mjs +4 -1
- package/engine/output/chrome-pdf.mjs +102 -0
- package/engine/output/static-server.mjs +64 -17
- package/engine/react/document-export.mjs +22 -0
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +5 -1
- package/src/openpress/app/OpenPressRuntime.tsx +85 -6
- package/src/openpress/reader/PageThumbnailsPanel.tsx +28 -5
- package/src/openpress/reader/PublicReaderPage.tsx +163 -74
- package/src/openpress/reader/SlidePresentationPage.tsx +37 -15
- package/src/openpress/reader/SlidePublicPage.tsx +332 -0
- package/src/openpress/reader/index.ts +1 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +5 -3
- package/src/openpress/reader/usePageViewportScale.ts +9 -5
- package/src/openpress/reader/usePanelState.ts +14 -5
- package/src/openpress/shared/index.ts +1 -0
- package/src/openpress/shared/staticSearch.ts +174 -0
- package/src/openpress/workbench/Workbench.tsx +61 -176
- package/src/openpress/workbench/actions/DeploymentControl.tsx +1 -1
- package/src/openpress/workbench/actions/ExportControl.tsx +267 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +32 -43
- package/src/openpress/workbench/actions/index.ts +1 -1
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +21 -5
- package/src/openpress/workbench/hooks/useWorkbenchNavigation.ts +42 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +6 -6
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +2 -278
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +44 -18
- package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +206 -0
- package/src/styles/openpress/app-shell.css +0 -83
- package/src/styles/openpress/print-route.css +1 -3
- package/src/styles/openpress/project-preview-panel.css +5 -783
- package/src/styles/openpress/public-viewer.css +7 -249
- package/src/styles/openpress/reader-runtime.css +0 -274
- package/src/styles/openpress/slide-presenter.css +150 -0
- package/src/styles/openpress/slide-public-viewer.css +222 -0
- package/src/styles/openpress/workbench-dialog.css +267 -0
- package/src/styles/openpress/workbench-export.css +154 -0
- package/src/styles/openpress/workbench-inline-editor.css +128 -0
- package/src/styles/openpress/workbench-panels.css +0 -88
- package/src/styles/openpress/workbench-search.css +257 -0
- package/src/styles/openpress/workbench-toolbar.css +422 -0
- package/src/styles/openpress/workbench.css +34 -1263
- package/src/styles/openpress/workspace-gallery.css +0 -5
- package/src/styles/openpress.css +7 -1
- package/vite.config.ts +66 -17
- package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
- package/src/styles/openpress/media-workspace.css +0 -230
package/engine/cli.mjs
CHANGED
|
@@ -85,9 +85,9 @@ 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>] [--pages <selector>] [--no-build] [--dry-run]
|
|
89
|
-
pdf [--output <outputDir>/<pdf.filename>] [--no-build] [--dry-run]
|
|
90
|
-
deploy --confirm [--dry-run]
|
|
88
|
+
image [--output <outputDir>] [--press <slug>] [--pages <selector>] [--no-build] [--dry-run]
|
|
89
|
+
pdf [--output <outputDir>/<pdf.filename>] [--press <slug>] [--no-build] [--dry-run]
|
|
90
|
+
deploy --confirm [--press <slug>] [--dry-run]
|
|
91
91
|
doctor [--json] [--no-cache] # version + skill staleness check
|
|
92
92
|
upgrade [--dry-run] [--no-deps] [--no-skills] [--json] # apply updates; agent-driven
|
|
93
93
|
migrate [--dry-run] [--no-deps] [--no-skills] [--json] # alias for upgrade; reads migration notes
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { captureUrlPagesToPng, printUrlToPdf, stopChildProcess, waitForPrintReady } from "../output/chrome-pdf.mjs";
|
|
7
|
-
import { loadConfig
|
|
7
|
+
import { loadConfig } from "../runtime/config.mjs";
|
|
8
8
|
import { exportDocument } from "../document-export.mjs";
|
|
9
9
|
import { optimizePdfMediaForStaticRoot } from "../output/pdf-media.mjs";
|
|
10
10
|
|
|
@@ -41,6 +41,7 @@ export function parseOptions(argv) {
|
|
|
41
41
|
else if (value === "--source") options.source = argv[++i];
|
|
42
42
|
else if (value === "--output") options.output = argv[++i];
|
|
43
43
|
else if (value === "--pages") options.pages = argv[++i];
|
|
44
|
+
else if (value === "--press") options.press = argv[++i];
|
|
44
45
|
else if (value.startsWith("--")) throw new Error(`Unknown option: ${value}`);
|
|
45
46
|
else positional.push(value);
|
|
46
47
|
}
|
|
@@ -103,6 +104,56 @@ export async function buildReactStatic({ root, noBuild = false, recurse, silent
|
|
|
103
104
|
return result.status ?? 1;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
export async function resolvePressSelection({ outputDir, slug }) {
|
|
108
|
+
const manifestPath = path.join(outputDir, "openpress", "workspace.json");
|
|
109
|
+
let manifest;
|
|
110
|
+
try {
|
|
111
|
+
const body = await fs.readFile(manifestPath, "utf8");
|
|
112
|
+
manifest = JSON.parse(body);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error?.code === "ENOENT") {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Cannot resolve --press: workspace manifest not found at ${manifestPath}. ` +
|
|
117
|
+
`Run a render first (or drop --no-build) so the manifest is regenerated.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
const presses = Array.isArray(manifest?.presses) ? manifest.presses : [];
|
|
123
|
+
if (presses.length === 0) {
|
|
124
|
+
throw new Error(`Workspace manifest at ${manifestPath} declares no Press entries.`);
|
|
125
|
+
}
|
|
126
|
+
const knownSlugs = presses.map((press) => press.slug || "").filter(Boolean);
|
|
127
|
+
const normalized = typeof slug === "string" ? slug.trim().replace(/^\/+|\/+$/g, "") : "";
|
|
128
|
+
if (!normalized) {
|
|
129
|
+
return { slug: presses[0].slug ?? "", title: presses[0].title ?? "", knownSlugs };
|
|
130
|
+
}
|
|
131
|
+
const match = presses.find((press) => (press.slug ?? "").replace(/^\/+|\/+$/g, "") === normalized);
|
|
132
|
+
if (!match) {
|
|
133
|
+
const listed = knownSlugs.length > 0 ? knownSlugs.join(", ") : "(none — workspace has no slugged presses)";
|
|
134
|
+
throw new Error(`Unknown --press "${slug}". Known slugs: ${listed}.`);
|
|
135
|
+
}
|
|
136
|
+
return { slug: match.slug ?? "", title: match.title ?? "", knownSlugs };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pressPrintUrl(host, port, slug) {
|
|
140
|
+
const normalized = (slug ?? "").replace(/^\/+|\/+$/g, "");
|
|
141
|
+
if (!normalized) return `http://${host}:${port}/?print=1`;
|
|
142
|
+
return `http://${host}:${port}/${normalized}?print=1`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function pressSuffixedFilename(baseFilename, slug) {
|
|
146
|
+
const normalized = (slug ?? "").replace(/^\/+|\/+$/g, "");
|
|
147
|
+
if (!normalized) return baseFilename;
|
|
148
|
+
const ext = path.extname(baseFilename);
|
|
149
|
+
const stem = ext ? baseFilename.slice(0, -ext.length) : baseFilename;
|
|
150
|
+
return `${stem}-${normalized}${ext}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function publicPdfHrefForFilename(filename) {
|
|
154
|
+
return `/${filename}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
106
157
|
export async function buildReactPdf({
|
|
107
158
|
root,
|
|
108
159
|
config,
|
|
@@ -111,31 +162,44 @@ export async function buildReactPdf({
|
|
|
111
162
|
port = "5185",
|
|
112
163
|
noBuild = false,
|
|
113
164
|
recurse,
|
|
165
|
+
pressSlug = null,
|
|
114
166
|
}) {
|
|
115
167
|
config ??= await loadConfig(root);
|
|
116
|
-
outPath ??= config.paths.pdf;
|
|
117
168
|
const renderCode = await buildReactStatic({ root, noBuild, recurse });
|
|
118
169
|
if (renderCode !== 0) throw new Error(`React render failed with exit code ${renderCode}`);
|
|
119
170
|
await optimizePdfMediaForStaticRoot(config.paths.outputDir);
|
|
171
|
+
|
|
172
|
+
const selection = pressSlug
|
|
173
|
+
? await resolvePressSelection({ outputDir: config.paths.outputDir, slug: pressSlug })
|
|
174
|
+
: { slug: "", title: "", knownSlugs: [] };
|
|
175
|
+
|
|
176
|
+
if (!outPath) {
|
|
177
|
+
const filename = selection.slug
|
|
178
|
+
? pressSuffixedFilename(config.pdf.filename, selection.slug)
|
|
179
|
+
: config.pdf.filename;
|
|
180
|
+
outPath = path.join(config.paths.outputDir, filename);
|
|
181
|
+
}
|
|
120
182
|
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
121
183
|
|
|
122
184
|
const server = await startStaticServer(root, config, host, port);
|
|
123
185
|
try {
|
|
124
|
-
const
|
|
186
|
+
const result = await printUrlToPdf({
|
|
125
187
|
root,
|
|
126
|
-
url:
|
|
188
|
+
url: pressPrintUrl(host, port, selection.slug),
|
|
127
189
|
outPath,
|
|
128
190
|
waitForReady: waitForPrintReady,
|
|
129
191
|
debuggingPortBase: 9300,
|
|
130
192
|
debuggingPortRange: 600,
|
|
131
193
|
profilePrefix: "chrome-pdf",
|
|
132
194
|
});
|
|
133
|
-
|
|
195
|
+
const pageCount = result?.pageCount ?? result;
|
|
196
|
+
const pressLabel = selection.slug ? ` (press: ${selection.title || selection.slug})` : "";
|
|
197
|
+
console.log(`${pageCount} OpenPress pages printed to PDF${pressLabel}`);
|
|
134
198
|
} finally {
|
|
135
199
|
await stopChildProcess(server);
|
|
136
200
|
}
|
|
137
201
|
|
|
138
|
-
return { pdfPath: outPath };
|
|
202
|
+
return { pdfPath: outPath, pressSlug: selection.slug };
|
|
139
203
|
}
|
|
140
204
|
|
|
141
205
|
export async function buildReactImages({
|
|
@@ -147,18 +211,27 @@ export async function buildReactImages({
|
|
|
147
211
|
noBuild = false,
|
|
148
212
|
recurse,
|
|
149
213
|
pageSelector = null,
|
|
214
|
+
pressSlug = null,
|
|
150
215
|
}) {
|
|
151
216
|
config ??= await loadConfig(root);
|
|
152
|
-
outDir ??= path.join(config.paths.outputDir, "images");
|
|
153
217
|
const renderCode = await buildReactStatic({ root, noBuild, recurse });
|
|
154
218
|
if (renderCode !== 0) throw new Error(`React render failed with exit code ${renderCode}`);
|
|
219
|
+
|
|
220
|
+
const selection = pressSlug
|
|
221
|
+
? await resolvePressSelection({ outputDir: config.paths.outputDir, slug: pressSlug })
|
|
222
|
+
: { slug: "", title: "", knownSlugs: [] };
|
|
223
|
+
|
|
224
|
+
if (!outDir) {
|
|
225
|
+
const folder = selection.slug ? `images-${selection.slug}` : "images";
|
|
226
|
+
outDir = path.join(config.paths.outputDir, folder);
|
|
227
|
+
}
|
|
155
228
|
await fs.mkdir(outDir, { recursive: true });
|
|
156
229
|
|
|
157
230
|
const server = await startStaticServer(root, config, host, port);
|
|
158
231
|
try {
|
|
159
232
|
const result = await captureUrlPagesToPng({
|
|
160
233
|
root,
|
|
161
|
-
url:
|
|
234
|
+
url: pressPrintUrl(host, port, selection.slug),
|
|
162
235
|
outDir,
|
|
163
236
|
waitForReady: waitForPrintReady,
|
|
164
237
|
debuggingPortBase: 9700,
|
|
@@ -166,15 +239,17 @@ export async function buildReactImages({
|
|
|
166
239
|
profilePrefix: "chrome-image",
|
|
167
240
|
pageSelector,
|
|
168
241
|
});
|
|
169
|
-
const
|
|
242
|
+
const pressLabel = selection.slug ? ` (press: ${selection.title || selection.slug})` : "";
|
|
243
|
+
const countLabel = pageSelector
|
|
170
244
|
? `${result.files.length}/${result.pageCount} OpenPress pages exported to PNG`
|
|
171
245
|
: `${result.files.length} OpenPress pages exported to PNG`;
|
|
172
|
-
console.log(
|
|
246
|
+
console.log(`${countLabel}${pressLabel}`);
|
|
173
247
|
return {
|
|
174
248
|
outDir,
|
|
175
249
|
files: result.files,
|
|
176
250
|
pageCount: result.pageCount,
|
|
177
251
|
selectedPageNumbers: result.selectedPageNumbers,
|
|
252
|
+
pressSlug: selection.slug,
|
|
178
253
|
};
|
|
179
254
|
} finally {
|
|
180
255
|
await stopChildProcess(server);
|
|
@@ -223,18 +298,19 @@ export function startStaticServer(root, config, host, port) {
|
|
|
223
298
|
});
|
|
224
299
|
}
|
|
225
300
|
|
|
226
|
-
export async function writePdfStageDeployConfig(root, source, config) {
|
|
301
|
+
export async function writePdfStageDeployConfig(root, source, config, { pdfFilename = config.pdf.filename } = {}) {
|
|
227
302
|
const deployRoot = path.resolve(root, source);
|
|
228
303
|
const openpressDir = path.join(deployRoot, "openpress");
|
|
304
|
+
const pdfHref = publicPdfHrefForFilename(pdfFilename);
|
|
229
305
|
await fs.mkdir(openpressDir, { recursive: true });
|
|
230
306
|
await fs.writeFile(
|
|
231
307
|
path.join(openpressDir, "deploy.json"),
|
|
232
|
-
`${JSON.stringify({ pdf:
|
|
308
|
+
`${JSON.stringify({ pdf: pdfHref, deployed_at: new Date().toISOString() }, null, 2)}\n`,
|
|
233
309
|
"utf8",
|
|
234
310
|
);
|
|
235
311
|
await fs.writeFile(
|
|
236
312
|
path.join(deployRoot, "_headers"),
|
|
237
|
-
`${
|
|
313
|
+
`${pdfHref}\n Content-Type: application/pdf\n Content-Disposition: inline; filename="${pdfFilename}"\n`,
|
|
238
314
|
"utf8",
|
|
239
315
|
);
|
|
240
316
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { deploySync } from "../output/deploy-sync.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildReactPdf,
|
|
5
|
+
formatOpenPressCommand,
|
|
6
|
+
pressSuffixedFilename,
|
|
7
|
+
runCommand,
|
|
8
|
+
writePdfStageDeployConfig,
|
|
9
|
+
} from "./_shared.mjs";
|
|
4
10
|
|
|
5
11
|
export async function run({ root, config, options, recurse }) {
|
|
6
12
|
if (config.deploy.requiresConfirmation === true && !options.confirm) {
|
|
@@ -10,11 +16,15 @@ export async function run({ root, config, options, recurse }) {
|
|
|
10
16
|
const source = config.deploy.source;
|
|
11
17
|
const projectName = config.deploy.projectName;
|
|
12
18
|
const commitDirty = config.deploy.commitDirty;
|
|
19
|
+
const pressSlug = normalizePressSlug(options.press);
|
|
20
|
+
const pdfFilename = pressSuffixedFilename(config.pdf.filename, pressSlug);
|
|
21
|
+
const pdfArgs = ["pdf", ".", "--output", `${source}/${pdfFilename}`];
|
|
22
|
+
if (pressSlug) pdfArgs.push("--press", pressSlug);
|
|
13
23
|
if (options.dryRun) {
|
|
14
24
|
console.log("OpenPress deploy dry run");
|
|
15
25
|
console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
|
|
16
26
|
console.log(`Step: deploy-sync (copy ${config.outputDir} → ${source})`);
|
|
17
|
-
console.log(`Command: ${formatOpenPressCommand(
|
|
27
|
+
console.log(`Command: ${formatOpenPressCommand(pdfArgs)}`);
|
|
18
28
|
console.log(`Step: write ${source}/openpress/deploy.json with deployment metadata`);
|
|
19
29
|
console.log(`Command: npx wrangler pages deploy ${source}${projectName ? ` --project-name=${projectName}` : ""}${commitDirty ? " --commit-dirty=true" : ""}`);
|
|
20
30
|
return 0;
|
|
@@ -22,10 +32,15 @@ export async function run({ root, config, options, recurse }) {
|
|
|
22
32
|
const renderCode = await recurse("render", [root, "--renderer", "react"]);
|
|
23
33
|
if (renderCode !== 0) return renderCode;
|
|
24
34
|
await deploySync(root, config.outputDir, source);
|
|
25
|
-
await buildReactPdf({ root, config, outPath: path.resolve(root, source,
|
|
26
|
-
await writePdfStageDeployConfig(root, source, config);
|
|
35
|
+
await buildReactPdf({ root, config, outPath: path.resolve(root, source, pdfFilename), noBuild: true, recurse, pressSlug });
|
|
36
|
+
await writePdfStageDeployConfig(root, source, config, { pdfFilename });
|
|
27
37
|
const wranglerArgs = ["wrangler", "pages", "deploy", source];
|
|
28
38
|
if (projectName) wranglerArgs.push(`--project-name=${projectName}`);
|
|
29
39
|
if (commitDirty) wranglerArgs.push("--commit-dirty=true");
|
|
30
40
|
return runCommand("npx", wranglerArgs, root);
|
|
31
41
|
}
|
|
42
|
+
|
|
43
|
+
function normalizePressSlug(value) {
|
|
44
|
+
if (typeof value !== "string") return "";
|
|
45
|
+
return value.trim().replace(/^\/+|\/+$/g, "");
|
|
46
|
+
}
|
|
@@ -3,20 +3,25 @@ import { STATIC_SERVER, buildReactImages, formatNodeScriptCommand, formatOpenPre
|
|
|
3
3
|
import { parsePageSelector } from "../runtime/page-selector.mjs";
|
|
4
4
|
|
|
5
5
|
export async function run({ root, config, options, recurse }) {
|
|
6
|
-
const outputDir = options.output ? path.resolve(root, options.output) :
|
|
6
|
+
const outputDir = options.output ? path.resolve(root, options.output) : undefined;
|
|
7
7
|
const host = options.host ?? "127.0.0.1";
|
|
8
8
|
const port = options.port ?? "5186";
|
|
9
9
|
|
|
10
10
|
const pageSelector = options.pages ? parsePageSelector(options.pages) : null;
|
|
11
|
+
const pressSlug = options.press ?? null;
|
|
11
12
|
|
|
12
13
|
if (options.dryRun) {
|
|
14
|
+
const pressPath = pressSlug ? `/${String(pressSlug).replace(/^\/+|\/+$/g, "")}` : "";
|
|
15
|
+
const previewDir = outputDir
|
|
16
|
+
?? path.join(config.paths.outputDir, pressSlug ? `images-${String(pressSlug).replace(/^\/+|\/+$/g, "")}` : "images");
|
|
13
17
|
console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
|
|
14
18
|
console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
|
|
15
|
-
console.log(`Chrome image export URL: http://${host}:${port}/?print=1`);
|
|
19
|
+
console.log(`Chrome image export URL: http://${host}:${port}${pressPath}/?print=1`);
|
|
20
|
+
if (pressSlug) console.log(`Press: ${pressSlug} (validated against workspace manifest at run time)`);
|
|
16
21
|
if (pageSelector) {
|
|
17
22
|
console.log(`Page selector: ${options.pages} (resolved at capture time against the rendered page count)`);
|
|
18
23
|
}
|
|
19
|
-
console.log(`Output: ${path.relative(root, path.join(
|
|
24
|
+
console.log(`Output: ${path.relative(root, path.join(previewDir, "page-001.png"))}`);
|
|
20
25
|
return 0;
|
|
21
26
|
}
|
|
22
27
|
|
|
@@ -29,6 +34,7 @@ export async function run({ root, config, options, recurse }) {
|
|
|
29
34
|
noBuild: options.noBuild,
|
|
30
35
|
recurse,
|
|
31
36
|
pageSelector,
|
|
37
|
+
pressSlug,
|
|
32
38
|
});
|
|
33
39
|
|
|
34
40
|
const suffix = pageSelector
|
package/engine/commands/pdf.mjs
CHANGED
|
@@ -7,9 +7,11 @@ export async function run({ root, config, options, recurse }) {
|
|
|
7
7
|
const relOutput = path.relative(root, outputPath ?? config.paths.pdf);
|
|
8
8
|
const host = options.host ?? "127.0.0.1";
|
|
9
9
|
const port = options.port ?? "5185";
|
|
10
|
+
const pressPath = options.press ? `/${String(options.press).replace(/^\/+|\/+$/g, "")}` : "";
|
|
10
11
|
console.log(`Command: ${formatOpenPressCommand(["render", ".", "--renderer", "react"])}`);
|
|
11
12
|
console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
|
|
12
|
-
console.log(`Command: Chrome --print-to-pdf=${relOutput} http://${host}:${port}/?print=1`);
|
|
13
|
+
console.log(`Command: Chrome --print-to-pdf=${relOutput} http://${host}:${port}${pressPath}/?print=1`);
|
|
14
|
+
if (options.press) console.log(`Press: ${options.press} (validated against workspace manifest at run time)`);
|
|
13
15
|
return 0;
|
|
14
16
|
}
|
|
15
17
|
const result = await buildReactPdf({
|
|
@@ -20,6 +22,7 @@ export async function run({ root, config, options, recurse }) {
|
|
|
20
22
|
port: options.port,
|
|
21
23
|
noBuild: options.noBuild,
|
|
22
24
|
recurse,
|
|
25
|
+
pressSlug: options.press ?? null,
|
|
23
26
|
});
|
|
24
27
|
console.log(`OpenPress PDF: ${path.relative(root, result.pdfPath)}`);
|
|
25
28
|
return 0;
|
|
@@ -101,6 +101,100 @@ export const DEFAULT_PRINT_VIEWPORT = Object.freeze({
|
|
|
101
101
|
mobile: false,
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
export function pageGeometryProbeExpression() {
|
|
105
|
+
return `(() => {
|
|
106
|
+
if (!document.body) return null;
|
|
107
|
+
// OpenPressRuntime sets --openpress-page-width / -height on the
|
|
108
|
+
// PrintDocument <main> via inline style. The workspace's global
|
|
109
|
+
// theme also defines a *default* --openpress-page-width on :root
|
|
110
|
+
// (e.g. 210mm for the A4 preset). If we let getComputedStyle fall
|
|
111
|
+
// back to :root we will silently return that default before the
|
|
112
|
+
// print document has even rendered — and the per-document override
|
|
113
|
+
// never gets a chance to apply. Require the actual print surface
|
|
114
|
+
// so the probe waits until React paints it instead of locking in
|
|
115
|
+
// the wrong size from the workspace stylesheet.
|
|
116
|
+
const target = document.querySelector('[data-openpress-print-document="true"]')
|
|
117
|
+
|| document.querySelector('.openpress-html-page');
|
|
118
|
+
if (!target) return null;
|
|
119
|
+
const cs = getComputedStyle(target);
|
|
120
|
+
const widthStr = cs.getPropertyValue('--openpress-page-width').trim();
|
|
121
|
+
const heightStr = cs.getPropertyValue('--openpress-page-height').trim();
|
|
122
|
+
if (!widthStr || !heightStr) return null;
|
|
123
|
+
const helper = document.createElement('div');
|
|
124
|
+
helper.style.position = 'absolute';
|
|
125
|
+
helper.style.left = '-99999px';
|
|
126
|
+
helper.style.top = '-99999px';
|
|
127
|
+
helper.style.visibility = 'hidden';
|
|
128
|
+
helper.style.pointerEvents = 'none';
|
|
129
|
+
helper.style.width = widthStr;
|
|
130
|
+
helper.style.height = heightStr;
|
|
131
|
+
document.body.appendChild(helper);
|
|
132
|
+
const rect = helper.getBoundingClientRect();
|
|
133
|
+
helper.remove();
|
|
134
|
+
if (!Number.isFinite(rect.width) || !Number.isFinite(rect.height) || rect.width <= 0 || rect.height <= 0) return null;
|
|
135
|
+
return { width: rect.width, height: rect.height };
|
|
136
|
+
})()`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function measurePageGeometryPx(client, { timeoutMs = 5000, pollIntervalMs = 50 } = {}) {
|
|
140
|
+
const deadline = Date.now() + timeoutMs;
|
|
141
|
+
while (Date.now() < deadline) {
|
|
142
|
+
const result = await client.send("Runtime.evaluate", {
|
|
143
|
+
returnByValue: true,
|
|
144
|
+
expression: pageGeometryProbeExpression(),
|
|
145
|
+
});
|
|
146
|
+
const dims = result.result?.value;
|
|
147
|
+
if (dims && Number.isFinite(dims.width) && dims.width > 0) {
|
|
148
|
+
return { width: dims.width, height: dims.height };
|
|
149
|
+
}
|
|
150
|
+
await delay(pollIntervalMs);
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function syncViewportToPageGeometry(client, viewport, options = {}) {
|
|
156
|
+
const dims = await measurePageGeometryPx(client, options);
|
|
157
|
+
if (!dims) return { viewport, pageDimensionsPx: null };
|
|
158
|
+
const targetWidth = Math.max(viewport.width, Math.ceil(dims.width));
|
|
159
|
+
const targetHeight = Math.max(viewport.height, Math.ceil(dims.height));
|
|
160
|
+
if (targetWidth === viewport.width && targetHeight === viewport.height) {
|
|
161
|
+
return { viewport, pageDimensionsPx: dims };
|
|
162
|
+
}
|
|
163
|
+
const next = { ...viewport, width: targetWidth, height: targetHeight };
|
|
164
|
+
await client.send("Emulation.setDeviceMetricsOverride", next);
|
|
165
|
+
return { viewport: next, pageDimensionsPx: dims };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Chrome's Page.printToPDF takes paperWidth / paperHeight in inches and
|
|
169
|
+
// honors them when preferCSSPageSize falls through. Because our @page
|
|
170
|
+
// rule reads CSS custom properties scoped to <main> instead of :root,
|
|
171
|
+
// preferCSSPageSize cannot resolve the size in headless Chrome — we have
|
|
172
|
+
// to pass the inches explicitly. Convert from CSS px at the 1in = 96px
|
|
173
|
+
// rate the rest of the runtime already assumes.
|
|
174
|
+
//
|
|
175
|
+
// Wider-than-tall geometries (slide 16:9, landscape pages) also need
|
|
176
|
+
// `landscape: true`: with `landscape: false` Chrome silently rotates the
|
|
177
|
+
// MediaBox to portrait so the short side becomes the width, leaving the
|
|
178
|
+
// content laid out for the wide canvas but cropped against the short page.
|
|
179
|
+
export function pageDimensionsPxToPaperInches(dims) {
|
|
180
|
+
if (!dims || !Number.isFinite(dims.width) || dims.width <= 0) return null;
|
|
181
|
+
if (!Number.isFinite(dims.height) || dims.height <= 0) return null;
|
|
182
|
+
// Chrome's printToPDF semantics: paperWidth/paperHeight are taken as
|
|
183
|
+
// *portrait* page dimensions, and `landscape: true` then rotates the
|
|
184
|
+
// canvas 90°. So for a 1920×1080 slide we have to pass the short side
|
|
185
|
+
// as paperWidth (11.25"), the long side as paperHeight (20"), and
|
|
186
|
+
// landscape: true — Chrome will produce a 20"×11.25" landscape page
|
|
187
|
+
// whose content frame matches the original 1920×1080 layout.
|
|
188
|
+
const widthIn = dims.width / 96;
|
|
189
|
+
const heightIn = dims.height / 96;
|
|
190
|
+
const landscape = widthIn > heightIn;
|
|
191
|
+
return {
|
|
192
|
+
paperWidth: landscape ? heightIn : widthIn,
|
|
193
|
+
paperHeight: landscape ? widthIn : heightIn,
|
|
194
|
+
landscape,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
104
198
|
export async function printUrlToPdf({
|
|
105
199
|
root,
|
|
106
200
|
url,
|
|
@@ -139,6 +233,13 @@ export async function printUrlToPdf({
|
|
|
139
233
|
try {
|
|
140
234
|
await preparePdfPage(client, { viewport });
|
|
141
235
|
await client.send("Page.navigate", { url });
|
|
236
|
+
// Widen the headless viewport when the document's page geometry is
|
|
237
|
+
// wider than the default A4 viewport, so layout uses the full page
|
|
238
|
+
// width before pagination. Paper size itself is driven by the
|
|
239
|
+
// @page rule in print-route.css, which now resolves
|
|
240
|
+
// --openpress-page-width / -height from :root (PrintDocument
|
|
241
|
+
// mirrors the per-document theme vars onto the document element).
|
|
242
|
+
await syncViewportToPageGeometry(client, viewport);
|
|
142
243
|
const readyResult = await waitForReady(client);
|
|
143
244
|
warnAboutOverflowingPages("PDF", readyResult);
|
|
144
245
|
const result = await client.send("Page.printToPDF", {
|
|
@@ -205,6 +306,7 @@ export async function captureUrlPagesToPng({
|
|
|
205
306
|
try {
|
|
206
307
|
await preparePdfPage(client, { viewport });
|
|
207
308
|
await client.send("Page.navigate", { url });
|
|
309
|
+
await syncViewportToPageGeometry(client, viewport);
|
|
208
310
|
const readyResult = await waitForReady(client);
|
|
209
311
|
warnAboutOverflowingPages("image", readyResult);
|
|
210
312
|
const pageCount = readyResult?.pageCount ?? 0;
|
|
@@ -208,13 +208,18 @@ async function handleLocalPdfExportRequest(req, res) {
|
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
const
|
|
212
|
-
const
|
|
211
|
+
const body = await readJsonBody(req);
|
|
212
|
+
const slug = normalizePressSlug(body?.press);
|
|
213
|
+
const result = await runLocalPdfExport(slug);
|
|
214
|
+
const pdfPath = pressPdfPath(slug);
|
|
215
|
+
const exists = await fileExists(pdfPath);
|
|
216
|
+
const command = slug ? `open-press pdf . --press ${slug}` : "open-press pdf .";
|
|
217
|
+
const pdfUrl = `/__openpress/local-pdf-file?${slug ? `press=${encodeURIComponent(slug)}&` : ""}ts=${Date.now()}`;
|
|
213
218
|
writeJson(res, result.code === 0 && exists ? 200 : 500, {
|
|
214
219
|
ok: result.code === 0 && exists,
|
|
215
220
|
code: result.code,
|
|
216
|
-
pdf:
|
|
217
|
-
command
|
|
221
|
+
pdf: pdfUrl,
|
|
222
|
+
command,
|
|
218
223
|
stdout: result.stdout,
|
|
219
224
|
stderr: result.stderr,
|
|
220
225
|
});
|
|
@@ -226,11 +231,15 @@ async function handleLocalPdfFileRequest(req, res) {
|
|
|
226
231
|
return;
|
|
227
232
|
}
|
|
228
233
|
|
|
234
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
235
|
+
const slug = normalizePressSlug(url.searchParams.get("press"));
|
|
236
|
+
const pdfPath = pressPdfPath(slug);
|
|
237
|
+
const filename = pressFilename(config.pdf.filename, slug);
|
|
229
238
|
try {
|
|
230
|
-
const body = await fs.readFile(
|
|
239
|
+
const body = await fs.readFile(pdfPath);
|
|
231
240
|
res.writeHead(200, {
|
|
232
241
|
"Content-Type": "application/pdf",
|
|
233
|
-
"Content-Disposition": `inline; filename="${
|
|
242
|
+
"Content-Disposition": `inline; filename="${filename}"`,
|
|
234
243
|
"Cache-Control": "no-store",
|
|
235
244
|
});
|
|
236
245
|
res.end(body);
|
|
@@ -239,12 +248,46 @@ async function handleLocalPdfFileRequest(req, res) {
|
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
|
|
251
|
+
function normalizePressSlug(value) {
|
|
252
|
+
if (typeof value !== "string") return "";
|
|
253
|
+
return value.trim().replace(/^\/+|\/+$/g, "");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function pressFilename(baseFilename, slug) {
|
|
257
|
+
if (!slug) return baseFilename;
|
|
258
|
+
const ext = path.extname(baseFilename);
|
|
259
|
+
const stem = ext ? baseFilename.slice(0, -ext.length) : baseFilename;
|
|
260
|
+
return `${stem}-${slug}${ext}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function pressPdfPath(slug) {
|
|
264
|
+
return path.join(config.outputDir, pressFilename(config.pdf.filename, slug));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function readJsonBody(req) {
|
|
268
|
+
try {
|
|
269
|
+
const chunks = [];
|
|
270
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
271
|
+
if (chunks.length === 0) return null;
|
|
272
|
+
const text = Buffer.concat(chunks.map((chunk) => (typeof chunk === "string" ? Buffer.from(chunk) : chunk))).toString("utf8");
|
|
273
|
+
if (!text.trim()) return null;
|
|
274
|
+
return JSON.parse(text);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
242
280
|
async function handleDeployRequest(req, res) {
|
|
243
281
|
if (req.method !== "POST") {
|
|
244
282
|
writeJson(res, 405, { ok: false, message: "Deploy endpoint requires POST." });
|
|
245
283
|
return;
|
|
246
284
|
}
|
|
247
285
|
|
|
286
|
+
const body = await readJsonBody(req);
|
|
287
|
+
const slug = normalizePressSlug(body?.press);
|
|
288
|
+
const command = slug ? `open-press deploy . --confirm --press ${slug}` : "open-press deploy . --confirm";
|
|
289
|
+
const pdfFilename = pressFilename(config.pdf.filename, slug);
|
|
290
|
+
|
|
248
291
|
if (!isDeployConfigured()) {
|
|
249
292
|
writeJson(res, 400, {
|
|
250
293
|
ok: false,
|
|
@@ -254,15 +297,15 @@ async function handleDeployRequest(req, res) {
|
|
|
254
297
|
deploy_adapter: config.deploy.adapter,
|
|
255
298
|
deploy_source: config.deploy.source,
|
|
256
299
|
deploy_project_name: config.deploy.projectName,
|
|
257
|
-
command
|
|
300
|
+
command,
|
|
258
301
|
});
|
|
259
302
|
return;
|
|
260
303
|
}
|
|
261
304
|
|
|
262
|
-
const result = await runDeploy();
|
|
305
|
+
const result = await runDeploy(slug);
|
|
263
306
|
const deployedUrl = extractDeployUrl(result.stdout);
|
|
264
307
|
if (result.code === 0 && deployedUrl) {
|
|
265
|
-
await writeDeploymentPublicUrl(deployedUrl);
|
|
308
|
+
await writeDeploymentPublicUrl(deployedUrl, pdfFilename);
|
|
266
309
|
}
|
|
267
310
|
const deploymentInfo = await readDeploymentInfo();
|
|
268
311
|
const publicUrl = deployedUrl ?? deploymentInfo.public_url;
|
|
@@ -270,10 +313,10 @@ async function handleDeployRequest(req, res) {
|
|
|
270
313
|
ok: result.code === 0,
|
|
271
314
|
code: result.code,
|
|
272
315
|
deployed_at: deploymentInfo.deployed_at,
|
|
273
|
-
pdf: deployedUrl ? `${deployedUrl}/${
|
|
316
|
+
pdf: deployedUrl ? `${deployedUrl}/${pdfFilename}` : deploymentInfo.pdf,
|
|
274
317
|
public_url: publicUrl,
|
|
275
318
|
dirty: false,
|
|
276
|
-
command
|
|
319
|
+
command,
|
|
277
320
|
stdout: result.stdout,
|
|
278
321
|
stderr: result.stderr,
|
|
279
322
|
});
|
|
@@ -356,9 +399,11 @@ async function handleMediaFileRequest(req, res, url) {
|
|
|
356
399
|
}
|
|
357
400
|
}
|
|
358
401
|
|
|
359
|
-
function runLocalPdfExport() {
|
|
402
|
+
function runLocalPdfExport(slug = "") {
|
|
403
|
+
const cliArgs = [CLI_ENTRY, "pdf", "."];
|
|
404
|
+
if (slug) cliArgs.push("--press", slug);
|
|
360
405
|
return new Promise((resolve) => {
|
|
361
|
-
const child = spawn("node",
|
|
406
|
+
const child = spawn("node", cliArgs, {
|
|
362
407
|
cwd: workspace,
|
|
363
408
|
shell: false,
|
|
364
409
|
});
|
|
@@ -379,9 +424,11 @@ function runLocalPdfExport() {
|
|
|
379
424
|
});
|
|
380
425
|
}
|
|
381
426
|
|
|
382
|
-
function runDeploy() {
|
|
427
|
+
function runDeploy(slug = "") {
|
|
428
|
+
const cliArgs = [CLI_ENTRY, "deploy", ".", "--confirm"];
|
|
429
|
+
if (slug) cliArgs.push("--press", slug);
|
|
383
430
|
return new Promise((resolve) => {
|
|
384
|
-
const child = spawn("node",
|
|
431
|
+
const child = spawn("node", cliArgs, {
|
|
385
432
|
cwd: workspace,
|
|
386
433
|
shell: false,
|
|
387
434
|
});
|
|
@@ -445,7 +492,7 @@ async function readDeploymentInfo() {
|
|
|
445
492
|
}
|
|
446
493
|
}
|
|
447
494
|
|
|
448
|
-
async function writeDeploymentPublicUrl(publicUrl) {
|
|
495
|
+
async function writeDeploymentPublicUrl(publicUrl, pdfFilename = config.pdf.filename) {
|
|
449
496
|
let deployConfig = {};
|
|
450
497
|
try {
|
|
451
498
|
deployConfig = JSON.parse(await fs.readFile(config.paths.deployMetadata, "utf8"));
|
|
@@ -455,7 +502,7 @@ async function writeDeploymentPublicUrl(publicUrl) {
|
|
|
455
502
|
await fs.mkdir(path.dirname(config.paths.deployMetadata), { recursive: true });
|
|
456
503
|
await fs.writeFile(
|
|
457
504
|
config.paths.deployMetadata,
|
|
458
|
-
`${JSON.stringify({ ...deployConfig, pdf: `${publicUrl}/${
|
|
505
|
+
`${JSON.stringify({ ...deployConfig, pdf: `${publicUrl}/${pdfFilename}`, public_url: publicUrl }, null, 2)}\n`,
|
|
459
506
|
"utf8",
|
|
460
507
|
);
|
|
461
508
|
}
|
|
@@ -9,6 +9,7 @@ import { pathToFileURL } from "node:url";
|
|
|
9
9
|
import React from "react";
|
|
10
10
|
import { documentRelativePath, pageToBlock } from "../output/page-block.mjs";
|
|
11
11
|
import { syncPublicAssets } from "../output/public-assets.mjs";
|
|
12
|
+
import { collectSourceTextFiles } from "../runtime/source-text-tools.mjs";
|
|
12
13
|
import { pageGeometryToTheme } from "../runtime/page-geometry.mjs";
|
|
13
14
|
import { normalizePageGeometry } from "../runtime/page-geometry.mjs";
|
|
14
15
|
import { createCaptionNumberingState, numberCaptionsInHtml } from "./caption-numbering.mjs";
|
|
@@ -125,6 +126,27 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
125
126
|
const workspacePath = path.join(entry.config.paths.publicDir, "workspace.json");
|
|
126
127
|
await fs.writeFile(workspacePath, JSON.stringify(workspaceManifest, null, 2), "utf8");
|
|
127
128
|
|
|
129
|
+
// Static search corpus — raw text of every content source file in the
|
|
130
|
+
// workspace, shipped as JSON so the deployed reader can search without
|
|
131
|
+
// a backend. Lives next to workspace.json so the public route can
|
|
132
|
+
// GET /openpress/search-corpus.json once and grep in memory. Workspace-
|
|
133
|
+
// scoped (not per-press) because most workspaces have a single Press
|
|
134
|
+
// and corpus size for typical content is small (<1MB raw); per-press
|
|
135
|
+
// scoping can come later if multi-Press search noise becomes a problem.
|
|
136
|
+
const corpusFiles = await collectSourceTextFiles(entry.config, { scope: "content" });
|
|
137
|
+
const corpus = {
|
|
138
|
+
kind: "search-corpus",
|
|
139
|
+
version: 1,
|
|
140
|
+
files: corpusFiles.map((file) => ({
|
|
141
|
+
scope: file.scope,
|
|
142
|
+
file: file.name,
|
|
143
|
+
path: file.relativePath,
|
|
144
|
+
text: file.text,
|
|
145
|
+
})),
|
|
146
|
+
};
|
|
147
|
+
const corpusPath = path.join(entry.config.paths.publicDir, "search-corpus.json");
|
|
148
|
+
await fs.writeFile(corpusPath, JSON.stringify(corpus), "utf8");
|
|
149
|
+
|
|
128
150
|
if (syncAssets) {
|
|
129
151
|
await syncPublicAssets(workspaceRoot, entry.config.paths.publicDir, entry.config);
|
|
130
152
|
}
|
package/package.json
CHANGED
|
@@ -230,7 +230,10 @@ export function OpenPressApp() {
|
|
|
230
230
|
const presentationSlug = state.activeSlug || currentRouteFromLocation().slug;
|
|
231
231
|
const openPresentation = state.document.meta.type === "slides" && presentationSlug
|
|
232
232
|
? (pageIndex: number) => {
|
|
233
|
-
|
|
233
|
+
const slug = normalizeSlug(presentationSlug);
|
|
234
|
+
const pathname = slug ? `/${slug}` : "/";
|
|
235
|
+
const hash = `#page-${String(pageIndex + 1).padStart(2, "0")}`;
|
|
236
|
+
window.open(`${pathname}?fullscreen=1${hash}`, "_blank", "noopener,noreferrer");
|
|
234
237
|
}
|
|
235
238
|
: undefined;
|
|
236
239
|
|
|
@@ -250,6 +253,7 @@ export function OpenPressApp() {
|
|
|
250
253
|
document={state.document}
|
|
251
254
|
runtimeMode={state.runtimeMode}
|
|
252
255
|
deploymentInfo={state.deploymentInfo}
|
|
256
|
+
activeSlug={state.activeSlug}
|
|
253
257
|
onDocumentRefresh={refreshDocument}
|
|
254
258
|
onOpenPresentation={openPresentation}
|
|
255
259
|
onExitPresentation={exitPresentation}
|