@open-press/core 0.7.1 → 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 (115) hide show
  1. package/engine/commands/dev.mjs +2 -2
  2. package/engine/output/chrome-pdf.mjs +18 -3
  3. package/engine/output/static-server.mjs +39 -0
  4. package/engine/react/comment-endpoint.mjs +13 -39
  5. package/engine/react/comment-marker.mjs +30 -6
  6. package/engine/react/document-entry.mjs +11 -0
  7. package/engine/react/document-export.mjs +30 -5
  8. package/engine/react/http-json.mjs +24 -0
  9. package/engine/react/mdx-compile.mjs +96 -3
  10. package/engine/react/measurement-css.mjs +93 -1
  11. package/engine/react/object-entities.mjs +119 -0
  12. package/engine/react/pipeline/allocate.mjs +10 -7
  13. package/engine/react/pipeline/frame-measurement.mjs +2 -0
  14. package/engine/react/project-asset-endpoint.mjs +6 -24
  15. package/engine/react/source-edit-endpoint.d.mts +10 -0
  16. package/engine/react/source-edit-endpoint.mjs +75 -0
  17. package/engine/react/sources/mdx-resolver.mjs +12 -14
  18. package/engine/react/style-discovery.mjs +1 -4
  19. package/engine/runtime/file-walk.mjs +22 -0
  20. package/engine/runtime/inspection.mjs +1 -20
  21. package/engine/runtime/path-utils.mjs +20 -0
  22. package/engine/runtime/source-text-tools.d.mts +102 -0
  23. package/engine/runtime/source-text-tools.mjs +551 -16
  24. package/engine/runtime/source-workspace.mjs +4 -31
  25. package/package.json +1 -1
  26. package/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
  27. package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
  28. package/src/openpress/app/index.ts +2 -0
  29. package/src/openpress/core/Frame.tsx +9 -11
  30. package/src/openpress/core/FrameContext.tsx +8 -3
  31. package/src/openpress/core/MdxArea.tsx +11 -12
  32. package/src/openpress/core/cn.ts +4 -0
  33. package/src/openpress/core/index.tsx +2 -1
  34. package/src/openpress/core/primitives.tsx +29 -8
  35. package/src/openpress/core/types.ts +8 -0
  36. package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  37. package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  38. package/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
  39. package/src/openpress/document-model/index.ts +6 -0
  40. package/src/openpress/document-model/objectEntityModel.ts +51 -0
  41. package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  42. package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  43. package/src/openpress/manuscript/index.tsx +49 -7
  44. package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  45. package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  46. package/src/openpress/reader/index.ts +10 -0
  47. package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  48. package/src/openpress/reader/readerTypes.ts +4 -0
  49. package/src/openpress/reader/usePageViewportScale.ts +119 -0
  50. package/src/openpress/reader/usePanelState.ts +56 -0
  51. package/src/openpress/reader/useReaderHashSync.ts +61 -0
  52. package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  53. package/src/openpress/reader/useReaderRuntime.ts +146 -0
  54. package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  55. package/src/openpress/shared/Panel.tsx +77 -0
  56. package/src/openpress/shared/index.ts +4 -0
  57. package/src/openpress/shared/numberUtils.ts +3 -0
  58. package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  59. package/src/openpress/workbench/Workbench.tsx +407 -0
  60. package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  61. package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  62. package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  63. package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  64. package/src/openpress/workbench/actions/index.ts +5 -0
  65. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  66. package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  67. package/src/openpress/workbench/dialog/index.ts +1 -0
  68. package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  69. package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  70. package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  71. package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  72. package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  73. package/src/openpress/workbench/document/index.ts +10 -0
  74. package/src/openpress/workbench/index.ts +2 -0
  75. package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  76. package/src/openpress/workbench/inspector/index.ts +5 -0
  77. package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  78. package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  79. package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  80. package/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
  81. package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  82. package/src/openpress/workbench/mentions/index.ts +2 -0
  83. package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  84. package/src/openpress/workbench/panels/Panel.tsx +1 -0
  85. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
  86. package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  87. package/src/openpress/workbench/panels/index.ts +3 -0
  88. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
  89. package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  90. package/src/openpress/workbench/project/index.ts +2 -0
  91. package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  92. package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  93. package/src/openpress/workbench/shell/index.ts +1 -0
  94. package/src/openpress/workbench/workbenchFormatters.ts +120 -0
  95. package/src/openpress/workbench/workbenchTypes.ts +35 -0
  96. package/src/styles/openpress/print-route.css +0 -2
  97. package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  98. package/src/styles/openpress/public-viewer.css +25 -320
  99. package/src/styles/openpress/reader-runtime.css +243 -55
  100. package/src/styles/openpress/responsive.css +145 -270
  101. package/src/styles/openpress/workbench-panels.css +214 -178
  102. package/src/styles/openpress/workbench.css +986 -451
  103. package/src/styles/openpress.css +1 -1
  104. package/vite.config.ts +50 -0
  105. package/src/openpress/inspector.ts +0 -282
  106. package/src/openpress/projectWorkspace.tsx +0 -919
  107. package/src/openpress/readerRuntime.ts +0 -230
  108. package/src/openpress/workbench.tsx +0 -1265
  109. package/src/openpress/workbenchTypes.ts +0 -4
  110. /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  111. /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  112. /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  113. /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  114. /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  115. /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) {
@@ -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;
@@ -153,9 +161,6 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
153
161
  const blockMap = {};
154
162
  const captionState = createCaptionNumberingState();
155
163
  const blocks = final.frames.map((frame, index) => {
156
- for (const id of frame.blockIds) {
157
- blockMap[id] = { id, pageIndex: index, pageNumber: index + 1 };
158
- }
159
164
  const source = {
160
165
  file: "index.tsx",
161
166
  path: "document/index.tsx",
@@ -164,6 +169,9 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
164
169
  sectionIndex: index + 1,
165
170
  };
166
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
+ }
167
175
  const block = pageToBlock(index, html, source, entry.config, {
168
176
  idPrefix: "openpress-page",
169
177
  anchorPrefix: "page",
@@ -196,12 +204,18 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
196
204
  }
197
205
  }
198
206
 
207
+ const objectEntities = buildObjectEntities({
208
+ frames: final.frames.map((frame, index) => ({ ...frame, pageIndex: index })),
209
+ blocks,
210
+ blockMap,
211
+ });
212
+
199
213
  const readerDocument = {
200
214
  meta: {
201
215
  title: trimmedString(entry.config.title) ?? "Untitled Document",
202
216
  subtitle: trimmedString(entry.config.subtitle) ?? "",
203
217
  organization: trimmedString(entry.config.organization) ?? "",
204
- workspaceLabel: trimmedString(entry.config.workspaceLabel) ?? trimmedString(entry.config.title) ?? "Untitled Document",
218
+ workspaceLabel: trimmedString(entry.config.workspaceLabel) ?? "",
205
219
  version: "openpress-press-tree-v1",
206
220
  },
207
221
  source: {
@@ -211,6 +225,7 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
211
225
  editMode: "source-mdx",
212
226
  styles,
213
227
  blockMap,
228
+ objectEntities,
214
229
  frames: final.frames.map((frame, index) => ({
215
230
  frameKey: frame.frameKey,
216
231
  role: frame.role ?? null,
@@ -296,6 +311,16 @@ function buildSourceBlockIndex(sources) {
296
311
  return index;
297
312
  }
298
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
+
299
324
  function buildTocContext({ sources, frames, allocation }) {
300
325
  const toc = {};
301
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
+ }
@@ -112,6 +112,7 @@ export function rehypeBlockIds(options = {}) {
112
112
  if (includeBlockIds && !includeBlockIds.has(id)) return false;
113
113
 
114
114
  setDataAttribute(node, "data-openpress-block-id", id);
115
+ setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
115
116
  const extraAttributes = blockAttributes.get(id);
116
117
  if (extraAttributes) {
117
118
  for (const [name, value] of Object.entries(extraAttributes)) {
@@ -142,9 +143,19 @@ function applyTableRowBlocks({
142
143
  includeBlockIds,
143
144
  }) {
144
145
  const rows = tableBodyRows(node);
146
+ const header = tableHeaderRow(node);
147
+ const caption = tableCaption(node);
148
+ const captionRecord = caption ? { id: `${id}-caption`, node: caption } : null;
149
+ const headerRecord = header ? { id: `${id}-h0`, node: header } : null;
150
+ const selectedCaption = captionRecord && (!includeBlockIds || includeBlockIds.has(captionRecord.id));
151
+ const selectedHeader = headerRecord && (!includeBlockIds || includeBlockIds.has(headerRecord.id));
152
+ const firstSelectedRowIndex = selectedFirstTableRowIndex(rows, includeBlockIds, id);
153
+ const renderCaption = selectedCaption || (captionRecord && includeBlockIds && firstSelectedRowIndex === 0);
154
+ const renderHeader = Boolean(headerRecord && (!includeBlockIds || firstSelectedRowIndex === 0 || selectedHeader));
145
155
  if (rows.length === 0) {
146
156
  if (includeBlockIds && !includeBlockIds.has(id)) return false;
147
157
  setDataAttribute(node, "data-openpress-block-id", id);
158
+ setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
148
159
  blocks.push({
149
160
  id,
150
161
  kind: "element",
@@ -165,15 +176,56 @@ function applyTableRowBlocks({
165
176
  const selected = includeBlockIds
166
177
  ? rowRecords.filter((row) => includeBlockIds.has(row.id))
167
178
  : rowRecords;
168
- if (selected.length === 0) return false;
179
+ if (selected.length === 0 && !selectedCaption && !selectedHeader) return false;
169
180
 
170
181
  setDataAttribute(node, "data-openpress-table-id", id);
182
+ if (headerRecord && renderHeader) {
183
+ setDataAttribute(headerRecord.node, "data-openpress-block-id", headerRecord.id);
184
+ setDataAttribute(headerRecord.node, "data-openpress-object-id", createBlockObjectEntityId(headerRecord.id));
185
+ setDataAttribute(headerRecord.node, "data-openpress-block-layout", "attached");
186
+ }
187
+ if (captionRecord) {
188
+ if (renderCaption) {
189
+ setDataAttribute(captionRecord.node, "data-openpress-block-id", captionRecord.id);
190
+ setDataAttribute(captionRecord.node, "data-openpress-object-id", createBlockObjectEntityId(captionRecord.id));
191
+ if (selectedCaption) {
192
+ blocks.push({
193
+ id: captionRecord.id,
194
+ kind: "element",
195
+ name: "caption",
196
+ text: textContent(captionRecord.node).trim() || undefined,
197
+ filePath,
198
+ chapterSlug,
199
+ tableId: id,
200
+ layout: "attached",
201
+ source: sourcePosition(captionRecord.node.position ?? node.position),
202
+ });
203
+ }
204
+ } else {
205
+ removeTableCaption(node);
206
+ }
207
+ }
208
+ if (headerRecord && selectedHeader) {
209
+ blocks.push({
210
+ id: headerRecord.id,
211
+ kind: "table-row",
212
+ name: "table-header-row",
213
+ text: textContent(headerRecord.node).trim() || undefined,
214
+ filePath,
215
+ chapterSlug,
216
+ tableId: id,
217
+ rowIndex: -1,
218
+ layout: "attached",
219
+ source: sourcePosition(headerRecord.node.position ?? node.position),
220
+ });
221
+ }
171
222
  const selectedNodes = new Set(selected.map((row) => row.node));
172
223
  pruneUnselectedTableRows(node, new Set(rowRecords.map((row) => row.node)), selectedNodes);
173
- if (selected[0]?.index > 0) stripTableHeader(node);
224
+ if (!renderHeader) stripTableHeader(node);
174
225
 
175
226
  for (const row of selected) {
176
227
  setDataAttribute(row.node, "data-openpress-block-id", row.id);
228
+ setDataAttribute(row.node, "data-openpress-object-id", createBlockObjectEntityId(row.id));
177
229
  blocks.push({
178
230
  id: row.id,
179
231
  kind: "table-row",
@@ -229,6 +281,7 @@ function normalizeTableCaptions(node) {
229
281
  type: "element",
230
282
  tagName: "caption",
231
283
  properties: {},
284
+ position: child.position,
232
285
  children: [{ type: "text", value: captionText }],
233
286
  });
234
287
  }
@@ -272,8 +325,10 @@ function wrapMdxComponents(components) {
272
325
  if (typeof Component !== "function") continue;
273
326
  wrapped[name] = function ComponentBlock(props = {}) {
274
327
  const blockId = props["data-openpress-block-id"];
328
+ const objectId = props["data-openpress-object-id"] || (blockId ? createBlockObjectEntityId(blockId) : undefined);
275
329
  const rest = { ...props };
276
330
  delete rest["data-openpress-block-id"];
331
+ delete rest["data-openpress-object-id"];
277
332
 
278
333
  if (!blockId) return React.createElement(Component, rest);
279
334
 
@@ -281,6 +336,7 @@ function wrapMdxComponents(components) {
281
336
  "div",
282
337
  {
283
338
  "data-openpress-block-id": blockId,
339
+ "data-openpress-object-id": objectId,
284
340
  "data-openpress-component-block": name,
285
341
  },
286
342
  React.createElement(Component, rest),
@@ -336,6 +392,7 @@ function applyListItemBlocks({
336
392
  if (items.length === 0) {
337
393
  if (includeBlockIds && !includeBlockIds.has(id)) return false;
338
394
  setDataAttribute(node, "data-openpress-block-id", id);
395
+ setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
339
396
  blocks.push({
340
397
  id,
341
398
  kind: "element",
@@ -375,6 +432,7 @@ function applyListItemBlocks({
375
432
 
376
433
  for (const item of selected) {
377
434
  setDataAttribute(item.node, "data-openpress-block-id", item.id);
435
+ setDataAttribute(item.node, "data-openpress-object-id", createBlockObjectEntityId(item.id));
378
436
  blocks.push({
379
437
  id: item.id,
380
438
  kind: "list-item",
@@ -419,6 +477,28 @@ function tableBodyRows(table) {
419
477
  return (table.children ?? []).filter((child) => child?.type === "element" && child.tagName === "tr");
420
478
  }
421
479
 
480
+ function tableHeaderRow(table) {
481
+ if (table?.type !== "element" || table.tagName !== "table") return null;
482
+ for (const child of table.children ?? []) {
483
+ if (child?.type !== "element" || child.tagName !== "thead") continue;
484
+ return (child.children ?? []).find((row) => row?.type === "element" && row.tagName === "tr") ?? null;
485
+ }
486
+ return null;
487
+ }
488
+
489
+ function selectedFirstTableRowIndex(rows, includeBlockIds, tableId) {
490
+ if (!includeBlockIds) return 0;
491
+ for (let index = 0; index < rows.length; index += 1) {
492
+ if (includeBlockIds.has(`${tableId}-r${index}`)) return index;
493
+ }
494
+ return -1;
495
+ }
496
+
497
+ function tableCaption(table) {
498
+ if (table?.type !== "element" || table.tagName !== "table") return null;
499
+ return (table.children ?? []).find((child) => child?.type === "element" && child.tagName === "caption") ?? null;
500
+ }
501
+
422
502
  function pruneUnselectedTableRows(node, rowNodes, selectedNodes) {
423
503
  if (!Array.isArray(node?.children)) return;
424
504
  node.children = node.children.filter((child) => {
@@ -432,10 +512,15 @@ function stripTableHeader(table) {
432
512
  if (!Array.isArray(table?.children)) return;
433
513
  table.children = table.children.filter((child) => {
434
514
  if (child?.type !== "element") return true;
435
- return child.tagName !== "caption" && child.tagName !== "thead";
515
+ return child.tagName !== "thead";
436
516
  });
437
517
  }
438
518
 
519
+ function removeTableCaption(table) {
520
+ if (!Array.isArray(table?.children)) return;
521
+ table.children = table.children.filter((child) => child?.type !== "element" || child.tagName !== "caption");
522
+ }
523
+
439
524
  function headingText(node) {
440
525
  if (!/^h[1-6]$/.test(String(node?.tagName ?? ""))) return undefined;
441
526
  return textContent(node).trim() || undefined;
@@ -470,6 +555,14 @@ function setDataAttribute(node, name, value) {
470
555
  node.properties[name] = value;
471
556
  }
472
557
 
558
+ function createObjectEntityId(kind, ...parts) {
559
+ return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
560
+ }
561
+
562
+ function createBlockObjectEntityId(blockId) {
563
+ return createObjectEntityId("mdx-block", blockId);
564
+ }
565
+
473
566
  function visit(node, visitor) {
474
567
  visitor(node);
475
568
  if (!Array.isArray(node?.children)) return;