@open-press/core 0.8.0 → 1.1.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 +17 -5
- package/engine/cli.mjs +9 -9
- package/engine/commands/_shared.mjs +70 -18
- package/engine/commands/deploy.mjs +3 -3
- package/engine/commands/dev.mjs +13 -4
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/inspect.mjs +3 -2
- package/engine/commands/pdf.mjs +2 -2
- package/engine/commands/preview.mjs +2 -2
- package/engine/commands/render.mjs +6 -4
- package/engine/commands/replace.mjs +1 -1
- package/engine/commands/search.mjs +1 -1
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +71 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +92 -0
- package/engine/output/static-server.mjs +60 -17
- package/engine/react/comment-marker.mjs +13 -13
- package/engine/react/document-entry.mjs +35 -28
- package/engine/react/document-export.mjs +309 -170
- package/engine/react/mdx-compile.mjs +30 -0
- package/engine/react/measurement-css.mjs +21 -0
- package/engine/react/object-entities.mjs +85 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +31 -65
- package/engine/react/pipeline/frame-measurement.mjs +4 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- package/engine/react/sources/mdx-resolver.mjs +1 -1
- package/engine/react/style-discovery.mjs +22 -4
- 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/page-geometry.mjs +131 -0
- package/engine/runtime/source-text-tools.mjs +1 -1
- package/engine/runtime/source-workspace.mjs +12 -3
- package/engine/runtime/validation.mjs +19 -10
- package/index.html +4 -0
- package/package.json +9 -12
- package/src/main.tsx +16 -0
- package/src/openpress/app/OpenPressApp.tsx +173 -17
- package/src/openpress/app/OpenPressRuntime.tsx +10 -2
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/core/Frame.tsx +20 -7
- package/src/openpress/core/FrameContext.tsx +2 -0
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/index.tsx +10 -3
- package/src/openpress/core/primitives.tsx +48 -1
- package/src/openpress/core/types.ts +86 -41
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/document-model/documentTypes.ts +9 -0
- package/src/openpress/document-model/index.ts +1 -0
- package/src/openpress/document-model/objectEntityModel.ts +4 -0
- package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/src/openpress/reader/index.ts +1 -0
- package/src/openpress/workbench/Workbench.tsx +120 -21
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +3 -3
- package/src/openpress/workbench/actions/index.ts +1 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +7 -1
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +5 -1
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +4 -2
- package/src/openpress/workbench/project/projectSourceModel.ts +2 -2
- package/src/openpress/workbench/workbenchFormatters.ts +2 -2
- package/src/styles/openpress/reader-runtime.css +9 -0
- package/src/styles/openpress/workbench-panels.css +113 -0
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +1 -5
- package/src/vite-env.d.ts +8 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +6 -6
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
|
+
import React from "react";
|
|
9
10
|
import { documentRelativePath, pageToBlock } from "../output/page-block.mjs";
|
|
10
11
|
import { syncPublicAssets } from "../output/public-assets.mjs";
|
|
12
|
+
import { pageGeometryToTheme } from "../runtime/page-geometry.mjs";
|
|
13
|
+
import { normalizePageGeometry } from "../runtime/page-geometry.mjs";
|
|
11
14
|
import { createCaptionNumberingState, numberCaptionsInHtml } from "./caption-numbering.mjs";
|
|
12
15
|
import { buildSectionScopedCss } from "./section-css.mjs";
|
|
13
16
|
import { CORE_ENTRY, createReactSsrServer, loadReactDocumentEntry } from "./document-entry.mjs";
|
|
@@ -50,7 +53,11 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
// Discover workspace for component scope and chapter-scoped style files.
|
|
53
|
-
|
|
56
|
+
// Pass every Press's resolved section-folders root so per-Press chapter
|
|
57
|
+
// folders (e.g. press/userstory/chapters/) are all picked up — the
|
|
58
|
+
// workspace can host more than one chapter root.
|
|
59
|
+
const sectionRoots = collectSectionRoots(entry.presses, entry.config.paths.documentRoot);
|
|
60
|
+
const workspace = await discoverSectionStyles(workspaceRoot, entry.config, { sectionRoots });
|
|
54
61
|
const coreAuthorComponents = {};
|
|
55
62
|
for (const name of ["MediaFigure", "ImageFigure"]) {
|
|
56
63
|
if (typeof coreModule[name] === "function") coreAuthorComponents[name] = coreModule[name];
|
|
@@ -60,199 +67,309 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
60
67
|
...(await loadComponentModules(server, workspace.globalComponents ?? [])),
|
|
61
68
|
};
|
|
62
69
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
sources: entry.sources,
|
|
67
|
-
documentRoot,
|
|
68
|
-
globalComponents,
|
|
69
|
-
});
|
|
70
|
+
// Build measurement CSS once at the workspace level — shared by every
|
|
71
|
+
// Press inside the Workspace.
|
|
72
|
+
const measurementCss = await buildReactMeasurementCss(workspaceRoot, entry.config, workspace);
|
|
70
73
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
PressContext,
|
|
83
|
-
sources,
|
|
84
|
-
hints,
|
|
85
|
-
});
|
|
86
|
-
lastFrames = frames;
|
|
87
|
-
validateAllChainsKnown(frames, sources);
|
|
88
|
-
const measurement = await measureFrames({
|
|
89
|
-
pressHtml: html,
|
|
90
|
-
sources,
|
|
91
|
-
renderRegistry,
|
|
92
|
-
css,
|
|
93
|
-
baseHref: pathToFileURL(`${documentRoot}${path.sep}`).href,
|
|
94
|
-
mediaDir: path.join(documentRoot, "media"),
|
|
95
|
-
captionNumbering: entry.config.captionNumbering,
|
|
74
|
+
// Write chapter-scoped CSS once (workspace shared). Every per-press
|
|
75
|
+
// readerDocument references the same file via "/openpress/chapter-scoped.css".
|
|
76
|
+
const chapterCss = await buildSectionScopedCss(workspace);
|
|
77
|
+
const sharedStyles = [];
|
|
78
|
+
await fs.mkdir(entry.config.paths.publicDir, { recursive: true });
|
|
79
|
+
if (chapterCss.trim()) {
|
|
80
|
+
await fs.writeFile(path.join(entry.config.paths.publicDir, "chapter-scoped.css"), chapterCss, "utf8");
|
|
81
|
+
sharedStyles.push({
|
|
82
|
+
kind: "chapter-scoped-css",
|
|
83
|
+
href: "/openpress/chapter-scoped.css",
|
|
84
|
+
path: "chapter-scoped.css",
|
|
96
85
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Iterate every Press declared inside <Workspace>. Single-doc
|
|
89
|
+
// workspaces just have length-1 here; the code path is uniform.
|
|
90
|
+
const pressResults = [];
|
|
91
|
+
for (const press of entry.presses) {
|
|
92
|
+
const result = await exportSinglePress({
|
|
93
|
+
press,
|
|
94
|
+
entry,
|
|
95
|
+
workspaceRoot,
|
|
96
|
+
server,
|
|
97
|
+
coreModule,
|
|
98
|
+
PressContext,
|
|
99
|
+
workspace,
|
|
100
|
+
globalComponents,
|
|
101
|
+
measurementCss,
|
|
102
|
+
sharedStyles,
|
|
102
103
|
});
|
|
103
|
-
|
|
104
|
-
const sample = measurement.mdxAreas
|
|
105
|
-
.slice(0, 5)
|
|
106
|
-
.map((a) => `${a.frameKey}#${a.indexInFrame} cap=${a.capacity.toFixed(0)} raw=${(a.rawHeight ?? 0).toFixed(0)}`);
|
|
107
|
-
const blocks = measurement.blockHeights
|
|
108
|
-
.slice(0, 8)
|
|
109
|
-
.map((b) => `${b.id} h=${b.height.toFixed(0)}`);
|
|
110
|
-
process.stderr.write(`[allocator iter ${iteration}]\n`);
|
|
111
|
-
process.stderr.write(` mdxAreas[0..4]: ${sample.join(" | ")}\n`);
|
|
112
|
-
process.stderr.write(` blocks[0..7]: ${blocks.join(" | ")}\n`);
|
|
113
|
-
process.stderr.write(` hints: ${JSON.stringify(alloc.hints.totalPagesPerChain)}\n`);
|
|
114
|
-
if (alloc.warnings.length > 0) {
|
|
115
|
-
process.stderr.write(` warnings: ${JSON.stringify(alloc.warnings)}\n`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (hintsEqual(hints, alloc.hints)) {
|
|
119
|
-
allocation = alloc.allocation;
|
|
120
|
-
warnings = alloc.warnings;
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
hints = alloc.hints;
|
|
104
|
+
pressResults.push(result);
|
|
124
105
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
106
|
+
|
|
107
|
+
// Build workspace.json — one entry per Press. The reader fetches
|
|
108
|
+
// this first to decide between gallery (length > 1) and direct
|
|
109
|
+
// load (length 1).
|
|
110
|
+
const workspaceManifest = {
|
|
111
|
+
version: 1,
|
|
112
|
+
name: typeof entry.workspaceProps?.name === "string" && entry.workspaceProps.name.trim()
|
|
113
|
+
? entry.workspaceProps.name.trim()
|
|
114
|
+
: null,
|
|
115
|
+
presses: pressResults.map((r) => ({
|
|
116
|
+
slug: r.slug,
|
|
117
|
+
title: r.readerDocument.meta.title,
|
|
118
|
+
page: r.readerDocument.theme ?? null,
|
|
119
|
+
pageCount: r.pageCount,
|
|
120
|
+
documentUrl: r.documentUrl,
|
|
121
|
+
})),
|
|
122
|
+
};
|
|
123
|
+
const workspacePath = path.join(entry.config.paths.publicDir, "workspace.json");
|
|
124
|
+
await fs.writeFile(workspacePath, JSON.stringify(workspaceManifest, null, 2), "utf8");
|
|
125
|
+
|
|
126
|
+
if (syncAssets) {
|
|
127
|
+
await syncPublicAssets(workspaceRoot, entry.config.paths.publicDir, entry.config);
|
|
130
128
|
}
|
|
131
129
|
|
|
132
|
-
const
|
|
130
|
+
const primary = pressResults[0];
|
|
131
|
+
return {
|
|
132
|
+
documentPath: primary?.documentPath,
|
|
133
|
+
pageCount: primary?.pageCount ?? 0,
|
|
134
|
+
document: primary?.readerDocument,
|
|
135
|
+
presses: pressResults,
|
|
136
|
+
};
|
|
137
|
+
} finally {
|
|
138
|
+
await server.close();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Render one Press from the Workspace into its own document.json.
|
|
143
|
+
// Called once per <Press> child; single-doc workspaces just call this
|
|
144
|
+
// once with the only Press. Returns the per-press summary the
|
|
145
|
+
// workspace manifest is built from.
|
|
146
|
+
async function exportSinglePress({
|
|
147
|
+
press,
|
|
148
|
+
entry,
|
|
149
|
+
workspaceRoot,
|
|
150
|
+
server,
|
|
151
|
+
coreModule,
|
|
152
|
+
PressContext,
|
|
153
|
+
workspace,
|
|
154
|
+
globalComponents,
|
|
155
|
+
measurementCss,
|
|
156
|
+
sharedStyles,
|
|
157
|
+
}) {
|
|
158
|
+
const slug = typeof press.metadata?.slug === "string" && press.metadata.slug.trim()
|
|
159
|
+
? press.metadata.slug.trim()
|
|
160
|
+
: "";
|
|
161
|
+
|
|
162
|
+
// Effective config for this press: workspace config with per-press
|
|
163
|
+
// metadata overlaid. Press JSX page prop wins over the workspace page.
|
|
164
|
+
const effectiveConfig = applyPressOverridesToConfig(entry.config, press.metadata);
|
|
165
|
+
const documentRoot = effectiveConfig.paths.documentRoot;
|
|
166
|
+
|
|
167
|
+
// Resolve sources for this press. The 1.0 contract reads them from
|
|
168
|
+
// <Press sources={[...]}>; the v0.x legacy path uses the synthesized
|
|
169
|
+
// record from `export const sources`.
|
|
170
|
+
const sourcesRecord = press.sources ?? {};
|
|
171
|
+
const { resolved: sources, renderData: renderRegistry } = await resolveAllSources({
|
|
172
|
+
sources: sourcesRecord,
|
|
173
|
+
documentRoot,
|
|
174
|
+
globalComponents,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Component the render pipeline drives. For Press elements captured
|
|
178
|
+
// by inspection (1.0 contract), wrap the captured element in a thin
|
|
179
|
+
// function component. For legacy projects without inspection data,
|
|
180
|
+
// fall back to the user's whole default export.
|
|
181
|
+
const PressComponent = press.element
|
|
182
|
+
? () => press.element
|
|
183
|
+
: entry.Press;
|
|
133
184
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
185
|
+
// Iterative allocation loop (identical to v0.x — paginates until the
|
|
186
|
+
// hints stabilise).
|
|
187
|
+
let hints = null;
|
|
188
|
+
let allocation = null;
|
|
189
|
+
let lastFrames = null;
|
|
190
|
+
let warnings = [];
|
|
191
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
192
|
+
const { html, frames } = expandPressTree({
|
|
193
|
+
Press: PressComponent,
|
|
137
194
|
PressContext,
|
|
138
195
|
sources,
|
|
139
196
|
hints,
|
|
140
|
-
|
|
141
|
-
|
|
197
|
+
});
|
|
198
|
+
lastFrames = frames;
|
|
199
|
+
validateAllChainsKnown(frames, sources);
|
|
200
|
+
const measurement = await measureFrames({
|
|
201
|
+
pressHtml: html,
|
|
202
|
+
sources,
|
|
142
203
|
renderRegistry,
|
|
204
|
+
css: measurementCss,
|
|
205
|
+
baseHref: pathToFileURL(`${documentRoot}${path.sep}`).href,
|
|
206
|
+
mediaDir: path.join(documentRoot, "media"),
|
|
207
|
+
captionNumbering: effectiveConfig.captionNumbering,
|
|
143
208
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
const alloc = allocateChains({
|
|
210
|
+
frames,
|
|
211
|
+
mdxAreas: measurement.mdxAreas,
|
|
212
|
+
blockHeights: measurement.blockHeights,
|
|
213
|
+
sources,
|
|
214
|
+
});
|
|
215
|
+
if (process.env.OPENPRESS_DEBUG_ALLOC) {
|
|
216
|
+
const sample = measurement.mdxAreas
|
|
217
|
+
.slice(0, 5)
|
|
218
|
+
.map((a) => `${a.frameKey}#${a.indexInFrame} cap=${a.capacity.toFixed(0)} raw=${(a.rawHeight ?? 0).toFixed(0)}`);
|
|
219
|
+
const blocks = measurement.blockHeights
|
|
220
|
+
.slice(0, 8)
|
|
221
|
+
.map((b) => `${b.id} h=${b.height.toFixed(0)}`);
|
|
222
|
+
process.stderr.write(`[allocator press=${slug || "(root)"} iter ${iteration}]\n`);
|
|
223
|
+
process.stderr.write(` mdxAreas[0..4]: ${sample.join(" | ")}\n`);
|
|
224
|
+
process.stderr.write(` blocks[0..7]: ${blocks.join(" | ")}\n`);
|
|
225
|
+
process.stderr.write(` hints: ${JSON.stringify(alloc.hints.totalPagesPerChain)}\n`);
|
|
226
|
+
if (alloc.warnings.length > 0) {
|
|
227
|
+
process.stderr.write(` warnings: ${JSON.stringify(alloc.warnings)}\n`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (hintsEqual(hints, alloc.hints)) {
|
|
231
|
+
allocation = alloc.allocation;
|
|
232
|
+
warnings = alloc.warnings;
|
|
233
|
+
break;
|
|
157
234
|
}
|
|
235
|
+
hints = alloc.hints;
|
|
236
|
+
}
|
|
237
|
+
if (allocation == null) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Allocation did not converge after ${MAX_ITERATIONS} iterations (press="${slug || "(root)"}"). ` +
|
|
240
|
+
`This usually means a chain keeps growing without fitting; check MdxArea capacities and block heights.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
158
243
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
244
|
+
const toc = buildTocContext({ sources, frames: lastFrames ?? [], allocation });
|
|
245
|
+
|
|
246
|
+
const final = await renderFinalPress({
|
|
247
|
+
Press: PressComponent,
|
|
248
|
+
PressContext,
|
|
249
|
+
sources,
|
|
250
|
+
hints,
|
|
251
|
+
toc,
|
|
252
|
+
allocation,
|
|
253
|
+
renderRegistry,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Build the reader's document.json. Same shape as v0.x; the only
|
|
257
|
+
// change is metadata.title comes from the per-press Press JSX prop.
|
|
258
|
+
const blockMap = {};
|
|
259
|
+
const captionState = createCaptionNumberingState();
|
|
260
|
+
const blocks = final.frames.map((frame, index) => {
|
|
261
|
+
const source = {
|
|
262
|
+
file: "index.tsx",
|
|
263
|
+
path: slug ? `press/${slug}/index.tsx` : "press/index.tsx",
|
|
264
|
+
kind: frame.role ?? "manuscript.content",
|
|
265
|
+
slug: frame.frameKey,
|
|
266
|
+
sectionIndex: index + 1,
|
|
267
|
+
};
|
|
268
|
+
const html = numberCaptionsInHtml(frame.html, effectiveConfig.captionNumbering, captionState);
|
|
269
|
+
for (const id of collectFrameBlockIds(frame.blockIds, html)) {
|
|
270
|
+
blockMap[id] = { id, pageIndex: index, pageNumber: index + 1, frameKey: frame.frameKey };
|
|
271
|
+
}
|
|
272
|
+
const block = pageToBlock(index, html, source, effectiveConfig, {
|
|
273
|
+
idPrefix: "openpress-page",
|
|
274
|
+
anchorPrefix: "page",
|
|
275
|
+
titleFallback: "Page",
|
|
187
276
|
});
|
|
277
|
+
return {
|
|
278
|
+
...block,
|
|
279
|
+
frameKey: frame.frameKey,
|
|
280
|
+
role: frame.role ?? null,
|
|
281
|
+
chrome: frame.chrome ?? true,
|
|
282
|
+
blockIds: frame.blockIds,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
188
285
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
sectionSlug: sourceRecord.sectionSlug,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
286
|
+
const sourceBlockIndex = buildSourceBlockIndex(sources);
|
|
287
|
+
for (const id of Object.keys(blockMap)) {
|
|
288
|
+
const sourceRecord = sourceBlockIndex.get(id);
|
|
289
|
+
if (sourceRecord) {
|
|
290
|
+
blockMap[id] = {
|
|
291
|
+
...blockMap[id],
|
|
292
|
+
kind: sourceRecord.kind,
|
|
293
|
+
name: sourceRecord.name,
|
|
294
|
+
path: sourceRecord.path,
|
|
295
|
+
source: sourceRecord.source,
|
|
296
|
+
chainId: sourceRecord.chainId,
|
|
297
|
+
sectionSlug: sourceRecord.sectionSlug,
|
|
298
|
+
};
|
|
205
299
|
}
|
|
300
|
+
}
|
|
206
301
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
302
|
+
const objectEntities = buildObjectEntities({
|
|
303
|
+
frames: final.frames.map((frame, index) => ({ ...frame, pageIndex: index })),
|
|
304
|
+
blocks,
|
|
305
|
+
blockMap,
|
|
306
|
+
});
|
|
212
307
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
308
|
+
const readerDocument = {
|
|
309
|
+
meta: {
|
|
310
|
+
title: trimmedString(effectiveConfig.title) ?? "Untitled Document",
|
|
311
|
+
subtitle: trimmedString(effectiveConfig.subtitle) ?? "",
|
|
312
|
+
organization: trimmedString(effectiveConfig.organization) ?? "",
|
|
313
|
+
workspaceLabel: trimmedString(effectiveConfig.workspaceLabel) ?? "",
|
|
314
|
+
version: "openpress-press-tree-v1",
|
|
315
|
+
},
|
|
316
|
+
theme: pageGeometryToTheme(effectiveConfig.page),
|
|
317
|
+
source: {
|
|
318
|
+
type: "openpress-press-tree-mdx",
|
|
319
|
+
contentDir: documentRelativePath(effectiveConfig, effectiveConfig.sourceDir),
|
|
320
|
+
editable: true,
|
|
321
|
+
editMode: "source-mdx",
|
|
322
|
+
styles: sharedStyles,
|
|
323
|
+
blockMap,
|
|
324
|
+
objectEntities,
|
|
325
|
+
frames: final.frames.map((frame, index) => ({
|
|
326
|
+
frameKey: frame.frameKey,
|
|
327
|
+
role: frame.role ?? null,
|
|
328
|
+
pageIndex: index,
|
|
329
|
+
mdxAreas: frame.mdxAreas.map((area) => ({
|
|
330
|
+
chainId: area.chainId,
|
|
331
|
+
indexInFrame: area.indexInFrame,
|
|
332
|
+
blockIds: area.blockIds,
|
|
238
333
|
})),
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
334
|
+
})),
|
|
335
|
+
chains: Object.keys(sources).flatMap((id) => Object.keys(sources[id].chains)),
|
|
336
|
+
warnings,
|
|
337
|
+
},
|
|
338
|
+
blocks,
|
|
339
|
+
};
|
|
244
340
|
|
|
245
|
-
|
|
246
|
-
|
|
341
|
+
// Output path: empty slug → root /openpress/document.json (legacy
|
|
342
|
+
// single-Press shape). Non-empty slug → /openpress/<slug>/document.json.
|
|
343
|
+
const pressOutputDir = slug
|
|
344
|
+
? path.join(effectiveConfig.paths.publicDir, slug)
|
|
345
|
+
: effectiveConfig.paths.publicDir;
|
|
346
|
+
await fs.mkdir(pressOutputDir, { recursive: true });
|
|
347
|
+
const documentPath = path.join(pressOutputDir, "document.json");
|
|
348
|
+
await fs.writeFile(documentPath, JSON.stringify(readerDocument, null, 2), "utf8");
|
|
247
349
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
350
|
+
return {
|
|
351
|
+
slug,
|
|
352
|
+
documentPath,
|
|
353
|
+
documentUrl: slug ? `/openpress/${slug}/document.json` : "/openpress/document.json",
|
|
354
|
+
readerDocument,
|
|
355
|
+
pageCount: blocks.length,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
251
358
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
359
|
+
// Apply per-Press JSX prop overrides onto the workspace-level config.
|
|
360
|
+
// Returns a new config object — the original is untouched so other
|
|
361
|
+
// presses in the same workspace get a clean base.
|
|
362
|
+
function applyPressOverridesToConfig(workspaceConfig, pressMetadata) {
|
|
363
|
+
if (!pressMetadata) return workspaceConfig;
|
|
364
|
+
const out = { ...workspaceConfig };
|
|
365
|
+
if (pressMetadata.title) out.title = pressMetadata.title;
|
|
366
|
+
if (pressMetadata.page !== undefined) {
|
|
367
|
+
out.page = normalizePageGeometry(pressMetadata.page);
|
|
255
368
|
}
|
|
369
|
+
if (pressMetadata.captionNumbering !== undefined) {
|
|
370
|
+
out.captionNumbering = { ...workspaceConfig.captionNumbering, ...pressMetadata.captionNumbering };
|
|
371
|
+
}
|
|
372
|
+
return out;
|
|
256
373
|
}
|
|
257
374
|
|
|
258
375
|
async function loadComponentModules(server, components) {
|
|
@@ -371,3 +488,25 @@ function trimmedString(value) {
|
|
|
371
488
|
const trimmed = value.trim();
|
|
372
489
|
return trimmed ? trimmed : null;
|
|
373
490
|
}
|
|
491
|
+
|
|
492
|
+
// Walk every Press's mdxSource descriptors and collect the absolute
|
|
493
|
+
// path each section-folders root resolves to. discoverSectionStyles
|
|
494
|
+
// iterates these to find section-scoped CSS across a multi-Press
|
|
495
|
+
// workspace where chapters live under per-Press subfolders.
|
|
496
|
+
function collectSectionRoots(presses, documentRoot) {
|
|
497
|
+
const roots = new Set();
|
|
498
|
+
for (const press of presses ?? []) {
|
|
499
|
+
const sources = press?.sources;
|
|
500
|
+
if (!sources || typeof sources !== "object") continue;
|
|
501
|
+
for (const descriptor of Object.values(sources)) {
|
|
502
|
+
if (descriptor?.type !== "mdx") continue;
|
|
503
|
+
if (descriptor?.preset !== "section-folders") continue;
|
|
504
|
+
const rel = typeof descriptor.root === "string" && descriptor.root.trim()
|
|
505
|
+
? descriptor.root.trim()
|
|
506
|
+
: "chapters";
|
|
507
|
+
roots.add(path.resolve(documentRoot, rel));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return [...roots];
|
|
511
|
+
}
|
|
512
|
+
|
|
@@ -183,6 +183,7 @@ function applyTableRowBlocks({
|
|
|
183
183
|
setDataAttribute(headerRecord.node, "data-openpress-block-id", headerRecord.id);
|
|
184
184
|
setDataAttribute(headerRecord.node, "data-openpress-object-id", createBlockObjectEntityId(headerRecord.id));
|
|
185
185
|
setDataAttribute(headerRecord.node, "data-openpress-block-layout", "attached");
|
|
186
|
+
annotateTableCells(headerRecord.node, headerRecord.id);
|
|
186
187
|
}
|
|
187
188
|
if (captionRecord) {
|
|
188
189
|
if (renderCaption) {
|
|
@@ -226,6 +227,13 @@ function applyTableRowBlocks({
|
|
|
226
227
|
for (const row of selected) {
|
|
227
228
|
setDataAttribute(row.node, "data-openpress-block-id", row.id);
|
|
228
229
|
setDataAttribute(row.node, "data-openpress-object-id", createBlockObjectEntityId(row.id));
|
|
230
|
+
// Bake cell-level object ids into every <td>/<th>. The inspector resolves
|
|
231
|
+
// a clicked target via `closest("[data-openpress-object-id]")` — without
|
|
232
|
+
// this, a click inside a cell would walk up to the row and a comment
|
|
233
|
+
// would target the entire row. With the cell-precision id present in the
|
|
234
|
+
// static HTML the inspector targets the individual cell, matching the
|
|
235
|
+
// engine's per-cell source-edit pipeline (`cellIndex`).
|
|
236
|
+
annotateTableCells(row.node, row.id);
|
|
229
237
|
blocks.push({
|
|
230
238
|
id: row.id,
|
|
231
239
|
kind: "table-row",
|
|
@@ -241,6 +249,28 @@ function applyTableRowBlocks({
|
|
|
241
249
|
return "skip";
|
|
242
250
|
}
|
|
243
251
|
|
|
252
|
+
function annotateTableCells(rowNode, rowBlockId) {
|
|
253
|
+
const children = Array.isArray(rowNode?.children) ? rowNode.children : [];
|
|
254
|
+
let cellIndex = 0;
|
|
255
|
+
for (const child of children) {
|
|
256
|
+
if (child?.type !== "element") continue;
|
|
257
|
+
if (child.tagName !== "td" && child.tagName !== "th") continue;
|
|
258
|
+
// Inherit the row's block id so `findObjectSelection` can resolve the
|
|
259
|
+
// cell's underlying SourceBlock (which lives on the row). The
|
|
260
|
+
// cell-precision `data-openpress-object-id` + cellIndex still let the
|
|
261
|
+
// inspector / source-edit pipeline target a single cell within that row.
|
|
262
|
+
// `data-openpress-inherited-block-id="true"` keeps the same convention
|
|
263
|
+
// the inline editor uses for caption / cell descendants, so block
|
|
264
|
+
// measurement (which queries `[data-openpress-block-id]`) can skip
|
|
265
|
+
// these and not double-count the row's height across N cells.
|
|
266
|
+
setDataAttribute(child, "data-openpress-block-id", rowBlockId);
|
|
267
|
+
setDataAttribute(child, "data-openpress-inherited-block-id", "true");
|
|
268
|
+
setDataAttribute(child, "data-openpress-object-id", `${createBlockObjectEntityId(rowBlockId)}:cell:${cellIndex}`);
|
|
269
|
+
setDataAttribute(child, "data-openpress-table-cell-index", String(cellIndex));
|
|
270
|
+
cellIndex += 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
244
274
|
export function remarkBlockOnlyMdx(options = {}) {
|
|
245
275
|
const filePath = String(options.filePath ?? "document.mdx");
|
|
246
276
|
|
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { buildComponentsCss, buildContentCss } from "../runtime/file-utils.mjs";
|
|
6
|
+
import { pageGeometryToTheme } from "../runtime/page-geometry.mjs";
|
|
6
7
|
import { buildSectionScopedCss } from "./section-css.mjs";
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
@@ -11,6 +12,7 @@ export async function buildReactMeasurementCss(root, config, workspace) {
|
|
|
11
12
|
const parts = [];
|
|
12
13
|
await appendOptionalFile(parts, path.join(config.paths.themeDir, "fonts.css"), "theme/fonts.css");
|
|
13
14
|
await appendOptionalFile(parts, path.join(config.paths.themeDir, "tokens.css"), "theme/tokens.css");
|
|
15
|
+
appendPageGeometryCss(parts, config.page);
|
|
14
16
|
parts.push("/* === public/openpress/content.css === */\n");
|
|
15
17
|
parts.push(await buildContentCss(root, config));
|
|
16
18
|
parts.push("\n/* === public/openpress/components.css === */\n");
|
|
@@ -23,6 +25,25 @@ export async function buildReactMeasurementCss(root, config, workspace) {
|
|
|
23
25
|
return rewriteAssetUrls(stripViewportMediaQueries(parts.join("\n")), config);
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function appendPageGeometryCss(parts, page) {
|
|
29
|
+
const theme = pageGeometryToTheme(page);
|
|
30
|
+
if (!theme) return;
|
|
31
|
+
|
|
32
|
+
const declarations = [
|
|
33
|
+
["--openpress-page-width", theme.pageWidth],
|
|
34
|
+
["--openpress-page-height", theme.pageHeight],
|
|
35
|
+
["--openpress-page-aspect-ratio", theme.pageAspectRatio],
|
|
36
|
+
["--openpress-page-height-ratio", theme.pageHeightRatio],
|
|
37
|
+
].filter(([, value]) => value);
|
|
38
|
+
|
|
39
|
+
parts.push("/* === openpress page geometry === */\n");
|
|
40
|
+
parts.push(":root {\n");
|
|
41
|
+
for (const [name, value] of declarations) {
|
|
42
|
+
parts.push(` ${name}: ${value};\n`);
|
|
43
|
+
}
|
|
44
|
+
parts.push("}\n\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
async function appendOptionalFile(parts, filePath, label) {
|
|
27
48
|
try {
|
|
28
49
|
const css = await fs.readFile(filePath, "utf8");
|