@open-press/core 0.7.0 → 0.8.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/commands/dev.mjs +2 -2
- package/engine/commands/upgrade.mjs +47 -5
- package/engine/output/chrome-pdf.mjs +18 -3
- package/engine/output/static-server.mjs +39 -0
- package/engine/react/comment-endpoint.mjs +13 -39
- package/engine/react/comment-marker.mjs +30 -6
- package/engine/react/document-entry.mjs +11 -0
- package/engine/react/document-export.mjs +45 -5
- package/engine/react/http-json.mjs +24 -0
- package/engine/react/mdx-compile.mjs +187 -3
- package/engine/react/measurement-css.mjs +93 -1
- package/engine/react/object-entities.mjs +119 -0
- package/engine/react/pipeline/allocate.mjs +10 -7
- package/engine/react/pipeline/frame-measurement.mjs +40 -9
- package/engine/react/project-asset-endpoint.mjs +6 -24
- package/engine/react/source-edit-endpoint.d.mts +10 -0
- package/engine/react/source-edit-endpoint.mjs +75 -0
- package/engine/react/sources/mdx-resolver.mjs +12 -14
- package/engine/react/style-discovery.mjs +1 -4
- package/engine/runtime/file-walk.mjs +22 -0
- package/engine/runtime/inspection.mjs +1 -20
- package/engine/runtime/path-utils.mjs +20 -0
- package/engine/runtime/source-text-tools.d.mts +102 -0
- package/engine/runtime/source-text-tools.mjs +551 -16
- package/engine/runtime/source-workspace.mjs +4 -31
- package/package.json +1 -1
- package/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
- package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
- package/src/openpress/app/index.ts +2 -0
- package/src/openpress/core/Frame.tsx +9 -11
- package/src/openpress/core/FrameContext.tsx +8 -3
- package/src/openpress/core/MdxArea.tsx +11 -12
- package/src/openpress/core/cn.ts +4 -0
- package/src/openpress/core/index.tsx +2 -1
- package/src/openpress/core/primitives.tsx +29 -8
- package/src/openpress/core/types.ts +8 -0
- package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
- package/src/openpress/document-model/index.ts +6 -0
- package/src/openpress/document-model/objectEntityModel.ts +51 -0
- package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/src/openpress/manuscript/index.tsx +49 -7
- package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/src/openpress/reader/index.ts +10 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/src/openpress/reader/readerTypes.ts +4 -0
- package/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/src/openpress/reader/usePanelState.ts +56 -0
- package/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/src/openpress/shared/Panel.tsx +77 -0
- package/src/openpress/shared/index.ts +4 -0
- package/src/openpress/shared/numberUtils.ts +3 -0
- package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/src/openpress/workbench/Workbench.tsx +407 -0
- package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/src/openpress/workbench/actions/index.ts +5 -0
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/src/openpress/workbench/dialog/index.ts +1 -0
- package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/src/openpress/workbench/document/index.ts +10 -0
- package/src/openpress/workbench/index.ts +2 -0
- package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/src/openpress/workbench/inspector/index.ts +5 -0
- package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
- package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/src/openpress/workbench/mentions/index.ts +2 -0
- package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
- package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/src/openpress/workbench/panels/index.ts +3 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
- package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/src/openpress/workbench/project/index.ts +2 -0
- package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/src/openpress/workbench/shell/index.ts +1 -0
- package/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/src/styles/openpress/print-route.css +0 -2
- package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/src/styles/openpress/public-viewer.css +25 -320
- package/src/styles/openpress/reader-runtime.css +243 -55
- package/src/styles/openpress/responsive.css +145 -270
- package/src/styles/openpress/workbench-panels.css +214 -178
- package/src/styles/openpress/workbench.css +986 -451
- package/src/styles/openpress.css +1 -1
- package/vite.config.ts +50 -0
- package/src/openpress/inspector.ts +0 -282
- package/src/openpress/projectWorkspace.tsx +0 -919
- package/src/openpress/readerRuntime.ts +0 -230
- package/src/openpress/workbench.tsx +0 -1265
- package/src/openpress/workbenchTypes.ts +0 -4
- /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
package/engine/commands/dev.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export async function run({ root, options }) {
|
|
|
16
16
|
if (!options.noBuild) {
|
|
17
17
|
console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} export .`);
|
|
18
18
|
}
|
|
19
|
-
console.log(`Command: npx vite --config vite.config.ts --host ${host} --port ${port}`);
|
|
19
|
+
console.log(`Command: npx vite --force --config vite.config.ts --host ${host} --port ${port}`);
|
|
20
20
|
return 0;
|
|
21
21
|
}
|
|
22
22
|
if (!options.noBuild) {
|
|
@@ -27,7 +27,7 @@ export async function run({ root, options }) {
|
|
|
27
27
|
await printDoctorNoticeIfStale(root);
|
|
28
28
|
|
|
29
29
|
console.log(`OpenPress dev: ${url}`);
|
|
30
|
-
return runCommand("npx", ["vite", "--config", "vite.config.ts", "--host", host, "--port", port], root);
|
|
30
|
+
return runCommand("npx", ["vite", "--force", "--config", "vite.config.ts", "--host", host, "--port", port], root);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
async function printDoctorNoticeIfStale(root) {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { diagnose } from "./doctor.mjs";
|
|
5
5
|
import { runCommand } from "./_shared.mjs";
|
|
6
6
|
|
|
7
|
+
// Migration notes live in the framework repo, not in scaffolded workspaces.
|
|
8
|
+
// `npx open-press upgrade` fetches the notes for each pending version and
|
|
9
|
+
// caches them under `.openpress/migrations/` so agents can read locally.
|
|
10
|
+
const MIGRATION_REMOTE_BASE = "https://raw.githubusercontent.com/quan0715/open-press/main/docs/migrations";
|
|
11
|
+
const MIGRATION_CACHE_DIR = path.join(".openpress", "migrations");
|
|
12
|
+
|
|
7
13
|
export async function run({ root, options }) {
|
|
8
14
|
const dryRun = Boolean(options?.dryRun);
|
|
9
15
|
const skipSkills = Boolean(options?.noSkills);
|
|
@@ -85,7 +91,11 @@ export async function run({ root, options }) {
|
|
|
85
91
|
process.stdout.write(" (no migration docs in this version range)\n\n");
|
|
86
92
|
} else {
|
|
87
93
|
for (const m of migrationContents) {
|
|
88
|
-
|
|
94
|
+
if (m.path) {
|
|
95
|
+
process.stdout.write(` ─ ${m.path}${m.fetched ? " (fetched from github)" : ""}\n`);
|
|
96
|
+
} else {
|
|
97
|
+
process.stdout.write(` ─ ${m.version}.md (not found locally or on github — check the repo manually)\n`);
|
|
98
|
+
}
|
|
89
99
|
}
|
|
90
100
|
process.stdout.write(
|
|
91
101
|
"\nAgent: open each file, identify document-level changes, grep document/ for affected patterns, propose edits before applying.\n",
|
|
@@ -107,11 +117,43 @@ async function hasCoreDep(root) {
|
|
|
107
117
|
|
|
108
118
|
async function loadMigrations(root, versions) {
|
|
109
119
|
const results = [];
|
|
120
|
+
const cacheDir = path.join(root, MIGRATION_CACHE_DIR);
|
|
121
|
+
await mkdir(cacheDir, { recursive: true });
|
|
122
|
+
|
|
110
123
|
for (const v of versions) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
// Framework repo has docs/migrations/ at root — prefer local if present
|
|
125
|
+
// (covers the open-press framework repo itself acting as a workspace).
|
|
126
|
+
const localDocsPath = path.join(root, "docs", "migrations", `${v}.md`);
|
|
127
|
+
if (existsSync(localDocsPath)) {
|
|
128
|
+
results.push({ version: v, path: path.relative(root, localDocsPath), fetched: false });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Otherwise fetch from GitHub raw and cache to .openpress/migrations/.
|
|
133
|
+
const cachedPath = path.join(cacheDir, `${v}.md`);
|
|
134
|
+
if (existsSync(cachedPath)) {
|
|
135
|
+
results.push({ version: v, path: path.relative(root, cachedPath), fetched: false });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const body = await fetchMigration(v);
|
|
140
|
+
if (body) {
|
|
141
|
+
await writeFile(cachedPath, body, "utf8");
|
|
142
|
+
results.push({ version: v, path: path.relative(root, cachedPath), fetched: true });
|
|
143
|
+
} else {
|
|
144
|
+
results.push({ version: v, path: null, fetched: false });
|
|
114
145
|
}
|
|
115
146
|
}
|
|
116
147
|
return results;
|
|
117
148
|
}
|
|
149
|
+
|
|
150
|
+
async function fetchMigration(version) {
|
|
151
|
+
const url = `${MIGRATION_REMOTE_BASE}/${version}.md`;
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(url, { headers: { Accept: "text/plain" } });
|
|
154
|
+
if (!res.ok) return null;
|
|
155
|
+
return await res.text();
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -91,6 +91,13 @@ const DEFAULT_PRINT_OPTIONS = {
|
|
|
91
91
|
marginLeft: 0,
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
export const DEFAULT_PRINT_VIEWPORT = Object.freeze({
|
|
95
|
+
width: 1200,
|
|
96
|
+
height: 1698,
|
|
97
|
+
deviceScaleFactor: 1,
|
|
98
|
+
mobile: false,
|
|
99
|
+
});
|
|
100
|
+
|
|
94
101
|
export async function printUrlToPdf({
|
|
95
102
|
root,
|
|
96
103
|
url,
|
|
@@ -98,6 +105,7 @@ export async function printUrlToPdf({
|
|
|
98
105
|
chrome,
|
|
99
106
|
waitForReady = waitForPrintReady,
|
|
100
107
|
printOptions = {},
|
|
108
|
+
viewport = DEFAULT_PRINT_VIEWPORT,
|
|
101
109
|
debuggingPortBase = 9600,
|
|
102
110
|
debuggingPortRange = 300,
|
|
103
111
|
profilePrefix = "chrome-pdf",
|
|
@@ -126,9 +134,7 @@ export async function printUrlToPdf({
|
|
|
126
134
|
const tab = await waitForChromeTab(debuggingPort);
|
|
127
135
|
const client = await connectChromeDevTools(tab.webSocketDebuggerUrl);
|
|
128
136
|
try {
|
|
129
|
-
await client
|
|
130
|
-
await client.send("Runtime.enable");
|
|
131
|
-
await client.send("Emulation.setEmulatedMedia", { media: "print" });
|
|
137
|
+
await preparePdfPage(client, { viewport });
|
|
132
138
|
await client.send("Page.navigate", { url });
|
|
133
139
|
const readyResult = await waitForReady(client);
|
|
134
140
|
const result = await client.send("Page.printToPDF", {
|
|
@@ -146,6 +152,15 @@ export async function printUrlToPdf({
|
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
export async function preparePdfPage(client, { viewport = DEFAULT_PRINT_VIEWPORT } = {}) {
|
|
156
|
+
await client.send("Page.enable");
|
|
157
|
+
await client.send("Runtime.enable");
|
|
158
|
+
if (viewport) {
|
|
159
|
+
await client.send("Emulation.setDeviceMetricsOverride", viewport);
|
|
160
|
+
}
|
|
161
|
+
await client.send("Emulation.setEmulatedMedia", { media: "print" });
|
|
162
|
+
}
|
|
163
|
+
|
|
149
164
|
export async function evaluateUrlWithChrome({
|
|
150
165
|
root,
|
|
151
166
|
url,
|
|
@@ -3,7 +3,9 @@ import http from "node:http";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
|
|
6
|
+
import { searchSourceText } from "../runtime/source-text-tools.mjs";
|
|
6
7
|
import { handleProjectAssetRequest } from "../react/project-asset-endpoint.mjs";
|
|
8
|
+
import { handleSourceEditRequest } from "../react/source-edit-endpoint.mjs";
|
|
7
9
|
|
|
8
10
|
const [rootArg = "dist", ...rest] = process.argv.slice(2);
|
|
9
11
|
const host = valueAfter(rest, "--host") ?? "127.0.0.1";
|
|
@@ -32,6 +34,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
32
34
|
await handleStatusRequest(req, res);
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
37
|
+
if (url.pathname === "/__openpress/search") {
|
|
38
|
+
await handleSearchRequest(req, res, url);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (url.pathname === "/__openpress/source-edit") {
|
|
42
|
+
await handleSourceEditRequest(req, res, { root: workspace });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
35
45
|
if (url.pathname === "/__openpress/local-pdf-export") {
|
|
36
46
|
await handleLocalPdfExportRequest(req, res);
|
|
37
47
|
return;
|
|
@@ -103,6 +113,35 @@ async function handleStatusRequest(req, res) {
|
|
|
103
113
|
});
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
async function handleSearchRequest(req, res, url) {
|
|
117
|
+
if (req.method !== "GET") {
|
|
118
|
+
writeJson(res, 405, { ok: false, message: "Search endpoint requires GET." });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const query = (url.searchParams.get("q") ?? "").trim();
|
|
123
|
+
if (!query) {
|
|
124
|
+
writeJson(res, 400, { ok: false, message: "Search query is required." });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const report = await searchSourceText({
|
|
130
|
+
config,
|
|
131
|
+
query,
|
|
132
|
+
scope: searchScopeFrom(url),
|
|
133
|
+
caseSensitive: url.searchParams.get("caseSensitive") === "true",
|
|
134
|
+
});
|
|
135
|
+
writeJson(res, 200, { ok: true, ...report });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
writeJson(res, 500, { ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function searchScopeFrom(url) {
|
|
142
|
+
return url.searchParams.get("scope") === "all" ? "all" : "content";
|
|
143
|
+
}
|
|
144
|
+
|
|
106
145
|
function valueAfter(args, flag) {
|
|
107
146
|
const index = args.indexOf(flag);
|
|
108
147
|
return index >= 0 ? args[index + 1] : undefined;
|
|
@@ -4,8 +4,7 @@ import {
|
|
|
4
4
|
listCommentMarkers,
|
|
5
5
|
updateCommentMarker,
|
|
6
6
|
} from "./comment-marker.mjs";
|
|
7
|
-
|
|
8
|
-
const MAX_COMMENT_BODY_BYTES = 64 * 1024;
|
|
7
|
+
import { readJsonBody, writeJson } from "./http-json.mjs";
|
|
9
8
|
|
|
10
9
|
export async function handleCommentRequest(req, res, {
|
|
11
10
|
root = ".",
|
|
@@ -16,17 +15,14 @@ export async function handleCommentRequest(req, res, {
|
|
|
16
15
|
try {
|
|
17
16
|
writeJson(res, 200, { ok: true, comments: await listCommentMarkers({ root }) });
|
|
18
17
|
} catch (error) {
|
|
19
|
-
|
|
20
|
-
ok: false,
|
|
21
|
-
message: error instanceof Error ? error.message : String(error),
|
|
22
|
-
});
|
|
18
|
+
writeErrorJson(res, error);
|
|
23
19
|
}
|
|
24
20
|
return;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
if (req.method === "DELETE") {
|
|
28
24
|
try {
|
|
29
|
-
const body = await readJsonBody(req);
|
|
25
|
+
const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
|
|
30
26
|
const result = await clearCommentMarkers({
|
|
31
27
|
root,
|
|
32
28
|
id: body?.id,
|
|
@@ -34,17 +30,14 @@ export async function handleCommentRequest(req, res, {
|
|
|
34
30
|
});
|
|
35
31
|
writeJson(res, 200, { ok: true, ...result });
|
|
36
32
|
} catch (error) {
|
|
37
|
-
|
|
38
|
-
ok: false,
|
|
39
|
-
message: error instanceof Error ? error.message : String(error),
|
|
40
|
-
});
|
|
33
|
+
writeErrorJson(res, error);
|
|
41
34
|
}
|
|
42
35
|
return;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
if (req.method === "PATCH") {
|
|
46
39
|
try {
|
|
47
|
-
const body = await readJsonBody(req);
|
|
40
|
+
const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
|
|
48
41
|
const result = await updateCommentMarker({
|
|
49
42
|
root,
|
|
50
43
|
id: body?.id,
|
|
@@ -64,10 +57,7 @@ export async function handleCommentRequest(req, res, {
|
|
|
64
57
|
},
|
|
65
58
|
});
|
|
66
59
|
} catch (error) {
|
|
67
|
-
|
|
68
|
-
ok: false,
|
|
69
|
-
message: error instanceof Error ? error.message : String(error),
|
|
70
|
-
});
|
|
60
|
+
writeErrorJson(res, error);
|
|
71
61
|
}
|
|
72
62
|
return;
|
|
73
63
|
}
|
|
@@ -78,7 +68,7 @@ export async function handleCommentRequest(req, res, {
|
|
|
78
68
|
}
|
|
79
69
|
|
|
80
70
|
try {
|
|
81
|
-
const body = await readJsonBody(req);
|
|
71
|
+
const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
|
|
82
72
|
const target = body?.target ?? {};
|
|
83
73
|
const result = await insertCommentMarker({
|
|
84
74
|
root,
|
|
@@ -100,29 +90,13 @@ export async function handleCommentRequest(req, res, {
|
|
|
100
90
|
},
|
|
101
91
|
});
|
|
102
92
|
} catch (error) {
|
|
103
|
-
|
|
104
|
-
ok: false,
|
|
105
|
-
message: error instanceof Error ? error.message : String(error),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function readJsonBody(req) {
|
|
111
|
-
let body = "";
|
|
112
|
-
for await (const chunk of req) {
|
|
113
|
-
body += String(chunk);
|
|
114
|
-
if (Buffer.byteLength(body, "utf8") > MAX_COMMENT_BODY_BYTES) {
|
|
115
|
-
throw new Error("OpenPress comment request body is too large.");
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
return JSON.parse(body || "{}");
|
|
120
|
-
} catch {
|
|
121
|
-
throw new Error("OpenPress comment request body must be valid JSON.");
|
|
93
|
+
writeErrorJson(res, error);
|
|
122
94
|
}
|
|
123
95
|
}
|
|
124
96
|
|
|
125
|
-
function
|
|
126
|
-
res
|
|
127
|
-
|
|
97
|
+
function writeErrorJson(res, error) {
|
|
98
|
+
writeJson(res, 400, {
|
|
99
|
+
ok: false,
|
|
100
|
+
message: error instanceof Error ? error.message : String(error),
|
|
101
|
+
});
|
|
128
102
|
}
|
|
@@ -14,8 +14,8 @@ const EDITABLE_COMMENT_SOURCE_PATTERNS = [
|
|
|
14
14
|
/^document\/.+\.mdx$/,
|
|
15
15
|
/^document\/.+\.tsx$/,
|
|
16
16
|
];
|
|
17
|
-
const COMMENT_MARKER_RE =
|
|
18
|
-
const COMMENT_LINE_RE = /^\s
|
|
17
|
+
const COMMENT_MARKER_RE = /(?:\{\/\*|\/\*)\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}?/g;
|
|
18
|
+
const COMMENT_LINE_RE = /^\s*(?:\{\/\*|\/\*)\s*@openpress-comment\b[^*]*\*\/\}?\s*$/;
|
|
19
19
|
|
|
20
20
|
export async function insertCommentMarker({
|
|
21
21
|
root = ".",
|
|
@@ -35,9 +35,15 @@ export async function insertCommentMarker({
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const noteText = normalizedNote(note);
|
|
38
|
-
const marker = createCommentMarker({ id, timestamp, note: noteText, hint });
|
|
39
38
|
const line = normalizeLineNumber(source?.line);
|
|
40
39
|
const text = await fs.readFile(absolutePath, "utf8");
|
|
40
|
+
const marker = createCommentMarker({
|
|
41
|
+
id,
|
|
42
|
+
timestamp,
|
|
43
|
+
note: noteText,
|
|
44
|
+
hint,
|
|
45
|
+
syntax: commentMarkerSyntaxForInsert(text, line, relativePath),
|
|
46
|
+
});
|
|
41
47
|
const nextText = insertLineBefore(text, line, marker);
|
|
42
48
|
await fs.writeFile(absolutePath, nextText, "utf8");
|
|
43
49
|
|
|
@@ -51,9 +57,10 @@ export async function insertCommentMarker({
|
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
export function createCommentMarker({ id = createCommentId(), timestamp = new Date().toISOString(), note, hint } = {}) {
|
|
60
|
+
export function createCommentMarker({ id = createCommentId(), timestamp = new Date().toISOString(), note, hint, syntax = "jsx" } = {}) {
|
|
55
61
|
const payload = { note: normalizedNote(note), ...(typeof hint === "string" && hint.trim() ? { hint: hint.trim() } : {}) };
|
|
56
|
-
|
|
62
|
+
const body = `@openpress-comment id="${escapeMarkerAttribute(id)}" ts="${escapeMarkerAttribute(timestamp)}" text="${encodeCommentPayload(payload)}"`;
|
|
63
|
+
return syntax === "block" ? `/* ${body} */` : `{/* ${body} */}`;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
export function decodeCommentMarkerText(marker) {
|
|
@@ -285,7 +292,7 @@ function replaceCommentMarkerLine(text, { id, note, hint, timestamp }) {
|
|
|
285
292
|
if (!COMMENT_LINE_RE.test(line)) continue;
|
|
286
293
|
const attrs = parseMarkerAttributes(line);
|
|
287
294
|
if (attrs.id !== id) continue;
|
|
288
|
-
const marker = createCommentMarker({ id, timestamp, note, hint });
|
|
295
|
+
const marker = createCommentMarker({ id, timestamp, note, hint, syntax: commentMarkerSyntaxForLine(line) });
|
|
289
296
|
lines[index] = `${line.match(/^\s*/)?.[0] ?? ""}${marker}`;
|
|
290
297
|
return {
|
|
291
298
|
text: `${lines.join(newline)}${hasTrailingNewline ? newline : ""}`,
|
|
@@ -311,6 +318,23 @@ function parseMarkerAttributes(value) {
|
|
|
311
318
|
return attrs;
|
|
312
319
|
}
|
|
313
320
|
|
|
321
|
+
function commentMarkerSyntaxForInsert(text, line, relativePath) {
|
|
322
|
+
if (!String(relativePath).endsWith(".tsx")) return "jsx";
|
|
323
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
324
|
+
const index = Math.min(Math.max(line - 1, 0), lines.length);
|
|
325
|
+
const priorContent = lines.slice(0, index).some((entry) => entry.trim().length > 0);
|
|
326
|
+
if (!priorContent) return "block";
|
|
327
|
+
const targetLine = lines[index] ?? "";
|
|
328
|
+
if (/^\s*(import\b|export\b|function\b|const\b|let\b|var\b|type\b|interface\b|return\b)/.test(targetLine)) {
|
|
329
|
+
return "block";
|
|
330
|
+
}
|
|
331
|
+
return "jsx";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function commentMarkerSyntaxForLine(line) {
|
|
335
|
+
return /^\s*\/\*/.test(line) ? "block" : "jsx";
|
|
336
|
+
}
|
|
337
|
+
|
|
314
338
|
function lineStartOffsets(text) {
|
|
315
339
|
const starts = [0];
|
|
316
340
|
for (let index = 0; index < text.length; index += 1) {
|
|
@@ -68,6 +68,7 @@ export async function createReactSsrServer(workspaceRoot = ".") {
|
|
|
68
68
|
return createViteServer({
|
|
69
69
|
configFile: false,
|
|
70
70
|
root: FRAMEWORK_ROOT,
|
|
71
|
+
cacheDir: path.join(resolvedWorkspaceRoot, ".openpress", "vite-ssr"),
|
|
71
72
|
appType: "custom",
|
|
72
73
|
logLevel: "silent",
|
|
73
74
|
plugins: [reactRuntimePlugin(), react()],
|
|
@@ -82,6 +83,16 @@ export async function createReactSsrServer(workspaceRoot = ".") {
|
|
|
82
83
|
{ find: "@/components", replacement: path.join(resolvedWorkspaceRoot, "document", "components") },
|
|
83
84
|
],
|
|
84
85
|
},
|
|
86
|
+
optimizeDeps: {
|
|
87
|
+
include: [
|
|
88
|
+
"@mdx-js/react",
|
|
89
|
+
"react",
|
|
90
|
+
"react-dom",
|
|
91
|
+
"react-dom/server",
|
|
92
|
+
"react/jsx-dev-runtime",
|
|
93
|
+
"react/jsx-runtime",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
85
96
|
server: {
|
|
86
97
|
middlewareMode: true,
|
|
87
98
|
fs: {
|
|
@@ -12,6 +12,7 @@ import { createCaptionNumberingState, numberCaptionsInHtml } from "./caption-num
|
|
|
12
12
|
import { buildSectionScopedCss } from "./section-css.mjs";
|
|
13
13
|
import { CORE_ENTRY, createReactSsrServer, loadReactDocumentEntry } from "./document-entry.mjs";
|
|
14
14
|
import { buildReactMeasurementCss } from "./measurement-css.mjs";
|
|
15
|
+
import { buildObjectEntities } from "./object-entities.mjs";
|
|
15
16
|
import { allocateChains } from "./pipeline/allocate.mjs";
|
|
16
17
|
import { measureFrames } from "./pipeline/frame-measurement.mjs";
|
|
17
18
|
import { renderFinalPress } from "./pipeline/final-render.mjs";
|
|
@@ -50,7 +51,14 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
50
51
|
|
|
51
52
|
// Discover workspace for component scope and chapter-scoped style files.
|
|
52
53
|
const workspace = await discoverSectionStyles(workspaceRoot, entry.config);
|
|
53
|
-
const
|
|
54
|
+
const coreAuthorComponents = {};
|
|
55
|
+
for (const name of ["MediaFigure", "ImageFigure"]) {
|
|
56
|
+
if (typeof coreModule[name] === "function") coreAuthorComponents[name] = coreModule[name];
|
|
57
|
+
}
|
|
58
|
+
const globalComponents = {
|
|
59
|
+
...coreAuthorComponents,
|
|
60
|
+
...(await loadComponentModules(server, workspace.globalComponents ?? [])),
|
|
61
|
+
};
|
|
54
62
|
|
|
55
63
|
// Resolve sources.
|
|
56
64
|
const documentRoot = entry.config.paths.documentRoot;
|
|
@@ -92,6 +100,21 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
92
100
|
blockHeights: measurement.blockHeights,
|
|
93
101
|
sources,
|
|
94
102
|
});
|
|
103
|
+
if (process.env.OPENPRESS_DEBUG_ALLOC) {
|
|
104
|
+
const sample = measurement.mdxAreas
|
|
105
|
+
.slice(0, 5)
|
|
106
|
+
.map((a) => `${a.frameKey}#${a.indexInFrame} cap=${a.capacity.toFixed(0)} raw=${(a.rawHeight ?? 0).toFixed(0)}`);
|
|
107
|
+
const blocks = measurement.blockHeights
|
|
108
|
+
.slice(0, 8)
|
|
109
|
+
.map((b) => `${b.id} h=${b.height.toFixed(0)}`);
|
|
110
|
+
process.stderr.write(`[allocator iter ${iteration}]\n`);
|
|
111
|
+
process.stderr.write(` mdxAreas[0..4]: ${sample.join(" | ")}\n`);
|
|
112
|
+
process.stderr.write(` blocks[0..7]: ${blocks.join(" | ")}\n`);
|
|
113
|
+
process.stderr.write(` hints: ${JSON.stringify(alloc.hints.totalPagesPerChain)}\n`);
|
|
114
|
+
if (alloc.warnings.length > 0) {
|
|
115
|
+
process.stderr.write(` warnings: ${JSON.stringify(alloc.warnings)}\n`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
95
118
|
if (hintsEqual(hints, alloc.hints)) {
|
|
96
119
|
allocation = alloc.allocation;
|
|
97
120
|
warnings = alloc.warnings;
|
|
@@ -138,9 +161,6 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
138
161
|
const blockMap = {};
|
|
139
162
|
const captionState = createCaptionNumberingState();
|
|
140
163
|
const blocks = final.frames.map((frame, index) => {
|
|
141
|
-
for (const id of frame.blockIds) {
|
|
142
|
-
blockMap[id] = { id, pageIndex: index, pageNumber: index + 1 };
|
|
143
|
-
}
|
|
144
164
|
const source = {
|
|
145
165
|
file: "index.tsx",
|
|
146
166
|
path: "document/index.tsx",
|
|
@@ -149,6 +169,9 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
149
169
|
sectionIndex: index + 1,
|
|
150
170
|
};
|
|
151
171
|
const html = numberCaptionsInHtml(frame.html, entry.config.captionNumbering, captionState);
|
|
172
|
+
for (const id of collectFrameBlockIds(frame.blockIds, html)) {
|
|
173
|
+
blockMap[id] = { id, pageIndex: index, pageNumber: index + 1, frameKey: frame.frameKey };
|
|
174
|
+
}
|
|
152
175
|
const block = pageToBlock(index, html, source, entry.config, {
|
|
153
176
|
idPrefix: "openpress-page",
|
|
154
177
|
anchorPrefix: "page",
|
|
@@ -181,12 +204,18 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
181
204
|
}
|
|
182
205
|
}
|
|
183
206
|
|
|
207
|
+
const objectEntities = buildObjectEntities({
|
|
208
|
+
frames: final.frames.map((frame, index) => ({ ...frame, pageIndex: index })),
|
|
209
|
+
blocks,
|
|
210
|
+
blockMap,
|
|
211
|
+
});
|
|
212
|
+
|
|
184
213
|
const readerDocument = {
|
|
185
214
|
meta: {
|
|
186
215
|
title: trimmedString(entry.config.title) ?? "Untitled Document",
|
|
187
216
|
subtitle: trimmedString(entry.config.subtitle) ?? "",
|
|
188
217
|
organization: trimmedString(entry.config.organization) ?? "",
|
|
189
|
-
workspaceLabel: trimmedString(entry.config.workspaceLabel) ??
|
|
218
|
+
workspaceLabel: trimmedString(entry.config.workspaceLabel) ?? "",
|
|
190
219
|
version: "openpress-press-tree-v1",
|
|
191
220
|
},
|
|
192
221
|
source: {
|
|
@@ -196,6 +225,7 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
196
225
|
editMode: "source-mdx",
|
|
197
226
|
styles,
|
|
198
227
|
blockMap,
|
|
228
|
+
objectEntities,
|
|
199
229
|
frames: final.frames.map((frame, index) => ({
|
|
200
230
|
frameKey: frame.frameKey,
|
|
201
231
|
role: frame.role ?? null,
|
|
@@ -281,6 +311,16 @@ function buildSourceBlockIndex(sources) {
|
|
|
281
311
|
return index;
|
|
282
312
|
}
|
|
283
313
|
|
|
314
|
+
function collectFrameBlockIds(allocatedIds, html) {
|
|
315
|
+
const ids = new Set(allocatedIds ?? []);
|
|
316
|
+
const pattern = /\sdata-openpress-block-id="([^"]+)"/g;
|
|
317
|
+
let match;
|
|
318
|
+
while ((match = pattern.exec(String(html ?? "")))) {
|
|
319
|
+
if (match[1]) ids.add(match[1]);
|
|
320
|
+
}
|
|
321
|
+
return ids;
|
|
322
|
+
}
|
|
323
|
+
|
|
284
324
|
function buildTocContext({ sources, frames, allocation }) {
|
|
285
325
|
const toc = {};
|
|
286
326
|
for (const source of Object.values(sources)) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const DEFAULT_MAX_BODY_BYTES = 64 * 1024;
|
|
2
|
+
|
|
3
|
+
export async function readJsonBody(req, {
|
|
4
|
+
maxBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5
|
+
bodyLabel = "Request",
|
|
6
|
+
} = {}) {
|
|
7
|
+
let body = "";
|
|
8
|
+
for await (const chunk of req) {
|
|
9
|
+
body += String(chunk);
|
|
10
|
+
if (Buffer.byteLength(body, "utf8") > maxBytes) {
|
|
11
|
+
throw new Error(`${bodyLabel} body is too large.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(body || "{}");
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(`${bodyLabel} body must be valid JSON.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeJson(res, status, body) {
|
|
22
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
23
|
+
res.end(`${JSON.stringify(body, null, 2)}\n`);
|
|
24
|
+
}
|