@open-press/cli 0.7.0 → 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/AGENTS.md +126 -0
- package/template/core/CHANGELOG.md +65 -0
- package/template/core/engine/commands/dev.mjs +2 -2
- package/template/core/engine/commands/upgrade.mjs +47 -5
- 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 +45 -5
- package/template/core/engine/react/http-json.mjs +24 -0
- package/template/core/engine/react/mdx-compile.mjs +187 -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 +40 -9
- 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/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +26 -12
- package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +37 -17
- package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +34 -16
- package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +22 -8
- package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +20 -15
- package/template/packs/academic-paper/document/components/Page.tsx +26 -3
- package/template/packs/academic-paper/document/index.tsx +51 -59
- package/template/packs/academic-paper/document/media/figure-placeholder.svg +9 -0
- package/template/packs/academic-paper/document/theme/base/page-contract.css +30 -13
- package/template/packs/academic-paper/document/theme/base/typography.css +30 -33
- package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +74 -47
- 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
|
@@ -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;
|
|
@@ -92,6 +100,21 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
92
100
|
blockHeights: measurement.blockHeights,
|
|
93
101
|
sources,
|
|
94
102
|
});
|
|
103
|
+
if (process.env.OPENPRESS_DEBUG_ALLOC) {
|
|
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
|
+
}
|
|
95
118
|
if (hintsEqual(hints, alloc.hints)) {
|
|
96
119
|
allocation = alloc.allocation;
|
|
97
120
|
warnings = alloc.warnings;
|
|
@@ -138,9 +161,6 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
138
161
|
const blockMap = {};
|
|
139
162
|
const captionState = createCaptionNumberingState();
|
|
140
163
|
const blocks = final.frames.map((frame, index) => {
|
|
141
|
-
for (const id of frame.blockIds) {
|
|
142
|
-
blockMap[id] = { id, pageIndex: index, pageNumber: index + 1 };
|
|
143
|
-
}
|
|
144
164
|
const source = {
|
|
145
165
|
file: "index.tsx",
|
|
146
166
|
path: "document/index.tsx",
|
|
@@ -149,6 +169,9 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
149
169
|
sectionIndex: index + 1,
|
|
150
170
|
};
|
|
151
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
|
+
}
|
|
152
175
|
const block = pageToBlock(index, html, source, entry.config, {
|
|
153
176
|
idPrefix: "openpress-page",
|
|
154
177
|
anchorPrefix: "page",
|
|
@@ -181,12 +204,18 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
181
204
|
}
|
|
182
205
|
}
|
|
183
206
|
|
|
207
|
+
const objectEntities = buildObjectEntities({
|
|
208
|
+
frames: final.frames.map((frame, index) => ({ ...frame, pageIndex: index })),
|
|
209
|
+
blocks,
|
|
210
|
+
blockMap,
|
|
211
|
+
});
|
|
212
|
+
|
|
184
213
|
const readerDocument = {
|
|
185
214
|
meta: {
|
|
186
215
|
title: trimmedString(entry.config.title) ?? "Untitled Document",
|
|
187
216
|
subtitle: trimmedString(entry.config.subtitle) ?? "",
|
|
188
217
|
organization: trimmedString(entry.config.organization) ?? "",
|
|
189
|
-
workspaceLabel: trimmedString(entry.config.workspaceLabel) ??
|
|
218
|
+
workspaceLabel: trimmedString(entry.config.workspaceLabel) ?? "",
|
|
190
219
|
version: "openpress-press-tree-v1",
|
|
191
220
|
},
|
|
192
221
|
source: {
|
|
@@ -196,6 +225,7 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
196
225
|
editMode: "source-mdx",
|
|
197
226
|
styles,
|
|
198
227
|
blockMap,
|
|
228
|
+
objectEntities,
|
|
199
229
|
frames: final.frames.map((frame, index) => ({
|
|
200
230
|
frameKey: frame.frameKey,
|
|
201
231
|
role: frame.role ?? null,
|
|
@@ -281,6 +311,16 @@ function buildSourceBlockIndex(sources) {
|
|
|
281
311
|
return index;
|
|
282
312
|
}
|
|
283
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
|
+
|
|
284
324
|
function buildTocContext({ sources, frames, allocation }) {
|
|
285
325
|
const toc = {};
|
|
286
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
|
+
}
|
|
@@ -99,9 +99,20 @@ export function rehypeBlockIds(options = {}) {
|
|
|
99
99
|
includeBlockIds,
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
+
if (block.name === "ul" || block.name === "ol") {
|
|
103
|
+
return applyListItemBlocks({
|
|
104
|
+
node,
|
|
105
|
+
id,
|
|
106
|
+
blocks,
|
|
107
|
+
filePath,
|
|
108
|
+
chapterSlug,
|
|
109
|
+
includeBlockIds,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
102
112
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
103
113
|
|
|
104
114
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
115
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
105
116
|
const extraAttributes = blockAttributes.get(id);
|
|
106
117
|
if (extraAttributes) {
|
|
107
118
|
for (const [name, value] of Object.entries(extraAttributes)) {
|
|
@@ -132,9 +143,19 @@ function applyTableRowBlocks({
|
|
|
132
143
|
includeBlockIds,
|
|
133
144
|
}) {
|
|
134
145
|
const rows = tableBodyRows(node);
|
|
146
|
+
const header = tableHeaderRow(node);
|
|
147
|
+
const caption = tableCaption(node);
|
|
148
|
+
const captionRecord = caption ? { id: `${id}-caption`, node: caption } : null;
|
|
149
|
+
const headerRecord = header ? { id: `${id}-h0`, node: header } : null;
|
|
150
|
+
const selectedCaption = captionRecord && (!includeBlockIds || includeBlockIds.has(captionRecord.id));
|
|
151
|
+
const selectedHeader = headerRecord && (!includeBlockIds || includeBlockIds.has(headerRecord.id));
|
|
152
|
+
const firstSelectedRowIndex = selectedFirstTableRowIndex(rows, includeBlockIds, id);
|
|
153
|
+
const renderCaption = selectedCaption || (captionRecord && includeBlockIds && firstSelectedRowIndex === 0);
|
|
154
|
+
const renderHeader = Boolean(headerRecord && (!includeBlockIds || firstSelectedRowIndex === 0 || selectedHeader));
|
|
135
155
|
if (rows.length === 0) {
|
|
136
156
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
137
157
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
158
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
138
159
|
blocks.push({
|
|
139
160
|
id,
|
|
140
161
|
kind: "element",
|
|
@@ -155,15 +176,56 @@ function applyTableRowBlocks({
|
|
|
155
176
|
const selected = includeBlockIds
|
|
156
177
|
? rowRecords.filter((row) => includeBlockIds.has(row.id))
|
|
157
178
|
: rowRecords;
|
|
158
|
-
if (selected.length === 0) return false;
|
|
179
|
+
if (selected.length === 0 && !selectedCaption && !selectedHeader) return false;
|
|
159
180
|
|
|
160
181
|
setDataAttribute(node, "data-openpress-table-id", id);
|
|
182
|
+
if (headerRecord && renderHeader) {
|
|
183
|
+
setDataAttribute(headerRecord.node, "data-openpress-block-id", headerRecord.id);
|
|
184
|
+
setDataAttribute(headerRecord.node, "data-openpress-object-id", createBlockObjectEntityId(headerRecord.id));
|
|
185
|
+
setDataAttribute(headerRecord.node, "data-openpress-block-layout", "attached");
|
|
186
|
+
}
|
|
187
|
+
if (captionRecord) {
|
|
188
|
+
if (renderCaption) {
|
|
189
|
+
setDataAttribute(captionRecord.node, "data-openpress-block-id", captionRecord.id);
|
|
190
|
+
setDataAttribute(captionRecord.node, "data-openpress-object-id", createBlockObjectEntityId(captionRecord.id));
|
|
191
|
+
if (selectedCaption) {
|
|
192
|
+
blocks.push({
|
|
193
|
+
id: captionRecord.id,
|
|
194
|
+
kind: "element",
|
|
195
|
+
name: "caption",
|
|
196
|
+
text: textContent(captionRecord.node).trim() || undefined,
|
|
197
|
+
filePath,
|
|
198
|
+
chapterSlug,
|
|
199
|
+
tableId: id,
|
|
200
|
+
layout: "attached",
|
|
201
|
+
source: sourcePosition(captionRecord.node.position ?? node.position),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
removeTableCaption(node);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (headerRecord && selectedHeader) {
|
|
209
|
+
blocks.push({
|
|
210
|
+
id: headerRecord.id,
|
|
211
|
+
kind: "table-row",
|
|
212
|
+
name: "table-header-row",
|
|
213
|
+
text: textContent(headerRecord.node).trim() || undefined,
|
|
214
|
+
filePath,
|
|
215
|
+
chapterSlug,
|
|
216
|
+
tableId: id,
|
|
217
|
+
rowIndex: -1,
|
|
218
|
+
layout: "attached",
|
|
219
|
+
source: sourcePosition(headerRecord.node.position ?? node.position),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
161
222
|
const selectedNodes = new Set(selected.map((row) => row.node));
|
|
162
223
|
pruneUnselectedTableRows(node, new Set(rowRecords.map((row) => row.node)), selectedNodes);
|
|
163
|
-
if (
|
|
224
|
+
if (!renderHeader) stripTableHeader(node);
|
|
164
225
|
|
|
165
226
|
for (const row of selected) {
|
|
166
227
|
setDataAttribute(row.node, "data-openpress-block-id", row.id);
|
|
228
|
+
setDataAttribute(row.node, "data-openpress-object-id", createBlockObjectEntityId(row.id));
|
|
167
229
|
blocks.push({
|
|
168
230
|
id: row.id,
|
|
169
231
|
kind: "table-row",
|
|
@@ -219,6 +281,7 @@ function normalizeTableCaptions(node) {
|
|
|
219
281
|
type: "element",
|
|
220
282
|
tagName: "caption",
|
|
221
283
|
properties: {},
|
|
284
|
+
position: child.position,
|
|
222
285
|
children: [{ type: "text", value: captionText }],
|
|
223
286
|
});
|
|
224
287
|
}
|
|
@@ -262,8 +325,10 @@ function wrapMdxComponents(components) {
|
|
|
262
325
|
if (typeof Component !== "function") continue;
|
|
263
326
|
wrapped[name] = function ComponentBlock(props = {}) {
|
|
264
327
|
const blockId = props["data-openpress-block-id"];
|
|
328
|
+
const objectId = props["data-openpress-object-id"] || (blockId ? createBlockObjectEntityId(blockId) : undefined);
|
|
265
329
|
const rest = { ...props };
|
|
266
330
|
delete rest["data-openpress-block-id"];
|
|
331
|
+
delete rest["data-openpress-object-id"];
|
|
267
332
|
|
|
268
333
|
if (!blockId) return React.createElement(Component, rest);
|
|
269
334
|
|
|
@@ -271,6 +336,7 @@ function wrapMdxComponents(components) {
|
|
|
271
336
|
"div",
|
|
272
337
|
{
|
|
273
338
|
"data-openpress-block-id": blockId,
|
|
339
|
+
"data-openpress-object-id": objectId,
|
|
274
340
|
"data-openpress-component-block": name,
|
|
275
341
|
},
|
|
276
342
|
React.createElement(Component, rest),
|
|
@@ -314,6 +380,89 @@ function blockInfo(node) {
|
|
|
314
380
|
return null;
|
|
315
381
|
}
|
|
316
382
|
|
|
383
|
+
function applyListItemBlocks({
|
|
384
|
+
node,
|
|
385
|
+
id,
|
|
386
|
+
blocks,
|
|
387
|
+
filePath,
|
|
388
|
+
chapterSlug,
|
|
389
|
+
includeBlockIds,
|
|
390
|
+
}) {
|
|
391
|
+
const items = listItems(node);
|
|
392
|
+
if (items.length === 0) {
|
|
393
|
+
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
394
|
+
setDataAttribute(node, "data-openpress-block-id", id);
|
|
395
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
396
|
+
blocks.push({
|
|
397
|
+
id,
|
|
398
|
+
kind: "element",
|
|
399
|
+
name: node.tagName,
|
|
400
|
+
text: textContent(node).trim() || undefined,
|
|
401
|
+
filePath,
|
|
402
|
+
chapterSlug,
|
|
403
|
+
source: sourcePosition(node.position),
|
|
404
|
+
});
|
|
405
|
+
return "skip";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const itemRecords = items.map((item, index) => ({
|
|
409
|
+
id: `${id}-i${index}`,
|
|
410
|
+
node: item,
|
|
411
|
+
index,
|
|
412
|
+
}));
|
|
413
|
+
const selected = includeBlockIds
|
|
414
|
+
? itemRecords.filter((item) => includeBlockIds.has(item.id))
|
|
415
|
+
: itemRecords;
|
|
416
|
+
if (selected.length === 0) return false;
|
|
417
|
+
|
|
418
|
+
setDataAttribute(node, "data-openpress-list-id", id);
|
|
419
|
+
|
|
420
|
+
// For ordered lists, continuation pages must keep numbering picking up
|
|
421
|
+
// from the first surviving item. `start` is the 1-based number of the
|
|
422
|
+
// first `<li>` rendered, so if the original list had `start="5"` and we
|
|
423
|
+
// dropped the first three items, continuation starts at 5 + 3 = 8.
|
|
424
|
+
if (node.tagName === "ol" && selected[0]?.index > 0) {
|
|
425
|
+
const baseStart = Number(node.properties?.start ?? 1);
|
|
426
|
+
const continuationStart = baseStart + selected[0].index;
|
|
427
|
+
node.properties = { ...node.properties, start: continuationStart };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const selectedNodes = new Set(selected.map((item) => item.node));
|
|
431
|
+
pruneUnselectedListItems(node, new Set(itemRecords.map((item) => item.node)), selectedNodes);
|
|
432
|
+
|
|
433
|
+
for (const item of selected) {
|
|
434
|
+
setDataAttribute(item.node, "data-openpress-block-id", item.id);
|
|
435
|
+
setDataAttribute(item.node, "data-openpress-object-id", createBlockObjectEntityId(item.id));
|
|
436
|
+
blocks.push({
|
|
437
|
+
id: item.id,
|
|
438
|
+
kind: "list-item",
|
|
439
|
+
name: "list-item",
|
|
440
|
+
text: textContent(item.node).trim() || undefined,
|
|
441
|
+
filePath,
|
|
442
|
+
chapterSlug,
|
|
443
|
+
listId: id,
|
|
444
|
+
listTag: node.tagName,
|
|
445
|
+
itemIndex: item.index,
|
|
446
|
+
source: sourcePosition(item.node.position ?? node.position),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return "skip";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function listItems(list) {
|
|
453
|
+
if (list?.type !== "element") return [];
|
|
454
|
+
if (list.tagName !== "ul" && list.tagName !== "ol") return [];
|
|
455
|
+
return (list.children ?? []).filter((child) => child?.type === "element" && child.tagName === "li");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function pruneUnselectedListItems(node, itemNodes, selectedNodes) {
|
|
459
|
+
if (!Array.isArray(node?.children)) return;
|
|
460
|
+
node.children = node.children.filter((child) => {
|
|
461
|
+
if (!itemNodes.has(child)) return true;
|
|
462
|
+
return selectedNodes.has(child);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
317
466
|
function tableBodyRows(table) {
|
|
318
467
|
if (table?.type !== "element" || table.tagName !== "table") return [];
|
|
319
468
|
const rows = [];
|
|
@@ -328,6 +477,28 @@ function tableBodyRows(table) {
|
|
|
328
477
|
return (table.children ?? []).filter((child) => child?.type === "element" && child.tagName === "tr");
|
|
329
478
|
}
|
|
330
479
|
|
|
480
|
+
function tableHeaderRow(table) {
|
|
481
|
+
if (table?.type !== "element" || table.tagName !== "table") return null;
|
|
482
|
+
for (const child of table.children ?? []) {
|
|
483
|
+
if (child?.type !== "element" || child.tagName !== "thead") continue;
|
|
484
|
+
return (child.children ?? []).find((row) => row?.type === "element" && row.tagName === "tr") ?? null;
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function selectedFirstTableRowIndex(rows, includeBlockIds, tableId) {
|
|
490
|
+
if (!includeBlockIds) return 0;
|
|
491
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
492
|
+
if (includeBlockIds.has(`${tableId}-r${index}`)) return index;
|
|
493
|
+
}
|
|
494
|
+
return -1;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function tableCaption(table) {
|
|
498
|
+
if (table?.type !== "element" || table.tagName !== "table") return null;
|
|
499
|
+
return (table.children ?? []).find((child) => child?.type === "element" && child.tagName === "caption") ?? null;
|
|
500
|
+
}
|
|
501
|
+
|
|
331
502
|
function pruneUnselectedTableRows(node, rowNodes, selectedNodes) {
|
|
332
503
|
if (!Array.isArray(node?.children)) return;
|
|
333
504
|
node.children = node.children.filter((child) => {
|
|
@@ -341,10 +512,15 @@ function stripTableHeader(table) {
|
|
|
341
512
|
if (!Array.isArray(table?.children)) return;
|
|
342
513
|
table.children = table.children.filter((child) => {
|
|
343
514
|
if (child?.type !== "element") return true;
|
|
344
|
-
return child.tagName !== "
|
|
515
|
+
return child.tagName !== "thead";
|
|
345
516
|
});
|
|
346
517
|
}
|
|
347
518
|
|
|
519
|
+
function removeTableCaption(table) {
|
|
520
|
+
if (!Array.isArray(table?.children)) return;
|
|
521
|
+
table.children = table.children.filter((child) => child?.type !== "element" || child.tagName !== "caption");
|
|
522
|
+
}
|
|
523
|
+
|
|
348
524
|
function headingText(node) {
|
|
349
525
|
if (!/^h[1-6]$/.test(String(node?.tagName ?? ""))) return undefined;
|
|
350
526
|
return textContent(node).trim() || undefined;
|
|
@@ -379,6 +555,14 @@ function setDataAttribute(node, name, value) {
|
|
|
379
555
|
node.properties[name] = value;
|
|
380
556
|
}
|
|
381
557
|
|
|
558
|
+
function createObjectEntityId(kind, ...parts) {
|
|
559
|
+
return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function createBlockObjectEntityId(blockId) {
|
|
563
|
+
return createObjectEntityId("mdx-block", blockId);
|
|
564
|
+
}
|
|
565
|
+
|
|
382
566
|
function visit(node, visitor) {
|
|
383
567
|
visitor(node);
|
|
384
568
|
if (!Array.isArray(node?.children)) return;
|
|
@@ -20,7 +20,7 @@ export async function buildReactMeasurementCss(root, config, workspace) {
|
|
|
20
20
|
parts.push("\n/* === public/openpress/chapter-scoped.css === */\n");
|
|
21
21
|
parts.push(chapterCss);
|
|
22
22
|
}
|
|
23
|
-
return rewriteAssetUrls(parts.join("\n"), config);
|
|
23
|
+
return rewriteAssetUrls(stripViewportMediaQueries(parts.join("\n")), config);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async function appendOptionalFile(parts, filePath, label) {
|
|
@@ -42,3 +42,95 @@ function rewriteAssetUrls(css, config) {
|
|
|
42
42
|
.replace(/url\((["'])?\/openpress\/fonts\//g, `url($1${themeFontsDir}`)
|
|
43
43
|
.replace(/url\((["'])?\/openpress\/katex-fonts\//g, `url($1${katexFontsDir}`);
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
function stripViewportMediaQueries(css) {
|
|
47
|
+
let output = "";
|
|
48
|
+
let cursor = 0;
|
|
49
|
+
|
|
50
|
+
while (cursor < css.length) {
|
|
51
|
+
const mediaIndex = css.indexOf("@media", cursor);
|
|
52
|
+
if (mediaIndex < 0) {
|
|
53
|
+
output += css.slice(cursor);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
output += css.slice(cursor, mediaIndex);
|
|
58
|
+
const blockStart = css.indexOf("{", mediaIndex);
|
|
59
|
+
if (blockStart < 0) {
|
|
60
|
+
output += css.slice(mediaIndex);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const prelude = css.slice(mediaIndex + "@media".length, blockStart);
|
|
65
|
+
const blockEnd = findCssBlockEnd(css, blockStart);
|
|
66
|
+
if (blockEnd < 0) {
|
|
67
|
+
output += css.slice(mediaIndex);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isViewportMediaPrelude(prelude)) {
|
|
72
|
+
output += css.slice(mediaIndex, blockEnd + 1);
|
|
73
|
+
}
|
|
74
|
+
cursor = blockEnd + 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isViewportMediaPrelude(prelude) {
|
|
81
|
+
if (/\bprint\b/i.test(prelude)) return false;
|
|
82
|
+
return /\(\s*(?:min-|max-)?(?:device-)?(?:width|height)\s*:/i.test(prelude)
|
|
83
|
+
|| /\(\s*orientation\s*:/i.test(prelude)
|
|
84
|
+
|| /\(\s*(?:min-|max-)?aspect-ratio\s*:/i.test(prelude);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findCssBlockEnd(css, blockStart) {
|
|
88
|
+
let depth = 0;
|
|
89
|
+
let quote = "";
|
|
90
|
+
let inComment = false;
|
|
91
|
+
|
|
92
|
+
for (let index = blockStart; index < css.length; index += 1) {
|
|
93
|
+
const current = css[index];
|
|
94
|
+
const next = css[index + 1];
|
|
95
|
+
|
|
96
|
+
if (inComment) {
|
|
97
|
+
if (current === "*" && next === "/") {
|
|
98
|
+
inComment = false;
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (quote) {
|
|
105
|
+
if (current === "\\") {
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (current === quote) quote = "";
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (current === "/" && next === "*") {
|
|
114
|
+
inComment = true;
|
|
115
|
+
index += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (current === "\"" || current === "'") {
|
|
120
|
+
quote = current;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (current === "{") {
|
|
125
|
+
depth += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (current === "}") {
|
|
130
|
+
depth -= 1;
|
|
131
|
+
if (depth === 0) return index;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return -1;
|
|
136
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export function createObjectEntityId(kind, ...parts) {
|
|
2
|
+
return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function createBlockObjectEntityId(blockId) {
|
|
6
|
+
return createObjectEntityId("mdx-block", blockId);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createPageObjectEntityId(frameKey) {
|
|
10
|
+
return createObjectEntityId("page", frameKey);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createFrameObjectEntityId(frameKey) {
|
|
14
|
+
return createObjectEntityId("frame", frameKey);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createMdxAreaObjectEntityId(frameKey, chainId, indexInFrame) {
|
|
18
|
+
return createObjectEntityId("mdx-area", frameKey, chainId, indexInFrame);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildObjectEntities({ frames, blocks, blockMap }) {
|
|
22
|
+
const entities = {};
|
|
23
|
+
const blockParentIdByBlockId = new Map();
|
|
24
|
+
|
|
25
|
+
for (const block of blocks) {
|
|
26
|
+
const frameKey = block.frameKey ?? block.id;
|
|
27
|
+
const sourcePath = block.source?.path;
|
|
28
|
+
const pageId = createPageObjectEntityId(frameKey);
|
|
29
|
+
const base = {
|
|
30
|
+
frameKey,
|
|
31
|
+
pageId,
|
|
32
|
+
source: sourcePath
|
|
33
|
+
? {
|
|
34
|
+
path: sourcePath,
|
|
35
|
+
file: block.source?.file,
|
|
36
|
+
line: 1,
|
|
37
|
+
column: 1,
|
|
38
|
+
}
|
|
39
|
+
: undefined,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
entities[pageId] = {
|
|
43
|
+
id: pageId,
|
|
44
|
+
kind: "page",
|
|
45
|
+
label: block.title || `Page ${block.pageNumber}`,
|
|
46
|
+
...base,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const frameId = createFrameObjectEntityId(frameKey);
|
|
50
|
+
entities[frameId] = {
|
|
51
|
+
id: frameId,
|
|
52
|
+
kind: "frame",
|
|
53
|
+
label: block.role || block.title || frameKey,
|
|
54
|
+
...base,
|
|
55
|
+
parentId: pageId,
|
|
56
|
+
metadata: { role: block.role ?? null, chrome: block.chrome ?? null },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const frame of frames) {
|
|
61
|
+
const pageId = createPageObjectEntityId(frame.frameKey);
|
|
62
|
+
const frameId = createFrameObjectEntityId(frame.frameKey);
|
|
63
|
+
for (const area of frame.mdxAreas ?? []) {
|
|
64
|
+
const id = createMdxAreaObjectEntityId(frame.frameKey, area.chainId, area.indexInFrame);
|
|
65
|
+
const firstEditableBlock = (area.blockIds ?? [])
|
|
66
|
+
.map((blockId) => blockMap[blockId])
|
|
67
|
+
.find((block) => block?.path);
|
|
68
|
+
for (const blockId of area.blockIds ?? []) blockParentIdByBlockId.set(blockId, id);
|
|
69
|
+
entities[id] = {
|
|
70
|
+
id,
|
|
71
|
+
kind: "mdx-area",
|
|
72
|
+
label: `${area.chainId} area ${area.indexInFrame + 1}`,
|
|
73
|
+
parentId: frameId,
|
|
74
|
+
pageId,
|
|
75
|
+
frameKey: frame.frameKey,
|
|
76
|
+
chainId: area.chainId,
|
|
77
|
+
source: firstEditableBlock
|
|
78
|
+
? {
|
|
79
|
+
path: firstEditableBlock.path,
|
|
80
|
+
source: firstEditableBlock.source,
|
|
81
|
+
line: firstEditableBlock.source?.line,
|
|
82
|
+
column: firstEditableBlock.source?.column,
|
|
83
|
+
}
|
|
84
|
+
: undefined,
|
|
85
|
+
metadata: { blockCount: area.blockIds?.length ?? 0 },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const block of Object.values(blockMap)) {
|
|
91
|
+
if (!block?.id) continue;
|
|
92
|
+
const id = createBlockObjectEntityId(block.id);
|
|
93
|
+
const pageId = block.frameKey ? createPageObjectEntityId(block.frameKey) : undefined;
|
|
94
|
+
entities[id] = {
|
|
95
|
+
id,
|
|
96
|
+
kind: "mdx-block",
|
|
97
|
+
label: block.name ? `${block.name} ${block.id}` : block.id,
|
|
98
|
+
parentId: blockParentIdByBlockId.get(block.id),
|
|
99
|
+
pageId,
|
|
100
|
+
blockId: block.id,
|
|
101
|
+
frameKey: block.frameKey,
|
|
102
|
+
chainId: block.chainId,
|
|
103
|
+
source: block.path
|
|
104
|
+
? {
|
|
105
|
+
path: block.path,
|
|
106
|
+
source: block.source,
|
|
107
|
+
line: block.source?.line,
|
|
108
|
+
column: block.source?.column,
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
111
|
+
metadata: {
|
|
112
|
+
blockKind: block.kind ?? null,
|
|
113
|
+
componentName: block.kind === "component" ? block.name ?? null : null,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return entities;
|
|
119
|
+
}
|
|
@@ -179,7 +179,8 @@ function greedyAllocate(blocks, regions) {
|
|
|
179
179
|
|
|
180
180
|
function shouldKeepWithNext(block, nextBlock) {
|
|
181
181
|
if (!nextBlock) return false;
|
|
182
|
-
|
|
182
|
+
const name = String(block?.name ?? "");
|
|
183
|
+
return /^h[1-6]$/.test(name) || name === "caption";
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
function recordAllocation(allocation, result, regions) {
|
|
@@ -223,12 +224,14 @@ function groupBlockHeights(blockHeights) {
|
|
|
223
224
|
|
|
224
225
|
function buildBlockStream(chainSource, heightMap) {
|
|
225
226
|
if (!chainSource || !heightMap) return [];
|
|
226
|
-
return chainSource
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
227
|
+
return chainSource
|
|
228
|
+
.filter((block) => block.layout !== "attached")
|
|
229
|
+
.map((block) => ({
|
|
230
|
+
id: block.id,
|
|
231
|
+
kind: block.kind,
|
|
232
|
+
name: block.name,
|
|
233
|
+
height: heightMap.get(block.id) ?? 0,
|
|
234
|
+
}));
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
function* iterateChains(sources) {
|