@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.
Files changed (116) hide show
  1. package/engine/commands/dev.mjs +2 -2
  2. package/engine/commands/upgrade.mjs +47 -5
  3. package/engine/output/chrome-pdf.mjs +18 -3
  4. package/engine/output/static-server.mjs +39 -0
  5. package/engine/react/comment-endpoint.mjs +13 -39
  6. package/engine/react/comment-marker.mjs +30 -6
  7. package/engine/react/document-entry.mjs +11 -0
  8. package/engine/react/document-export.mjs +45 -5
  9. package/engine/react/http-json.mjs +24 -0
  10. package/engine/react/mdx-compile.mjs +187 -3
  11. package/engine/react/measurement-css.mjs +93 -1
  12. package/engine/react/object-entities.mjs +119 -0
  13. package/engine/react/pipeline/allocate.mjs +10 -7
  14. package/engine/react/pipeline/frame-measurement.mjs +40 -9
  15. package/engine/react/project-asset-endpoint.mjs +6 -24
  16. package/engine/react/source-edit-endpoint.d.mts +10 -0
  17. package/engine/react/source-edit-endpoint.mjs +75 -0
  18. package/engine/react/sources/mdx-resolver.mjs +12 -14
  19. package/engine/react/style-discovery.mjs +1 -4
  20. package/engine/runtime/file-walk.mjs +22 -0
  21. package/engine/runtime/inspection.mjs +1 -20
  22. package/engine/runtime/path-utils.mjs +20 -0
  23. package/engine/runtime/source-text-tools.d.mts +102 -0
  24. package/engine/runtime/source-text-tools.mjs +551 -16
  25. package/engine/runtime/source-workspace.mjs +4 -31
  26. package/package.json +1 -1
  27. package/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
  28. package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
  29. package/src/openpress/app/index.ts +2 -0
  30. package/src/openpress/core/Frame.tsx +9 -11
  31. package/src/openpress/core/FrameContext.tsx +8 -3
  32. package/src/openpress/core/MdxArea.tsx +11 -12
  33. package/src/openpress/core/cn.ts +4 -0
  34. package/src/openpress/core/index.tsx +2 -1
  35. package/src/openpress/core/primitives.tsx +29 -8
  36. package/src/openpress/core/types.ts +8 -0
  37. package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  38. package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  39. package/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
  40. package/src/openpress/document-model/index.ts +6 -0
  41. package/src/openpress/document-model/objectEntityModel.ts +51 -0
  42. package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  43. package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  44. package/src/openpress/manuscript/index.tsx +49 -7
  45. package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  46. package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  47. package/src/openpress/reader/index.ts +10 -0
  48. package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  49. package/src/openpress/reader/readerTypes.ts +4 -0
  50. package/src/openpress/reader/usePageViewportScale.ts +119 -0
  51. package/src/openpress/reader/usePanelState.ts +56 -0
  52. package/src/openpress/reader/useReaderHashSync.ts +61 -0
  53. package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  54. package/src/openpress/reader/useReaderRuntime.ts +146 -0
  55. package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  56. package/src/openpress/shared/Panel.tsx +77 -0
  57. package/src/openpress/shared/index.ts +4 -0
  58. package/src/openpress/shared/numberUtils.ts +3 -0
  59. package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  60. package/src/openpress/workbench/Workbench.tsx +407 -0
  61. package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  62. package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  63. package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  64. package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  65. package/src/openpress/workbench/actions/index.ts +5 -0
  66. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  67. package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  68. package/src/openpress/workbench/dialog/index.ts +1 -0
  69. package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  70. package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  71. package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  72. package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  73. package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  74. package/src/openpress/workbench/document/index.ts +10 -0
  75. package/src/openpress/workbench/index.ts +2 -0
  76. package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  77. package/src/openpress/workbench/inspector/index.ts +5 -0
  78. package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  79. package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  80. package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  81. package/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
  82. package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  83. package/src/openpress/workbench/mentions/index.ts +2 -0
  84. package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  85. package/src/openpress/workbench/panels/Panel.tsx +1 -0
  86. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
  87. package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  88. package/src/openpress/workbench/panels/index.ts +3 -0
  89. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
  90. package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  91. package/src/openpress/workbench/project/index.ts +2 -0
  92. package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  93. package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  94. package/src/openpress/workbench/shell/index.ts +1 -0
  95. package/src/openpress/workbench/workbenchFormatters.ts +120 -0
  96. package/src/openpress/workbench/workbenchTypes.ts +35 -0
  97. package/src/styles/openpress/print-route.css +0 -2
  98. package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  99. package/src/styles/openpress/public-viewer.css +25 -320
  100. package/src/styles/openpress/reader-runtime.css +243 -55
  101. package/src/styles/openpress/responsive.css +145 -270
  102. package/src/styles/openpress/workbench-panels.css +214 -178
  103. package/src/styles/openpress/workbench.css +986 -451
  104. package/src/styles/openpress.css +1 -1
  105. package/vite.config.ts +50 -0
  106. package/src/openpress/inspector.ts +0 -282
  107. package/src/openpress/projectWorkspace.tsx +0 -919
  108. package/src/openpress/readerRuntime.ts +0 -230
  109. package/src/openpress/workbench.tsx +0 -1265
  110. package/src/openpress/workbenchTypes.ts +0 -4
  111. /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  112. /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  113. /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  114. /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  115. /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  116. /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
@@ -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
- process.stdout.write(` ─ ${m.path}\n`);
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
- const p = path.join(root, "docs", "migrations", `${v}.md`);
112
- if (existsSync(p)) {
113
- results.push({ version: v, path: path.relative(root, p) });
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.send("Page.enable");
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
- writeJson(res, 400, {
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
- writeJson(res, 400, {
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
- writeJson(res, 400, {
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
- writeJson(res, 400, {
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 writeJson(res, status, body) {
126
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
127
- res.end(`${JSON.stringify(body, null, 2)}\n`);
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 = /\{\/\*\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}/g;
18
- const COMMENT_LINE_RE = /^\s*\{\/\*\s*@openpress-comment\b[^*]*\*\/\}\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
- return `{/* @openpress-comment id="${escapeMarkerAttribute(id)}" ts="${escapeMarkerAttribute(timestamp)}" text="${encodeCommentPayload(payload)}" */}`;
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 globalComponents = await loadComponentModules(server, workspace.globalComponents ?? []);
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) ?? trimmedString(entry.config.title) ?? "Untitled Document",
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
+ }