@open-press/core 1.1.3 → 1.2.0
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 +2 -2
- package/engine/commands/_shared.mjs +91 -9
- package/engine/commands/image.mjs +20 -4
- package/engine/commands/pdf.mjs +4 -1
- package/engine/output/chrome-pdf.mjs +257 -52
- package/engine/output/static-server.mjs +48 -8
- package/engine/react/document-export.mjs +22 -0
- package/engine/runtime/inspection.mjs +65 -7
- package/engine/runtime/page-selector.mjs +87 -0
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +1 -0
- package/src/openpress/app/OpenPressRuntime.tsx +59 -5
- package/src/openpress/reader/PublicReaderPage.tsx +163 -74
- package/src/openpress/reader/SlidePresentationPage.tsx +8 -3
- 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 +19 -16
- package/src/openpress/workbench/actions/SearchControl.tsx +32 -43
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +14 -3
- package/src/openpress/workbench/inspector/useInspectorComments.ts +6 -6
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +44 -18
- package/vite.config.ts +50 -8
|
@@ -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,6 +248,35 @@ 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." });
|
|
@@ -356,9 +394,11 @@ async function handleMediaFileRequest(req, res, url) {
|
|
|
356
394
|
}
|
|
357
395
|
}
|
|
358
396
|
|
|
359
|
-
function runLocalPdfExport() {
|
|
397
|
+
function runLocalPdfExport(slug = "") {
|
|
398
|
+
const cliArgs = [CLI_ENTRY, "pdf", "."];
|
|
399
|
+
if (slug) cliArgs.push("--press", slug);
|
|
360
400
|
return new Promise((resolve) => {
|
|
361
|
-
const child = spawn("node",
|
|
401
|
+
const child = spawn("node", cliArgs, {
|
|
362
402
|
cwd: workspace,
|
|
363
403
|
shell: false,
|
|
364
404
|
});
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -250,6 +250,7 @@ export function OpenPressApp() {
|
|
|
250
250
|
document={state.document}
|
|
251
251
|
runtimeMode={state.runtimeMode}
|
|
252
252
|
deploymentInfo={state.deploymentInfo}
|
|
253
|
+
activeSlug={state.activeSlug}
|
|
253
254
|
onDocumentRefresh={refreshDocument}
|
|
254
255
|
onOpenPresentation={openPresentation}
|
|
255
256
|
onExitPresentation={exitPresentation}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, type CSSProperties } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
|
2
2
|
import { PrintDocument, PublicViewer, SlidePresentationPage } from "../reader";
|
|
3
3
|
import { isPresentationModeLocation, isPrintModeLocation, isWorkspaceModeLocation } from "../shared";
|
|
4
4
|
import { HtmlWorkbench } from "../workbench";
|
|
@@ -15,6 +15,12 @@ interface OpenPressRuntimeProps {
|
|
|
15
15
|
document: ReaderDocument;
|
|
16
16
|
runtimeMode?: OpenPressRuntimeMode;
|
|
17
17
|
deploymentInfo?: DeploymentInfo;
|
|
18
|
+
// Active Press slug — supplied by OpenPressApp when the active document
|
|
19
|
+
// came from a multi-Press workspace. The workbench passes this through to
|
|
20
|
+
// useDeploymentWorkbench so the local PDF export endpoint can target the
|
|
21
|
+
// right Press instead of defaulting to the first one and producing a
|
|
22
|
+
// "0 pages observed" timeout.
|
|
23
|
+
activeSlug?: string;
|
|
18
24
|
onDocumentRefresh?: () => void | Promise<void>;
|
|
19
25
|
onOpenPresentation?: (pageIndex: number) => void;
|
|
20
26
|
onExitPresentation?: (pageIndex: number) => void;
|
|
@@ -28,6 +34,7 @@ export function OpenPressRuntime({
|
|
|
28
34
|
document,
|
|
29
35
|
runtimeMode,
|
|
30
36
|
deploymentInfo = { online: false },
|
|
37
|
+
activeSlug,
|
|
31
38
|
onDocumentRefresh,
|
|
32
39
|
onOpenPresentation,
|
|
33
40
|
onExitPresentation,
|
|
@@ -35,19 +42,29 @@ export function OpenPressRuntime({
|
|
|
35
42
|
}: OpenPressRuntimeProps) {
|
|
36
43
|
const style = themeToCssVariables(document.theme);
|
|
37
44
|
const htmlPages = document.blocks.filter((block): block is HtmlPageBlock => block.kind === "htmlPage");
|
|
45
|
+
// The mode flags below all read window.location synchronously. They
|
|
46
|
+
// would otherwise stay frozen at their mount-time values when
|
|
47
|
+
// OpenPressApp re-renders us in response to a client-side URL change
|
|
48
|
+
// (e.g. /<slug>/present -> /<slug>/preview after exiting the slide
|
|
49
|
+
// presenter), so the SlidePresentationPage exits to the wrong
|
|
50
|
+
// route-driven branch (PublicViewer instead of HtmlWorkbench) and the
|
|
51
|
+
// user sees the legacy public-viewer chrome until a hard reload.
|
|
52
|
+
// Bump a version on every pathname/search change so the memos
|
|
53
|
+
// re-evaluate exactly when the URL does.
|
|
54
|
+
const routeVersion = useLocationVersion();
|
|
38
55
|
const activeRuntimeMode = useMemo<OpenPressRuntimeMode>(() => {
|
|
39
56
|
if (runtimeMode) return runtimeMode;
|
|
40
57
|
if (typeof window === "undefined") return "preview";
|
|
41
58
|
return isPresentationModeLocation(window.location) ? "present" : "preview";
|
|
42
|
-
}, [runtimeMode]);
|
|
59
|
+
}, [runtimeMode, routeVersion]);
|
|
43
60
|
const workspaceMode = useMemo(() => {
|
|
44
61
|
if (typeof window === "undefined") return false;
|
|
45
62
|
return isWorkspaceModeLocation(window.location);
|
|
46
|
-
}, []);
|
|
63
|
+
}, [routeVersion]);
|
|
47
64
|
const printMode = useMemo(() => {
|
|
48
65
|
if (typeof window === "undefined") return false;
|
|
49
66
|
return isPrintModeLocation(window.location);
|
|
50
|
-
}, []);
|
|
67
|
+
}, [routeVersion]);
|
|
51
68
|
|
|
52
69
|
if (htmlPages.length > 0) {
|
|
53
70
|
if (printMode) {
|
|
@@ -74,8 +91,9 @@ export function OpenPressRuntime({
|
|
|
74
91
|
document={document}
|
|
75
92
|
pages={htmlPages}
|
|
76
93
|
style={style}
|
|
77
|
-
|
|
94
|
+
workspaceMode={workspaceMode}
|
|
78
95
|
deploymentInfo={deploymentInfo}
|
|
96
|
+
pressSlug={activeSlug ?? null}
|
|
79
97
|
onDocumentRefresh={onDocumentRefresh}
|
|
80
98
|
onOpenPresentation={onOpenPresentation}
|
|
81
99
|
onBackToWorkspace={onBackToWorkspace}
|
|
@@ -110,6 +128,42 @@ function EmptyState({ style, workspaceMode }: { style: CSSProperties; workspaceM
|
|
|
110
128
|
);
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
// Bump a counter whenever client-side navigation changes pathname /
|
|
132
|
+
// search / hash, so location-derived memos in OpenPressRuntime
|
|
133
|
+
// re-evaluate. popstate fires on browser back/forward; we also patch
|
|
134
|
+
// pushState / replaceState because the SPA itself calls those when
|
|
135
|
+
// the user opens a Press, exits a slide presenter, or toggles between
|
|
136
|
+
// /<slug>/preview and /<slug>/present.
|
|
137
|
+
function useLocationVersion() {
|
|
138
|
+
const [version, setVersion] = useState(0);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (typeof window === "undefined") return undefined;
|
|
141
|
+
const bump = () => setVersion((value) => value + 1);
|
|
142
|
+
window.addEventListener("popstate", bump);
|
|
143
|
+
window.addEventListener("hashchange", bump);
|
|
144
|
+
const history = window.history;
|
|
145
|
+
const originalPushState = history.pushState.bind(history);
|
|
146
|
+
const originalReplaceState = history.replaceState.bind(history);
|
|
147
|
+
history.pushState = function patchedPushState(...args) {
|
|
148
|
+
const result = originalPushState(...args);
|
|
149
|
+
bump();
|
|
150
|
+
return result;
|
|
151
|
+
} as typeof history.pushState;
|
|
152
|
+
history.replaceState = function patchedReplaceState(...args) {
|
|
153
|
+
const result = originalReplaceState(...args);
|
|
154
|
+
bump();
|
|
155
|
+
return result;
|
|
156
|
+
} as typeof history.replaceState;
|
|
157
|
+
return () => {
|
|
158
|
+
window.removeEventListener("popstate", bump);
|
|
159
|
+
window.removeEventListener("hashchange", bump);
|
|
160
|
+
history.pushState = originalPushState;
|
|
161
|
+
history.replaceState = originalReplaceState;
|
|
162
|
+
};
|
|
163
|
+
}, []);
|
|
164
|
+
return version;
|
|
165
|
+
}
|
|
166
|
+
|
|
113
167
|
function themeToCssVariables(theme?: Theme) {
|
|
114
168
|
const style: CSSProperties & Record<`--${string}`, string> = {
|
|
115
169
|
"--openpress-font-family": theme?.fontFamily ?? "'Noto Sans TC', 'PingFang TC', sans-serif",
|