@open-press/cli 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 (119) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/template/core/CHANGELOG.md +45 -0
  4. package/template/core/engine/commands/dev.mjs +2 -2
  5. package/template/core/engine/output/chrome-pdf.mjs +18 -3
  6. package/template/core/engine/output/static-server.mjs +39 -0
  7. package/template/core/engine/react/comment-endpoint.mjs +13 -39
  8. package/template/core/engine/react/comment-marker.mjs +30 -6
  9. package/template/core/engine/react/document-entry.mjs +11 -0
  10. package/template/core/engine/react/document-export.mjs +30 -5
  11. package/template/core/engine/react/http-json.mjs +24 -0
  12. package/template/core/engine/react/mdx-compile.mjs +96 -3
  13. package/template/core/engine/react/measurement-css.mjs +93 -1
  14. package/template/core/engine/react/object-entities.mjs +119 -0
  15. package/template/core/engine/react/pipeline/allocate.mjs +10 -7
  16. package/template/core/engine/react/pipeline/frame-measurement.mjs +2 -0
  17. package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
  18. package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
  19. package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
  20. package/template/core/engine/react/sources/mdx-resolver.mjs +12 -14
  21. package/template/core/engine/react/style-discovery.mjs +1 -4
  22. package/template/core/engine/runtime/file-walk.mjs +22 -0
  23. package/template/core/engine/runtime/inspection.mjs +1 -20
  24. package/template/core/engine/runtime/path-utils.mjs +20 -0
  25. package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
  26. package/template/core/engine/runtime/source-text-tools.mjs +551 -16
  27. package/template/core/engine/runtime/source-workspace.mjs +4 -31
  28. package/template/core/package.json +1 -1
  29. package/template/core/src/main.tsx +2 -2
  30. package/template/core/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
  31. package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
  32. package/template/core/src/openpress/app/index.ts +2 -0
  33. package/template/core/src/openpress/core/Frame.tsx +9 -11
  34. package/template/core/src/openpress/core/FrameContext.tsx +8 -3
  35. package/template/core/src/openpress/core/MdxArea.tsx +11 -12
  36. package/template/core/src/openpress/core/cn.ts +4 -0
  37. package/template/core/src/openpress/core/index.tsx +2 -1
  38. package/template/core/src/openpress/core/primitives.tsx +29 -8
  39. package/template/core/src/openpress/core/types.ts +8 -0
  40. package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  41. package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  42. package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
  43. package/template/core/src/openpress/document-model/index.ts +6 -0
  44. package/template/core/src/openpress/document-model/objectEntityModel.ts +51 -0
  45. package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  46. package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  47. package/template/core/src/openpress/manuscript/index.tsx +49 -7
  48. package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  49. package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  50. package/template/core/src/openpress/reader/index.ts +10 -0
  51. package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  52. package/template/core/src/openpress/reader/readerTypes.ts +4 -0
  53. package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
  54. package/template/core/src/openpress/reader/usePanelState.ts +56 -0
  55. package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
  56. package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  57. package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
  58. package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  59. package/template/core/src/openpress/shared/Panel.tsx +77 -0
  60. package/template/core/src/openpress/shared/index.ts +4 -0
  61. package/template/core/src/openpress/shared/numberUtils.ts +3 -0
  62. package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  63. package/template/core/src/openpress/workbench/Workbench.tsx +407 -0
  64. package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  65. package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  66. package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  67. package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  68. package/template/core/src/openpress/workbench/actions/index.ts +5 -0
  69. package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  70. package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  71. package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
  72. package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  73. package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  74. package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  75. package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  76. package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  77. package/template/core/src/openpress/workbench/document/index.ts +10 -0
  78. package/template/core/src/openpress/workbench/index.ts +2 -0
  79. package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  80. package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
  81. package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  82. package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  83. package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  84. package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
  85. package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  86. package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
  87. package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  88. package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
  89. package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
  90. package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  91. package/template/core/src/openpress/workbench/panels/index.ts +3 -0
  92. package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
  93. package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  94. package/template/core/src/openpress/workbench/project/index.ts +2 -0
  95. package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  96. package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  97. package/template/core/src/openpress/workbench/shell/index.ts +1 -0
  98. package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
  99. package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
  100. package/template/core/src/styles/openpress/print-route.css +0 -2
  101. package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  102. package/template/core/src/styles/openpress/public-viewer.css +25 -320
  103. package/template/core/src/styles/openpress/reader-runtime.css +243 -55
  104. package/template/core/src/styles/openpress/responsive.css +145 -270
  105. package/template/core/src/styles/openpress/workbench-panels.css +214 -178
  106. package/template/core/src/styles/openpress/workbench.css +986 -451
  107. package/template/core/src/styles/openpress.css +1 -1
  108. package/template/core/vite.config.ts +50 -0
  109. package/template/core/src/openpress/inspector.ts +0 -282
  110. package/template/core/src/openpress/projectWorkspace.tsx +0 -919
  111. package/template/core/src/openpress/readerRuntime.ts +0 -230
  112. package/template/core/src/openpress/workbench.tsx +0 -1265
  113. package/template/core/src/openpress/workbenchTypes.ts +0 -4
  114. /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  115. /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  116. /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  117. /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  118. /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  119. /package/template/core/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Scaffolder for [open-press](https://github.com/quan0715/open-press) — an AI-first fixed-layout document workspace.
4
4
 
5
+ ## Prerequisite
6
+
7
+ Node.js 20 or newer with `npm` / `npx`.
8
+
5
9
  ## Quick start
6
10
 
7
11
  ```bash
@@ -20,7 +24,7 @@ npx @open-press/cli init <target> [flags]
20
24
 
21
25
  | Flag | Description |
22
26
  | -------------------- | --------------------------------------------------------------------------- |
23
- | `--pack <name>` | Style pack starter: `editorial-monograph` or `claude-document` |
27
+ | `--pack <name>` | Style pack starter: `editorial-monograph`, `claude-document`, or `academic-paper` |
24
28
  | `--title <s>` | Document title (written to `openpress.config.mjs`) |
25
29
  | `--subtitle <s>` | Document subtitle |
26
30
  | `--organization <s>` | Organization name |
@@ -46,6 +50,7 @@ Workspace commands (run via `npm run` or `node engine/cli.mjs`):
46
50
 
47
51
  ```
48
52
  npm run dev # start workbench
53
+ npm run openpress:export # refresh public/openpress/document.json
49
54
  npm run build # render production output (dist-react/)
50
55
  npm run preview # preview production build
51
56
  npm run openpress:validate # structural checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Scaffolder for open-press — AI-first fixed-layout document workspaces.",
6
6
  "license": "MIT",
@@ -1,5 +1,50 @@
1
1
  # @open-press/core
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Workbench architecture**: the monolithic `workbench.tsx` is split into a modular structure under `workbench/{shell,panels,actions,inspector,mentions,project,document}/`. New panels: `DeploymentControl`, `SearchControl`, `PendingCommentsPanel`, `DocumentPanel`, `ProjectEntryPanel`, plus `WorkbenchShell` and `InlineInspectorLayer`.
8
+ - **Module reorganization**: source tree split into typed subdirectories:
9
+ - `app/`: `OpenPressApp`, `OpenPressRuntime` (replaces the old `App.tsx` + `renderer.tsx`)
10
+ - `document-model/`: `anchorMap`, `documentIndexes`, `documentTypes`, `objectEntityModel`, `projectIdentity`, `reactDocumentMetadata`
11
+ - `reader/`: `PublicReaderPage`, `ReaderNavigationPanel`, `useReaderRuntime`, registry/route/scroll/state helpers
12
+ - `shared/`: `frameScheduler`, `runtimeMode`, `Panel`, `numberUtils`
13
+ - **Object-entity model**: `Frame` and `MdxArea` now expose `data-openpress-object-id`. New `document-model/objectEntityModel` defines the id format.
14
+ - **`MediaFigure` / `ImageFigure`**: new core primitives that accept `src/alt/caption` and resolve relative paths to `/openpress/media/...` automatically.
15
+ - **`<Sections>` default page**: `page` prop becomes optional; when omitted the built-in `DefaultSectionPage` renders the standard manuscript frame.
16
+ - **Engine helpers**: new `engine/react/{http-json,object-entities,source-edit-endpoint}.mjs` and `engine/runtime/{file-walk,path-utils}.mjs` runtime helpers. `engine/runtime/source-text-tools` exports TypeScript definitions.
17
+ - **Dev endpoints**: vite plugin wires `/__openpress/search` and `/__openpress/source-edit` middlewares for the new workbench search + inline editing flows.
18
+ - **Inline source editor**: ships the `InlineSourceEditorLayer` UI on top of `useInlineDocumentEditor` + `/__openpress/source-edit`. The hook now uses a `MutationObserver` so newly inserted blocks become editable, and routes mouse clicks through `focus()` to preserve selection on `contenteditable` boundaries. Workbench wires `sourceEditorTarget` state into the layer.
19
+ - **Table editing in the source pipeline**: table captions are emitted as standalone source blocks (`kind=element`, `name=caption`, `layout="attached"`) with `data-openpress-block-id`/`data-openpress-object-id` markers and preserved source positions; the allocator treats `layout="attached"` blocks as non-paginable. `applySourceBlockTableCellEditToText` (in `engine/runtime/source-text-tools`) accepts a `cellIndex` so the inline source editor can target a single `<td>`.
20
+ - **Reader pagination**: arrow-key pagination now defers to the user's active text selection. Shift-arrow / mouse-drag selections no longer get swallowed by the page-turn shortcut.
21
+ - **Page zoom + spread layout**: new `reader/pageViewportScaleModel` + `usePageViewportScale` hook drive a `--openpress-page-viewport-scale` CSS variable on the page container; the workbench toolbar exposes a `PageZoomControl` dropdown with fit-width / fit-page / fixed percentages plus a one-page / two-page spread toggle.
22
+ - **Inspector cell-precision comments**: `CommentDraft` gains an optional `targetObjectId` so a comment can point at a sub-block (e.g. a single table cell) while still attributing the source position to its enclosing block. `formatInspectorHint` carries the value through to the wire hint.
23
+ - **Shared `WorkbenchDialog` shell**: portal + backdrop + header (eyebrow / title / title-meta / close) + optional footer. `DeploymentControl`, `SearchControl`, and `ProjectPreviewDialog` all render through this shell now, replacing the prior per-dialog scaffolding under `openpress-deploy-dialog-*` / `openpress-search-dialog-*` / `openpress-project-preview-dialog__*`.
24
+ - **`WorkbenchControlPanel` registry**: `HtmlWorkbench` now accepts an `extraControlPanels?: WorkbenchPanel[]` prop and renders the right-side panel from a `{ id, render }` registry. Built-in panels (pending comments, project entry) ship as the first entries.
25
+ - **Workbench state hooks**: extract `useDeploymentWorkbench` and `useInspectorComments` from `HtmlWorkbench`; `useReaderRuntime` is split into focused sub-hooks (`usePanelState`, `useReaderScrollAnchor`, `useReaderHashSync`, `useReaderKeyboardNav`).
26
+ - **`InlineInspectorLayer` memoization**: now wrapped in `React.memo` with a stable `geometryVersion` prop so the geometry / event listeners no longer rebuild on every parent render.
27
+ - **Panels open lazily**: `usePanelState` now defaults both panels closed, so the reader opens with a clean stage; resize never auto-opens them.
28
+
29
+ ### Patch Changes
30
+
31
+ - Inspector: fix comment-marker count and multi-target marker rendering.
32
+ - Inspector: object-entity id helpers consolidate in `document-model/objectEntityModel` instead of being duplicated inside `Frame`, `MdxArea`, manuscript `TocArea`, and `PublicReaderPage`.
33
+ - Inline editor: `useInlineDocumentEditor` exposes `onDocumentEdited`; `OpenPressApp` re-loads `/openpress/document.json` after a successful inline edit so derived indexes stay in sync.
34
+ - Dev: reset Vite optimizer cache so workspace-side dependencies are picked up.
35
+ - Workbench dialog: viewport-aware width + max-height so a big media preview doesn't blow up the dialog to full screen.
36
+ - Project composer: add `/apply-comments` to the skill mention list so pending comment resolution can be invoked from the workbench.
37
+ - Carries forward the 0.7.1 measurement + pagination fixes (font/image readiness, relative media src inlining, list-per-item paging, `OPENPRESS_DEBUG_ALLOC`, academic-paper starter body overflow).
38
+
39
+ ### Breaking Changes
40
+
41
+ - `FrameContext.consumeArea(chainId)` return type changes from `ReactNode | null` to `{ indexInFrame: number; blocks: ReactNode | null }`. Custom `Frame` consumers must read `.blocks`.
42
+ - `App` export is renamed to `OpenPressApp` and now lives under `@open-press/core/app`. The old `renderer.tsx` is replaced by `OpenPressRuntime`.
43
+ - `data-openpress-mdx-area-empty` is now always emitted (`"true"` / `"false"`). Selectors that relied on the attribute being absent need updating.
44
+ - Reader `ViewMode` collapses to `"paged"` only — the legacy `"reading"` flow mode is removed. Use `usePageViewportScale` for free-scaling instead.
45
+ - Several internal module paths moved into subdirectories (`document-model/`, `reader/`, `shared/`, `workbench/...`). Consumers that deep-imported from the openpress source must switch to the new barrels.
46
+ - Shared dialog scaffolding (backdrop, container, header, close button) moved from per-dialog class families (`openpress-deploy-dialog-backdrop`, `__panel`, `__panel header`, `__close`, etc.) to the shared `openpress-workbench-dialog*` family. Per-dialog modifier classes (`openpress-deploy-dialog`, `openpress-search-dialog`, `openpress-project-preview-dialog`) are still applied for dialog-specific styling. Selectors that targeted the old scaffolding names (notably `*-backdrop` and `__panel`) need updating; selectors that combine the modifier class with new `__heading` / `__footer` / `__close` modifiers continue to work.
47
+
3
48
  ## 0.7.1
4
49
 
5
50
  ### Patch Changes
@@ -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
+ }