@open-press/core 0.7.1 → 1.0.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 -3
- package/engine/cli.mjs +8 -8
- package/engine/commands/_shared.mjs +37 -15
- package/engine/commands/dev.mjs +2 -2
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +63 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +110 -3
- package/engine/output/static-server.mjs +87 -9
- package/engine/react/comment-endpoint.mjs +13 -39
- package/engine/react/comment-marker.mjs +43 -19
- package/engine/react/document-entry.mjs +46 -28
- package/engine/react/document-export.mjs +328 -164
- package/engine/react/http-json.mjs +24 -0
- package/engine/react/mdx-compile.mjs +126 -3
- package/engine/react/measurement-css.mjs +114 -1
- package/engine/react/object-entities.mjs +204 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +41 -72
- package/engine/react/pipeline/frame-measurement.mjs +6 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- package/engine/react/project-asset-endpoint.mjs +6 -24
- package/engine/react/source-edit-endpoint.d.mts +10 -0
- package/engine/react/source-edit-endpoint.mjs +75 -0
- package/engine/react/sources/mdx-resolver.mjs +13 -15
- package/engine/react/style-discovery.mjs +23 -8
- package/engine/runtime/config.d.mts +8 -0
- package/engine/runtime/config.mjs +57 -60
- package/engine/runtime/file-utils.mjs +9 -1
- package/engine/runtime/file-walk.mjs +22 -0
- package/engine/runtime/inspection.mjs +1 -20
- package/engine/runtime/page-geometry.mjs +131 -0
- package/engine/runtime/path-utils.mjs +20 -0
- package/engine/runtime/source-text-tools.d.mts +102 -0
- package/engine/runtime/source-text-tools.mjs +551 -16
- package/engine/runtime/source-workspace.mjs +16 -34
- package/engine/runtime/validation.mjs +19 -10
- package/package.json +3 -5
- package/src/openpress/app/OpenPressApp.tsx +296 -0
- package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/app/index.ts +2 -0
- package/src/openpress/core/Frame.tsx +26 -15
- package/src/openpress/core/FrameContext.tsx +10 -3
- package/src/openpress/core/MdxArea.tsx +11 -12
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/cn.ts +4 -0
- package/src/openpress/core/index.tsx +11 -3
- package/src/openpress/core/primitives.tsx +74 -6
- package/src/openpress/core/types.ts +94 -41
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
- package/src/openpress/document-model/index.ts +7 -0
- package/src/openpress/document-model/objectEntityModel.ts +55 -0
- package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/manuscript/index.tsx +49 -7
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/src/openpress/reader/index.ts +11 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/src/openpress/reader/readerTypes.ts +4 -0
- package/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/src/openpress/reader/usePanelState.ts +56 -0
- package/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/src/openpress/shared/Panel.tsx +77 -0
- package/src/openpress/shared/index.ts +4 -0
- package/src/openpress/shared/numberUtils.ts +3 -0
- package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/src/openpress/workbench/Workbench.tsx +506 -0
- package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/src/openpress/workbench/actions/index.ts +6 -0
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/src/openpress/workbench/dialog/index.ts +1 -0
- package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/src/openpress/workbench/document/index.ts +10 -0
- package/src/openpress/workbench/index.ts +2 -0
- package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/src/openpress/workbench/inspector/index.ts +5 -0
- package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
- package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/src/openpress/workbench/mentions/index.ts +2 -0
- package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
- package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/src/openpress/workbench/panels/index.ts +3 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
- package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/src/openpress/workbench/project/index.ts +2 -0
- package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/src/openpress/workbench/shell/index.ts +1 -0
- package/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/src/styles/openpress/print-route.css +0 -2
- package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/src/styles/openpress/public-viewer.css +25 -320
- package/src/styles/openpress/reader-runtime.css +252 -55
- package/src/styles/openpress/responsive.css +145 -270
- package/src/styles/openpress/workbench-panels.css +327 -178
- package/src/styles/openpress/workbench.css +986 -451
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +2 -1
- package/tsconfig.json +1 -1
- package/vite.config.ts +50 -0
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
- package/src/openpress/App.tsx +0 -127
- package/src/openpress/inspector.ts +0 -282
- package/src/openpress/projectWorkspace.tsx +0 -919
- package/src/openpress/readerRuntime.ts +0 -230
- package/src/openpress/workbench.tsx +0 -1265
- package/src/openpress/workbenchTypes.ts +0 -4
- /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// - `hints`: { totalPagesPerChain: { [chainId]: number } } — fed back to <Sections>
|
|
7
7
|
// - `warnings`: chain-overflowed, etc.
|
|
8
8
|
|
|
9
|
+
import { allocateBlocksToRegions, estimateRegionsNeeded } from "../pagination/allocator.mjs";
|
|
10
|
+
|
|
9
11
|
const SANITY_LIMIT = 200;
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -58,7 +60,7 @@ export function allocateChains({ frames, mdxAreas, blockHeights, sources }) {
|
|
|
58
60
|
continue;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
const { result, neededAreas } =
|
|
63
|
+
const { result, neededAreas } = allocateBlocksToFiniteRegions(blocks, regions);
|
|
62
64
|
const lastOverflow = regions[regions.length - 1].__overflow;
|
|
63
65
|
const sourceFramesForChain = uniqueFramesForChain(frames, chainId);
|
|
64
66
|
|
|
@@ -110,76 +112,41 @@ export function allocateChains({ frames, mdxAreas, blockHeights, sources }) {
|
|
|
110
112
|
};
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
function
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
function allocateBlocksToFiniteRegions(blocks, regions) {
|
|
116
|
+
const byRegionId = new Map(regions.map((region) => [region.id, region]));
|
|
117
|
+
const result = allocateBlocksToRegions(blocks, finiteRegionStream(regions), {
|
|
118
|
+
keepWithNext: shouldKeepWithNext,
|
|
119
|
+
});
|
|
120
|
+
const filled = result.regions.map((region) => ({
|
|
121
|
+
region: byRegionId.get(region.regionId),
|
|
122
|
+
blockIds: region.blockIds,
|
|
123
|
+
})).filter((fill) => fill.region);
|
|
124
|
+
const consumed = result.consumedCount;
|
|
125
|
+
const remaining = blocks.slice(consumed);
|
|
126
|
+
const extraNeeded = remaining.length > 0
|
|
127
|
+
? estimateRegionsNeeded(remaining, regions[regions.length - 1].capacity, {
|
|
128
|
+
keepWithNext: shouldKeepWithNext,
|
|
129
|
+
})
|
|
130
|
+
: 0;
|
|
131
|
+
return {
|
|
132
|
+
result: { filled, consumed },
|
|
133
|
+
neededAreas: filled.length + extraNeeded,
|
|
127
134
|
};
|
|
128
|
-
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
|
|
129
|
-
const block = blocks[blockIndex];
|
|
130
|
-
while (regionIndex < regions.length) {
|
|
131
|
-
const region = regions[regionIndex];
|
|
132
|
-
const nextBlock = blocks[blockIndex + 1];
|
|
133
|
-
const keepWithNextHeight = shouldKeepWithNext(block, nextBlock) ? block.height + nextBlock.height : 0;
|
|
134
|
-
if (
|
|
135
|
-
currentBlockIds.length > 0 &&
|
|
136
|
-
keepWithNextHeight > 0 &&
|
|
137
|
-
currentHeight + keepWithNextHeight > region.capacity
|
|
138
|
-
) {
|
|
139
|
-
flush();
|
|
140
|
-
regionIndex += 1;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (currentBlockIds.length === 0 || currentHeight + block.height <= region.capacity) {
|
|
144
|
-
currentBlockIds.push(block.id);
|
|
145
|
-
currentHeight += block.height;
|
|
146
|
-
consumed += 1;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
// Doesn't fit — flush current region and advance
|
|
150
|
-
flush();
|
|
151
|
-
regionIndex += 1;
|
|
152
|
-
}
|
|
153
|
-
if (regionIndex >= regions.length) break;
|
|
154
|
-
}
|
|
155
|
-
flush();
|
|
156
|
-
// neededAreas = total regions consumed if we had unlimited supply
|
|
157
|
-
// For overflow detection we estimate: if consumed < blocks.length, we need more areas.
|
|
158
|
-
let neededAreas = filled.length;
|
|
159
|
-
if (consumed < blocks.length) {
|
|
160
|
-
// Estimate how many more areas needed
|
|
161
|
-
const lastCap = regions[regions.length - 1].capacity;
|
|
162
|
-
const remainingBlocks = blocks.slice(consumed);
|
|
163
|
-
let h = 0;
|
|
164
|
-
let extra = 0;
|
|
165
|
-
let inExtra = false;
|
|
166
|
-
for (const b of remainingBlocks) {
|
|
167
|
-
if (!inExtra || h + b.height > lastCap) {
|
|
168
|
-
extra += 1;
|
|
169
|
-
h = b.height;
|
|
170
|
-
inExtra = true;
|
|
171
|
-
} else {
|
|
172
|
-
h += b.height;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
neededAreas += extra;
|
|
176
|
-
}
|
|
177
|
-
return { result: { filled, consumed }, neededAreas };
|
|
178
135
|
}
|
|
179
136
|
|
|
180
137
|
function shouldKeepWithNext(block, nextBlock) {
|
|
181
138
|
if (!nextBlock) return false;
|
|
182
|
-
|
|
139
|
+
const name = String(block?.name ?? "");
|
|
140
|
+
return /^h[1-6]$/.test(name) || name === "caption";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function finiteRegionStream(regions) {
|
|
144
|
+
let index = 0;
|
|
145
|
+
return {
|
|
146
|
+
next() {
|
|
147
|
+
return index < regions.length ? regions[index++] : null;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
183
150
|
}
|
|
184
151
|
|
|
185
152
|
function recordAllocation(allocation, result, regions) {
|
|
@@ -223,12 +190,14 @@ function groupBlockHeights(blockHeights) {
|
|
|
223
190
|
|
|
224
191
|
function buildBlockStream(chainSource, heightMap) {
|
|
225
192
|
if (!chainSource || !heightMap) return [];
|
|
226
|
-
return chainSource
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
193
|
+
return chainSource
|
|
194
|
+
.filter((block) => block.layout !== "attached")
|
|
195
|
+
.map((block) => ({
|
|
196
|
+
id: block.id,
|
|
197
|
+
kind: block.kind,
|
|
198
|
+
name: block.name,
|
|
199
|
+
height: heightMap.get(block.id) ?? 0,
|
|
200
|
+
}));
|
|
232
201
|
}
|
|
233
202
|
|
|
234
203
|
function* iterateChains(sources) {
|
|
@@ -221,6 +221,12 @@ async function runChromiumMeasurement(html, viewport) {
|
|
|
221
221
|
const parentTop = chain.parentElement?.getBoundingClientRect().top ?? chain.getBoundingClientRect().top;
|
|
222
222
|
let previousBottom = parentTop;
|
|
223
223
|
for (const el of Array.from(chain.querySelectorAll("[data-openpress-block-id]"))) {
|
|
224
|
+
if (el.tagName.toLowerCase() === "caption") continue;
|
|
225
|
+
if (el.getAttribute("data-openpress-block-layout") === "attached") continue;
|
|
226
|
+
// Cells inherit their row's block-id so the inspector can resolve a
|
|
227
|
+
// SourceBlock when clicking inside a <td>. Skip them here so the
|
|
228
|
+
// row's measured height isn't overwritten by each cell.
|
|
229
|
+
if (el.getAttribute("data-openpress-inherited-block-id") === "true") continue;
|
|
224
230
|
const rect = el.getBoundingClientRect();
|
|
225
231
|
out.push({
|
|
226
232
|
id: el.getAttribute("data-openpress-block-id"),
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Walks the user's default-exported component tree to extract
|
|
2
|
+
// <Workspace> and <Press> metadata declared as JSX props.
|
|
3
|
+
//
|
|
4
|
+
// The 1.0 contract says <Press> carries every per-document setting on
|
|
5
|
+
// its props (title, page, sources, slug, theme, componentsDir) and is
|
|
6
|
+
// always nested inside <Workspace>. This helper invokes the user's
|
|
7
|
+
// component once at load time to inspect those props before the engine
|
|
8
|
+
// runs its render pipeline.
|
|
9
|
+
//
|
|
10
|
+
// Safe to call because Workspace, Press, and (typically) the user's
|
|
11
|
+
// default export are inert function components that just return JSX —
|
|
12
|
+
// they don't use hooks at the entry boundary.
|
|
13
|
+
|
|
14
|
+
import React from "react";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Inspect the user's default export and extract every <Press> child
|
|
18
|
+
* of the (optional) <Workspace> wrapper. The export pipeline iterates
|
|
19
|
+
* the returned `presses` array — single-Press workspaces simply have
|
|
20
|
+
* length 1, multi-Press have length N. There is no separate code path
|
|
21
|
+
* for the single-Press case.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {Function} opts.UserComponent The default export of press/index.tsx.
|
|
25
|
+
* @param {symbol} opts.PRESS_MARKER Marker identifying Press components.
|
|
26
|
+
* @param {symbol} opts.WORKSPACE_MARKER Marker identifying Workspace components.
|
|
27
|
+
* @returns {{
|
|
28
|
+
* workspaceProps: Record<string, unknown>,
|
|
29
|
+
* presses: Array<{
|
|
30
|
+
* element: object, // ReactElement
|
|
31
|
+
* props: Record<string, unknown>, // Press JSX props (no children)
|
|
32
|
+
* metadata: {
|
|
33
|
+
* title?: string,
|
|
34
|
+
* page?: unknown,
|
|
35
|
+
* slug?: string,
|
|
36
|
+
* theme?: string,
|
|
37
|
+
* componentsDir?: string,
|
|
38
|
+
* captionNumbering?: unknown,
|
|
39
|
+
* },
|
|
40
|
+
* sources: Record<string, unknown> | null, // mdxSource() descriptors keyed by id
|
|
41
|
+
* children: unknown, // raw children for re-rendering
|
|
42
|
+
* index: number, // position in the Workspace
|
|
43
|
+
* }>,
|
|
44
|
+
* wrappedInWorkspace: boolean,
|
|
45
|
+
* }}
|
|
46
|
+
*/
|
|
47
|
+
export function inspectPressTree({ UserComponent, PRESS_MARKER, WORKSPACE_MARKER }) {
|
|
48
|
+
if (typeof UserComponent !== "function") {
|
|
49
|
+
return emptyResult();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let root;
|
|
53
|
+
try {
|
|
54
|
+
root = UserComponent({});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// The user's default export threw before returning JSX. This is rare
|
|
57
|
+
// (function components at the entry boundary don't normally use hooks
|
|
58
|
+
// that could fail), but we treat it as "no Press metadata declared"
|
|
59
|
+
// and let the render pipeline surface the real error later with
|
|
60
|
+
// full React error context.
|
|
61
|
+
return emptyResult();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isReactElement(root)) return emptyResult();
|
|
65
|
+
|
|
66
|
+
const workspaceProps = isMarked(root, WORKSPACE_MARKER) ? extractProps(root) : {};
|
|
67
|
+
const wrappedInWorkspace = isMarked(root, WORKSPACE_MARKER);
|
|
68
|
+
|
|
69
|
+
// Find every <Press> element in the tree (Workspace child, or root itself).
|
|
70
|
+
const pressElements = collectPressElements(root, PRESS_MARKER);
|
|
71
|
+
|
|
72
|
+
const presses = pressElements.map((element, index) => {
|
|
73
|
+
const props = extractProps(element);
|
|
74
|
+
return {
|
|
75
|
+
element,
|
|
76
|
+
props,
|
|
77
|
+
metadata: pickPressMetadata(props),
|
|
78
|
+
sources: extractSources(props),
|
|
79
|
+
children: element.props?.children ?? null,
|
|
80
|
+
index,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
workspaceProps,
|
|
86
|
+
presses,
|
|
87
|
+
wrappedInWorkspace,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function emptyResult() {
|
|
92
|
+
return {
|
|
93
|
+
workspaceProps: {},
|
|
94
|
+
presses: [],
|
|
95
|
+
wrappedInWorkspace: false,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isReactElement(value) {
|
|
100
|
+
return value && typeof value === "object" && "type" in value && "props" in value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isMarked(element, marker) {
|
|
104
|
+
if (!isReactElement(element)) return false;
|
|
105
|
+
const type = element.type;
|
|
106
|
+
if (!type) return false;
|
|
107
|
+
// Components are tagged via `Component.openpressMarker = MARKER`.
|
|
108
|
+
return type.openpressMarker === marker;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractProps(element) {
|
|
112
|
+
if (!isReactElement(element) || !element.props) return {};
|
|
113
|
+
// Drop children — props are non-tree metadata only.
|
|
114
|
+
const { children, ...rest } = element.props;
|
|
115
|
+
return rest;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collectPressElements(root, PRESS_MARKER) {
|
|
119
|
+
const found = [];
|
|
120
|
+
walk(root);
|
|
121
|
+
return found;
|
|
122
|
+
|
|
123
|
+
function walk(node) {
|
|
124
|
+
if (!isReactElement(node)) {
|
|
125
|
+
// Could be array / fragment / string / number — flatten and recurse.
|
|
126
|
+
if (Array.isArray(node)) {
|
|
127
|
+
for (const child of node) walk(child);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (isMarked(node, PRESS_MARKER)) {
|
|
132
|
+
found.push(node);
|
|
133
|
+
// Don't descend into Press — its children are the document tree,
|
|
134
|
+
// not more workspace structure.
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Recurse into children + Fragment-like wrappers.
|
|
138
|
+
const children = node.props?.children;
|
|
139
|
+
if (children == null) return;
|
|
140
|
+
React.Children.forEach(children, walk);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pickPressMetadata(pressProps) {
|
|
145
|
+
const out = {};
|
|
146
|
+
if (typeof pressProps.title === "string") out.title = pressProps.title;
|
|
147
|
+
if (pressProps.page !== undefined) out.page = pressProps.page;
|
|
148
|
+
if (typeof pressProps.slug === "string") out.slug = pressProps.slug;
|
|
149
|
+
if (typeof pressProps.theme === "string") out.theme = pressProps.theme;
|
|
150
|
+
if (typeof pressProps.componentsDir === "string") out.componentsDir = pressProps.componentsDir;
|
|
151
|
+
if (pressProps.captionNumbering !== undefined) out.captionNumbering = pressProps.captionNumbering;
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Convert the v1.0 <Press sources={[ mdxSource({ id, ... }), ... ]}> array
|
|
156
|
+
// into the engine's expected sources record { [id]: descriptor }. Returns
|
|
157
|
+
// null if no sources prop was declared (engine falls back to the named
|
|
158
|
+
// `export const sources` from the entry module — the v0.x shape).
|
|
159
|
+
function extractSources(pressProps) {
|
|
160
|
+
if (!Array.isArray(pressProps.sources)) return null;
|
|
161
|
+
const out = {};
|
|
162
|
+
for (const entry of pressProps.sources) {
|
|
163
|
+
if (!entry || typeof entry !== "object") continue;
|
|
164
|
+
const id = typeof entry.id === "string" ? entry.id : null;
|
|
165
|
+
if (!id) continue;
|
|
166
|
+
// Strip the id field — the engine's descriptor shape doesn't carry it
|
|
167
|
+
// (id was the record key in v0.x).
|
|
168
|
+
const { id: _omit, ...descriptor } = entry;
|
|
169
|
+
out[id] = descriptor;
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
@@ -3,8 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { loadConfig } from "../runtime/config.mjs";
|
|
4
4
|
import { collectSourceTextFiles } from "../runtime/source-text-tools.mjs";
|
|
5
5
|
import { insertCommentMarker } from "./comment-marker.mjs";
|
|
6
|
-
|
|
7
|
-
const MAX_PROJECT_ASSET_BODY_BYTES = 64 * 1024;
|
|
6
|
+
import { readJsonBody, writeJson } from "./http-json.mjs";
|
|
8
7
|
|
|
9
8
|
export async function handleProjectAssetRequest(req, res, {
|
|
10
9
|
root = ".",
|
|
@@ -16,7 +15,7 @@ export async function handleProjectAssetRequest(req, res, {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
try {
|
|
19
|
-
const body = await readJsonBody(req);
|
|
18
|
+
const body = await readJsonBody(req, { bodyLabel: "Project asset request" });
|
|
20
19
|
const config = await loadConfig(root);
|
|
21
20
|
const action = stringValue(body?.action);
|
|
22
21
|
const kind = stringValue(body?.kind);
|
|
@@ -53,6 +52,7 @@ export async function handleProjectAssetRequest(req, res, {
|
|
|
53
52
|
note: body?.note,
|
|
54
53
|
commentTarget: body?.commentTarget,
|
|
55
54
|
currentSource: body?.currentSource,
|
|
55
|
+
objectEntity: body?.objectEntity,
|
|
56
56
|
timestamp,
|
|
57
57
|
});
|
|
58
58
|
writeJson(res, 200, { ok: true, ...result });
|
|
@@ -133,6 +133,7 @@ async function createProjectAssetComment({
|
|
|
133
133
|
note,
|
|
134
134
|
commentTarget,
|
|
135
135
|
currentSource,
|
|
136
|
+
objectEntity,
|
|
136
137
|
timestamp,
|
|
137
138
|
}) {
|
|
138
139
|
const normalizedName = normalizeAssetName(kind, name);
|
|
@@ -146,13 +147,14 @@ async function createProjectAssetComment({
|
|
|
146
147
|
commentTarget: stringValue(commentTarget),
|
|
147
148
|
currentSource,
|
|
148
149
|
});
|
|
150
|
+
const objectHint = stringValue(objectEntity?.id) ? ` object=${stringValue(objectEntity.id)}` : "";
|
|
149
151
|
|
|
150
152
|
const result = await insertCommentMarker({
|
|
151
153
|
root: config.root,
|
|
152
154
|
path: target.path,
|
|
153
155
|
source: { line: target.line, column: 1 },
|
|
154
156
|
note: `${assetLabel(kind, normalizedName)}:${noteText}`,
|
|
155
|
-
hint: `openpress-project-asset kind=${kind} action=comment target=${target.reason} asset=${normalizedName}`,
|
|
157
|
+
hint: `openpress-project-asset kind=${kind} action=comment target=${target.reason} asset=${normalizedName}${objectHint}`,
|
|
156
158
|
timestamp,
|
|
157
159
|
});
|
|
158
160
|
|
|
@@ -357,23 +359,3 @@ async function fileExists(filePath) {
|
|
|
357
359
|
return false;
|
|
358
360
|
}
|
|
359
361
|
}
|
|
360
|
-
|
|
361
|
-
async function readJsonBody(req) {
|
|
362
|
-
let body = "";
|
|
363
|
-
for await (const chunk of req) {
|
|
364
|
-
body += String(chunk);
|
|
365
|
-
if (Buffer.byteLength(body, "utf8") > MAX_PROJECT_ASSET_BODY_BYTES) {
|
|
366
|
-
throw new Error("Project asset request body is too large.");
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
try {
|
|
370
|
-
return JSON.parse(body || "{}");
|
|
371
|
-
} catch {
|
|
372
|
-
throw new Error("Project asset request body must be valid JSON.");
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function writeJson(res, status, body) {
|
|
377
|
-
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
378
|
-
res.end(`${JSON.stringify(body, null, 2)}\n`);
|
|
379
|
-
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { loadConfig } from "../runtime/config.mjs";
|
|
2
|
+
import { applySourceBlockTextEdit, readSourceBlockText } from "../runtime/source-text-tools.mjs";
|
|
3
|
+
import { exportReactDocument } from "./document-export.mjs";
|
|
4
|
+
import { readJsonBody, writeJson } from "./http-json.mjs";
|
|
5
|
+
|
|
6
|
+
export async function handleSourceEditRequest(req, res, {
|
|
7
|
+
root = ".",
|
|
8
|
+
refreshDocument = true,
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (req.method === "GET") {
|
|
11
|
+
try {
|
|
12
|
+
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
13
|
+
const config = await loadConfig(root);
|
|
14
|
+
const sourceText = await readSourceBlockText({
|
|
15
|
+
config,
|
|
16
|
+
path: requestUrl.searchParams.get("path"),
|
|
17
|
+
source: {
|
|
18
|
+
line: Number(requestUrl.searchParams.get("line")),
|
|
19
|
+
column: Number(requestUrl.searchParams.get("column") || 1),
|
|
20
|
+
endLine: Number(requestUrl.searchParams.get("endLine") || requestUrl.searchParams.get("line")),
|
|
21
|
+
endColumn: Number(requestUrl.searchParams.get("endColumn") || requestUrl.searchParams.get("column") || 1),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
writeJson(res, 200, { ok: true, source: sourceText });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
writeJson(res, 400, {
|
|
27
|
+
ok: false,
|
|
28
|
+
message: error instanceof Error ? error.message : String(error),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (req.method !== "POST") {
|
|
35
|
+
writeJson(res, 405, { ok: false, message: "OpenPress source edit endpoint requires GET or POST." });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const body = await readJsonBody(req, {
|
|
41
|
+
bodyLabel: "OpenPress source edit request",
|
|
42
|
+
maxBytes: 256 * 1024,
|
|
43
|
+
});
|
|
44
|
+
const config = await loadConfig(root);
|
|
45
|
+
const edit = await applySourceBlockTextEdit({
|
|
46
|
+
config,
|
|
47
|
+
path: body?.path,
|
|
48
|
+
source: body?.source,
|
|
49
|
+
text: body?.text,
|
|
50
|
+
kind: body?.kind,
|
|
51
|
+
name: body?.name,
|
|
52
|
+
blockId: body?.blockId,
|
|
53
|
+
sourceMode: body?.sourceMode === true,
|
|
54
|
+
});
|
|
55
|
+
const exported = refreshDocument && body?.refreshDocument !== false
|
|
56
|
+
? await exportReactDocument(root, { syncAssets: false })
|
|
57
|
+
: null;
|
|
58
|
+
|
|
59
|
+
writeJson(res, 200, {
|
|
60
|
+
ok: true,
|
|
61
|
+
edit,
|
|
62
|
+
document: exported
|
|
63
|
+
? {
|
|
64
|
+
path: exported.documentPath,
|
|
65
|
+
pageCount: exported.pageCount,
|
|
66
|
+
}
|
|
67
|
+
: undefined,
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
writeJson(res, 400, {
|
|
71
|
+
ok: false,
|
|
72
|
+
message: error instanceof Error ? error.message : String(error),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
import fs from "node:fs/promises";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import React from "react";
|
|
13
|
+
import { documentRelativePath, resolveDocumentRelativePath } from "../../runtime/path-utils.mjs";
|
|
13
14
|
import { compileMdx } from "../mdx-compile.mjs";
|
|
14
15
|
import { createHeadingState, fallbackOutlineItems, headingAttributesForBlock } from "./heading-numbering.mjs";
|
|
15
16
|
|
|
16
17
|
const MDX_EXT = ".mdx";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* Resolve all sources registered in `
|
|
20
|
+
* Resolve all sources registered in `press/index.tsx`.
|
|
20
21
|
*
|
|
21
22
|
* @param {object} opts
|
|
22
23
|
* @param {Record<string, object>} opts.sources The raw `sources` export.
|
|
@@ -103,6 +104,7 @@ async function resolveSource({ sourceId, descriptor, documentRoot, globalCompone
|
|
|
103
104
|
kind: block.kind,
|
|
104
105
|
name: block.name,
|
|
105
106
|
text: block.text,
|
|
107
|
+
layout: block.layout,
|
|
106
108
|
chainId,
|
|
107
109
|
sectionSlug: section.slug,
|
|
108
110
|
path: documentRelative(file.absolutePath, documentRoot),
|
|
@@ -323,6 +325,7 @@ function compileTocBlocks({ tocBlocks, chainId, blockIds, toc }) {
|
|
|
323
325
|
{
|
|
324
326
|
className,
|
|
325
327
|
"data-openpress-block-id": block.id,
|
|
328
|
+
"data-openpress-object-id": createBlockObjectEntityId(block.id),
|
|
326
329
|
"data-openpress-toc-entry": block.sectionSlug,
|
|
327
330
|
},
|
|
328
331
|
React.createElement(
|
|
@@ -352,6 +355,14 @@ function locateSection(renderData, chainId) {
|
|
|
352
355
|
throw new Error(`No section found for chainId "${chainId}" in source "${renderData.sourceId}".`);
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
function createObjectEntityId(kind, ...parts) {
|
|
359
|
+
return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function createBlockObjectEntityId(blockId) {
|
|
363
|
+
return createObjectEntityId("mdx-block", blockId);
|
|
364
|
+
}
|
|
365
|
+
|
|
355
366
|
// ---------------------------------------------------------------------------
|
|
356
367
|
// Validation
|
|
357
368
|
// ---------------------------------------------------------------------------
|
|
@@ -425,17 +436,4 @@ function deriveTitleFromDirName(name) {
|
|
|
425
436
|
.join(" ");
|
|
426
437
|
}
|
|
427
438
|
|
|
428
|
-
|
|
429
|
-
return path.relative(documentRoot, absolutePath).split(path.sep).join("/");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function resolveDocumentRelativePath(documentRoot, rel, label) {
|
|
433
|
-
if (typeof rel !== "string" || !rel.trim()) throw new Error(`${label} must be a non-empty document-relative path.`);
|
|
434
|
-
if (rel.includes("..")) throw new Error(`${label} contains "..", rejected.`);
|
|
435
|
-
const absolutePath = path.resolve(documentRoot, rel);
|
|
436
|
-
const relCheck = path.relative(documentRoot, absolutePath);
|
|
437
|
-
if (relCheck.startsWith("..") || path.isAbsolute(relCheck)) {
|
|
438
|
-
throw new Error(`${label} escapes the document root.`);
|
|
439
|
-
}
|
|
440
|
-
return absolutePath;
|
|
441
|
-
}
|
|
439
|
+
const documentRelative = documentRelativePath;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { documentRelativePath } from "../runtime/path-utils.mjs";
|
|
3
4
|
|
|
4
5
|
// Style discovery — only used to find per-section CSS files for the
|
|
5
6
|
// section-folders preset. MDX content discovery lives in `sources/mdx-resolver`.
|
|
@@ -8,13 +9,31 @@ import path from "node:path";
|
|
|
8
9
|
|
|
9
10
|
const COMPONENT_EXT = ".tsx";
|
|
10
11
|
|
|
11
|
-
export async function discoverSectionStyles(root = ".", config = {}) {
|
|
12
|
+
export async function discoverSectionStyles(root = ".", config = {}, { sectionRoots } = {}) {
|
|
12
13
|
const workspaceRoot = path.resolve(root);
|
|
13
|
-
const documentRoot = config.paths?.documentRoot ?? path.join(workspaceRoot, "
|
|
14
|
+
const documentRoot = config.paths?.documentRoot ?? path.join(workspaceRoot, "press");
|
|
14
15
|
const componentsRoot = config.paths?.componentsDir ?? path.join(documentRoot, "components");
|
|
15
|
-
const sectionsRoot = config.paths?.chaptersDir ?? config.paths?.sourceDir ?? path.join(documentRoot, "chapters");
|
|
16
16
|
const globalComponents = await discoverComponents(componentsRoot, documentRoot, "global");
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
// Multi-Press workspaces can place their chapters under per-Press
|
|
19
|
+
// subfolders (e.g. press/userstory/chapters/, press/slidepack/chapters/).
|
|
20
|
+
// The caller passes each Press's resolved section-folders root; we
|
|
21
|
+
// discover sections in every root and merge. Duplicate paths are
|
|
22
|
+
// de-duplicated by absolutePath. Falls back to the workspace-default
|
|
23
|
+
// root (press/chapters/) when no roots are passed in.
|
|
24
|
+
const effectiveRoots = sectionRoots && sectionRoots.length > 0
|
|
25
|
+
? sectionRoots
|
|
26
|
+
: [config.paths?.chaptersDir ?? config.paths?.sourceDir ?? path.join(documentRoot, "chapters")];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const sections = [];
|
|
29
|
+
for (const sectionsRoot of effectiveRoots) {
|
|
30
|
+
const found = await discoverSections(documentRoot, sectionsRoot);
|
|
31
|
+
for (const section of found) {
|
|
32
|
+
if (seen.has(section.absolutePath)) continue;
|
|
33
|
+
seen.add(section.absolutePath);
|
|
34
|
+
sections.push(section);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
18
37
|
|
|
19
38
|
return {
|
|
20
39
|
root: workspaceRoot,
|
|
@@ -102,10 +121,6 @@ function pathRecord(absolutePath, documentRoot) {
|
|
|
102
121
|
};
|
|
103
122
|
}
|
|
104
123
|
|
|
105
|
-
function documentRelativePath(absolutePath, documentRoot) {
|
|
106
|
-
return path.relative(documentRoot, absolutePath).split(path.sep).join("/");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
124
|
function compareSectionDirectories(a, b) {
|
|
110
125
|
const left = sectionSortKey(a.name);
|
|
111
126
|
const right = sectionSortKey(b.name);
|
|
@@ -10,6 +10,14 @@ export interface ResolvedConfig {
|
|
|
10
10
|
componentsDir: string;
|
|
11
11
|
publicDir: string;
|
|
12
12
|
outputDir: string;
|
|
13
|
+
page: null | {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
width: string;
|
|
17
|
+
height: string;
|
|
18
|
+
aspectRatio?: string;
|
|
19
|
+
heightRatio?: string;
|
|
20
|
+
};
|
|
13
21
|
pdf: {
|
|
14
22
|
filename: string;
|
|
15
23
|
};
|