@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.
- package/README.md +6 -1
- package/package.json +1 -1
- package/template/core/CHANGELOG.md +45 -0
- package/template/core/engine/commands/dev.mjs +2 -2
- package/template/core/engine/output/chrome-pdf.mjs +18 -3
- package/template/core/engine/output/static-server.mjs +39 -0
- package/template/core/engine/react/comment-endpoint.mjs +13 -39
- package/template/core/engine/react/comment-marker.mjs +30 -6
- package/template/core/engine/react/document-entry.mjs +11 -0
- package/template/core/engine/react/document-export.mjs +30 -5
- package/template/core/engine/react/http-json.mjs +24 -0
- package/template/core/engine/react/mdx-compile.mjs +96 -3
- package/template/core/engine/react/measurement-css.mjs +93 -1
- package/template/core/engine/react/object-entities.mjs +119 -0
- package/template/core/engine/react/pipeline/allocate.mjs +10 -7
- package/template/core/engine/react/pipeline/frame-measurement.mjs +2 -0
- package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
- package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
- package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
- package/template/core/engine/react/sources/mdx-resolver.mjs +12 -14
- package/template/core/engine/react/style-discovery.mjs +1 -4
- package/template/core/engine/runtime/file-walk.mjs +22 -0
- package/template/core/engine/runtime/inspection.mjs +1 -20
- package/template/core/engine/runtime/path-utils.mjs +20 -0
- package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
- package/template/core/engine/runtime/source-text-tools.mjs +551 -16
- package/template/core/engine/runtime/source-workspace.mjs +4 -31
- package/template/core/package.json +1 -1
- package/template/core/src/main.tsx +2 -2
- package/template/core/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
- package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
- package/template/core/src/openpress/app/index.ts +2 -0
- package/template/core/src/openpress/core/Frame.tsx +9 -11
- package/template/core/src/openpress/core/FrameContext.tsx +8 -3
- package/template/core/src/openpress/core/MdxArea.tsx +11 -12
- package/template/core/src/openpress/core/cn.ts +4 -0
- package/template/core/src/openpress/core/index.tsx +2 -1
- package/template/core/src/openpress/core/primitives.tsx +29 -8
- package/template/core/src/openpress/core/types.ts +8 -0
- package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
- package/template/core/src/openpress/document-model/index.ts +6 -0
- package/template/core/src/openpress/document-model/objectEntityModel.ts +51 -0
- package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/template/core/src/openpress/manuscript/index.tsx +49 -7
- package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/template/core/src/openpress/reader/index.ts +10 -0
- package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/template/core/src/openpress/reader/readerTypes.ts +4 -0
- package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/template/core/src/openpress/reader/usePanelState.ts +56 -0
- package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/template/core/src/openpress/shared/Panel.tsx +77 -0
- package/template/core/src/openpress/shared/index.ts +4 -0
- package/template/core/src/openpress/shared/numberUtils.ts +3 -0
- package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/template/core/src/openpress/workbench/Workbench.tsx +407 -0
- package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/template/core/src/openpress/workbench/actions/index.ts +5 -0
- package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
- package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/template/core/src/openpress/workbench/document/index.ts +10 -0
- package/template/core/src/openpress/workbench/index.ts +2 -0
- package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
- package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
- package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
- package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
- package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/template/core/src/openpress/workbench/panels/index.ts +3 -0
- package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
- package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/template/core/src/openpress/workbench/project/index.ts +2 -0
- package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/template/core/src/openpress/workbench/shell/index.ts +1 -0
- package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/template/core/src/styles/openpress/print-route.css +0 -2
- package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/template/core/src/styles/openpress/public-viewer.css +25 -320
- package/template/core/src/styles/openpress/reader-runtime.css +243 -55
- package/template/core/src/styles/openpress/responsive.css +145 -270
- package/template/core/src/styles/openpress/workbench-panels.css +214 -178
- package/template/core/src/styles/openpress/workbench.css +986 -451
- package/template/core/src/styles/openpress.css +1 -1
- package/template/core/vite.config.ts +50 -0
- package/template/core/src/openpress/inspector.ts +0 -282
- package/template/core/src/openpress/projectWorkspace.tsx +0 -919
- package/template/core/src/openpress/readerRuntime.ts +0 -230
- package/template/core/src/openpress/workbench.tsx +0 -1265
- package/template/core/src/openpress/workbenchTypes.ts +0 -4
- /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /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 `
|
|
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,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
|
|
130
|
-
await client.send("Runtime.enable");
|
|
131
|
-
await client.send("Emulation.setEmulatedMedia", { media: "print" });
|
|
137
|
+
await preparePdfPage(client, { viewport });
|
|
132
138
|
await client.send("Page.navigate", { url });
|
|
133
139
|
const readyResult = await waitForReady(client);
|
|
134
140
|
const result = await client.send("Page.printToPDF", {
|
|
@@ -146,6 +152,15 @@ export async function printUrlToPdf({
|
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
export async function preparePdfPage(client, { viewport = DEFAULT_PRINT_VIEWPORT } = {}) {
|
|
156
|
+
await client.send("Page.enable");
|
|
157
|
+
await client.send("Runtime.enable");
|
|
158
|
+
if (viewport) {
|
|
159
|
+
await client.send("Emulation.setDeviceMetricsOverride", viewport);
|
|
160
|
+
}
|
|
161
|
+
await client.send("Emulation.setEmulatedMedia", { media: "print" });
|
|
162
|
+
}
|
|
163
|
+
|
|
149
164
|
export async function evaluateUrlWithChrome({
|
|
150
165
|
root,
|
|
151
166
|
url,
|
|
@@ -3,7 +3,9 @@ import http from "node:http";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
|
|
6
|
+
import { searchSourceText } from "../runtime/source-text-tools.mjs";
|
|
6
7
|
import { handleProjectAssetRequest } from "../react/project-asset-endpoint.mjs";
|
|
8
|
+
import { handleSourceEditRequest } from "../react/source-edit-endpoint.mjs";
|
|
7
9
|
|
|
8
10
|
const [rootArg = "dist", ...rest] = process.argv.slice(2);
|
|
9
11
|
const host = valueAfter(rest, "--host") ?? "127.0.0.1";
|
|
@@ -32,6 +34,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
32
34
|
await handleStatusRequest(req, res);
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
37
|
+
if (url.pathname === "/__openpress/search") {
|
|
38
|
+
await handleSearchRequest(req, res, url);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (url.pathname === "/__openpress/source-edit") {
|
|
42
|
+
await handleSourceEditRequest(req, res, { root: workspace });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
35
45
|
if (url.pathname === "/__openpress/local-pdf-export") {
|
|
36
46
|
await handleLocalPdfExportRequest(req, res);
|
|
37
47
|
return;
|
|
@@ -103,6 +113,35 @@ async function handleStatusRequest(req, res) {
|
|
|
103
113
|
});
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
async function handleSearchRequest(req, res, url) {
|
|
117
|
+
if (req.method !== "GET") {
|
|
118
|
+
writeJson(res, 405, { ok: false, message: "Search endpoint requires GET." });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const query = (url.searchParams.get("q") ?? "").trim();
|
|
123
|
+
if (!query) {
|
|
124
|
+
writeJson(res, 400, { ok: false, message: "Search query is required." });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const report = await searchSourceText({
|
|
130
|
+
config,
|
|
131
|
+
query,
|
|
132
|
+
scope: searchScopeFrom(url),
|
|
133
|
+
caseSensitive: url.searchParams.get("caseSensitive") === "true",
|
|
134
|
+
});
|
|
135
|
+
writeJson(res, 200, { ok: true, ...report });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
writeJson(res, 500, { ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function searchScopeFrom(url) {
|
|
142
|
+
return url.searchParams.get("scope") === "all" ? "all" : "content";
|
|
143
|
+
}
|
|
144
|
+
|
|
106
145
|
function valueAfter(args, flag) {
|
|
107
146
|
const index = args.indexOf(flag);
|
|
108
147
|
return index >= 0 ? args[index + 1] : undefined;
|
|
@@ -4,8 +4,7 @@ import {
|
|
|
4
4
|
listCommentMarkers,
|
|
5
5
|
updateCommentMarker,
|
|
6
6
|
} from "./comment-marker.mjs";
|
|
7
|
-
|
|
8
|
-
const MAX_COMMENT_BODY_BYTES = 64 * 1024;
|
|
7
|
+
import { readJsonBody, writeJson } from "./http-json.mjs";
|
|
9
8
|
|
|
10
9
|
export async function handleCommentRequest(req, res, {
|
|
11
10
|
root = ".",
|
|
@@ -16,17 +15,14 @@ export async function handleCommentRequest(req, res, {
|
|
|
16
15
|
try {
|
|
17
16
|
writeJson(res, 200, { ok: true, comments: await listCommentMarkers({ root }) });
|
|
18
17
|
} catch (error) {
|
|
19
|
-
|
|
20
|
-
ok: false,
|
|
21
|
-
message: error instanceof Error ? error.message : String(error),
|
|
22
|
-
});
|
|
18
|
+
writeErrorJson(res, error);
|
|
23
19
|
}
|
|
24
20
|
return;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
if (req.method === "DELETE") {
|
|
28
24
|
try {
|
|
29
|
-
const body = await readJsonBody(req);
|
|
25
|
+
const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
|
|
30
26
|
const result = await clearCommentMarkers({
|
|
31
27
|
root,
|
|
32
28
|
id: body?.id,
|
|
@@ -34,17 +30,14 @@ export async function handleCommentRequest(req, res, {
|
|
|
34
30
|
});
|
|
35
31
|
writeJson(res, 200, { ok: true, ...result });
|
|
36
32
|
} catch (error) {
|
|
37
|
-
|
|
38
|
-
ok: false,
|
|
39
|
-
message: error instanceof Error ? error.message : String(error),
|
|
40
|
-
});
|
|
33
|
+
writeErrorJson(res, error);
|
|
41
34
|
}
|
|
42
35
|
return;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
if (req.method === "PATCH") {
|
|
46
39
|
try {
|
|
47
|
-
const body = await readJsonBody(req);
|
|
40
|
+
const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
|
|
48
41
|
const result = await updateCommentMarker({
|
|
49
42
|
root,
|
|
50
43
|
id: body?.id,
|
|
@@ -64,10 +57,7 @@ export async function handleCommentRequest(req, res, {
|
|
|
64
57
|
},
|
|
65
58
|
});
|
|
66
59
|
} catch (error) {
|
|
67
|
-
|
|
68
|
-
ok: false,
|
|
69
|
-
message: error instanceof Error ? error.message : String(error),
|
|
70
|
-
});
|
|
60
|
+
writeErrorJson(res, error);
|
|
71
61
|
}
|
|
72
62
|
return;
|
|
73
63
|
}
|
|
@@ -78,7 +68,7 @@ export async function handleCommentRequest(req, res, {
|
|
|
78
68
|
}
|
|
79
69
|
|
|
80
70
|
try {
|
|
81
|
-
const body = await readJsonBody(req);
|
|
71
|
+
const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
|
|
82
72
|
const target = body?.target ?? {};
|
|
83
73
|
const result = await insertCommentMarker({
|
|
84
74
|
root,
|
|
@@ -100,29 +90,13 @@ export async function handleCommentRequest(req, res, {
|
|
|
100
90
|
},
|
|
101
91
|
});
|
|
102
92
|
} catch (error) {
|
|
103
|
-
|
|
104
|
-
ok: false,
|
|
105
|
-
message: error instanceof Error ? error.message : String(error),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function readJsonBody(req) {
|
|
111
|
-
let body = "";
|
|
112
|
-
for await (const chunk of req) {
|
|
113
|
-
body += String(chunk);
|
|
114
|
-
if (Buffer.byteLength(body, "utf8") > MAX_COMMENT_BODY_BYTES) {
|
|
115
|
-
throw new Error("OpenPress comment request body is too large.");
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
return JSON.parse(body || "{}");
|
|
120
|
-
} catch {
|
|
121
|
-
throw new Error("OpenPress comment request body must be valid JSON.");
|
|
93
|
+
writeErrorJson(res, error);
|
|
122
94
|
}
|
|
123
95
|
}
|
|
124
96
|
|
|
125
|
-
function
|
|
126
|
-
res
|
|
127
|
-
|
|
97
|
+
function writeErrorJson(res, error) {
|
|
98
|
+
writeJson(res, 400, {
|
|
99
|
+
ok: false,
|
|
100
|
+
message: error instanceof Error ? error.message : String(error),
|
|
101
|
+
});
|
|
128
102
|
}
|
|
@@ -14,8 +14,8 @@ const EDITABLE_COMMENT_SOURCE_PATTERNS = [
|
|
|
14
14
|
/^document\/.+\.mdx$/,
|
|
15
15
|
/^document\/.+\.tsx$/,
|
|
16
16
|
];
|
|
17
|
-
const COMMENT_MARKER_RE =
|
|
18
|
-
const COMMENT_LINE_RE = /^\s
|
|
17
|
+
const COMMENT_MARKER_RE = /(?:\{\/\*|\/\*)\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}?/g;
|
|
18
|
+
const COMMENT_LINE_RE = /^\s*(?:\{\/\*|\/\*)\s*@openpress-comment\b[^*]*\*\/\}?\s*$/;
|
|
19
19
|
|
|
20
20
|
export async function insertCommentMarker({
|
|
21
21
|
root = ".",
|
|
@@ -35,9 +35,15 @@ export async function insertCommentMarker({
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const noteText = normalizedNote(note);
|
|
38
|
-
const marker = createCommentMarker({ id, timestamp, note: noteText, hint });
|
|
39
38
|
const line = normalizeLineNumber(source?.line);
|
|
40
39
|
const text = await fs.readFile(absolutePath, "utf8");
|
|
40
|
+
const marker = createCommentMarker({
|
|
41
|
+
id,
|
|
42
|
+
timestamp,
|
|
43
|
+
note: noteText,
|
|
44
|
+
hint,
|
|
45
|
+
syntax: commentMarkerSyntaxForInsert(text, line, relativePath),
|
|
46
|
+
});
|
|
41
47
|
const nextText = insertLineBefore(text, line, marker);
|
|
42
48
|
await fs.writeFile(absolutePath, nextText, "utf8");
|
|
43
49
|
|
|
@@ -51,9 +57,10 @@ export async function insertCommentMarker({
|
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
export function createCommentMarker({ id = createCommentId(), timestamp = new Date().toISOString(), note, hint } = {}) {
|
|
60
|
+
export function createCommentMarker({ id = createCommentId(), timestamp = new Date().toISOString(), note, hint, syntax = "jsx" } = {}) {
|
|
55
61
|
const payload = { note: normalizedNote(note), ...(typeof hint === "string" && hint.trim() ? { hint: hint.trim() } : {}) };
|
|
56
|
-
|
|
62
|
+
const body = `@openpress-comment id="${escapeMarkerAttribute(id)}" ts="${escapeMarkerAttribute(timestamp)}" text="${encodeCommentPayload(payload)}"`;
|
|
63
|
+
return syntax === "block" ? `/* ${body} */` : `{/* ${body} */}`;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
export function decodeCommentMarkerText(marker) {
|
|
@@ -285,7 +292,7 @@ function replaceCommentMarkerLine(text, { id, note, hint, timestamp }) {
|
|
|
285
292
|
if (!COMMENT_LINE_RE.test(line)) continue;
|
|
286
293
|
const attrs = parseMarkerAttributes(line);
|
|
287
294
|
if (attrs.id !== id) continue;
|
|
288
|
-
const marker = createCommentMarker({ id, timestamp, note, hint });
|
|
295
|
+
const marker = createCommentMarker({ id, timestamp, note, hint, syntax: commentMarkerSyntaxForLine(line) });
|
|
289
296
|
lines[index] = `${line.match(/^\s*/)?.[0] ?? ""}${marker}`;
|
|
290
297
|
return {
|
|
291
298
|
text: `${lines.join(newline)}${hasTrailingNewline ? newline : ""}`,
|
|
@@ -311,6 +318,23 @@ function parseMarkerAttributes(value) {
|
|
|
311
318
|
return attrs;
|
|
312
319
|
}
|
|
313
320
|
|
|
321
|
+
function commentMarkerSyntaxForInsert(text, line, relativePath) {
|
|
322
|
+
if (!String(relativePath).endsWith(".tsx")) return "jsx";
|
|
323
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
324
|
+
const index = Math.min(Math.max(line - 1, 0), lines.length);
|
|
325
|
+
const priorContent = lines.slice(0, index).some((entry) => entry.trim().length > 0);
|
|
326
|
+
if (!priorContent) return "block";
|
|
327
|
+
const targetLine = lines[index] ?? "";
|
|
328
|
+
if (/^\s*(import\b|export\b|function\b|const\b|let\b|var\b|type\b|interface\b|return\b)/.test(targetLine)) {
|
|
329
|
+
return "block";
|
|
330
|
+
}
|
|
331
|
+
return "jsx";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function commentMarkerSyntaxForLine(line) {
|
|
335
|
+
return /^\s*\/\*/.test(line) ? "block" : "jsx";
|
|
336
|
+
}
|
|
337
|
+
|
|
314
338
|
function lineStartOffsets(text) {
|
|
315
339
|
const starts = [0];
|
|
316
340
|
for (let index = 0; index < text.length; index += 1) {
|
|
@@ -68,6 +68,7 @@ export async function createReactSsrServer(workspaceRoot = ".") {
|
|
|
68
68
|
return createViteServer({
|
|
69
69
|
configFile: false,
|
|
70
70
|
root: FRAMEWORK_ROOT,
|
|
71
|
+
cacheDir: path.join(resolvedWorkspaceRoot, ".openpress", "vite-ssr"),
|
|
71
72
|
appType: "custom",
|
|
72
73
|
logLevel: "silent",
|
|
73
74
|
plugins: [reactRuntimePlugin(), react()],
|
|
@@ -82,6 +83,16 @@ export async function createReactSsrServer(workspaceRoot = ".") {
|
|
|
82
83
|
{ find: "@/components", replacement: path.join(resolvedWorkspaceRoot, "document", "components") },
|
|
83
84
|
],
|
|
84
85
|
},
|
|
86
|
+
optimizeDeps: {
|
|
87
|
+
include: [
|
|
88
|
+
"@mdx-js/react",
|
|
89
|
+
"react",
|
|
90
|
+
"react-dom",
|
|
91
|
+
"react-dom/server",
|
|
92
|
+
"react/jsx-dev-runtime",
|
|
93
|
+
"react/jsx-runtime",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
85
96
|
server: {
|
|
86
97
|
middlewareMode: true,
|
|
87
98
|
fs: {
|
|
@@ -12,6 +12,7 @@ import { createCaptionNumberingState, numberCaptionsInHtml } from "./caption-num
|
|
|
12
12
|
import { buildSectionScopedCss } from "./section-css.mjs";
|
|
13
13
|
import { CORE_ENTRY, createReactSsrServer, loadReactDocumentEntry } from "./document-entry.mjs";
|
|
14
14
|
import { buildReactMeasurementCss } from "./measurement-css.mjs";
|
|
15
|
+
import { buildObjectEntities } from "./object-entities.mjs";
|
|
15
16
|
import { allocateChains } from "./pipeline/allocate.mjs";
|
|
16
17
|
import { measureFrames } from "./pipeline/frame-measurement.mjs";
|
|
17
18
|
import { renderFinalPress } from "./pipeline/final-render.mjs";
|
|
@@ -50,7 +51,14 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
50
51
|
|
|
51
52
|
// Discover workspace for component scope and chapter-scoped style files.
|
|
52
53
|
const workspace = await discoverSectionStyles(workspaceRoot, entry.config);
|
|
53
|
-
const
|
|
54
|
+
const coreAuthorComponents = {};
|
|
55
|
+
for (const name of ["MediaFigure", "ImageFigure"]) {
|
|
56
|
+
if (typeof coreModule[name] === "function") coreAuthorComponents[name] = coreModule[name];
|
|
57
|
+
}
|
|
58
|
+
const globalComponents = {
|
|
59
|
+
...coreAuthorComponents,
|
|
60
|
+
...(await loadComponentModules(server, workspace.globalComponents ?? [])),
|
|
61
|
+
};
|
|
54
62
|
|
|
55
63
|
// Resolve sources.
|
|
56
64
|
const documentRoot = entry.config.paths.documentRoot;
|
|
@@ -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) ??
|
|
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
|
+
}
|