@open-press/core 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/engine/commands/dev.mjs +2 -2
- package/engine/commands/upgrade.mjs +47 -5
- package/engine/output/chrome-pdf.mjs +18 -3
- package/engine/output/static-server.mjs +39 -0
- package/engine/react/comment-endpoint.mjs +13 -39
- package/engine/react/comment-marker.mjs +30 -6
- package/engine/react/document-entry.mjs +11 -0
- package/engine/react/document-export.mjs +45 -5
- package/engine/react/http-json.mjs +24 -0
- package/engine/react/mdx-compile.mjs +187 -3
- package/engine/react/measurement-css.mjs +93 -1
- package/engine/react/object-entities.mjs +119 -0
- package/engine/react/pipeline/allocate.mjs +10 -7
- package/engine/react/pipeline/frame-measurement.mjs +40 -9
- 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 +12 -14
- package/engine/react/style-discovery.mjs +1 -4
- package/engine/runtime/file-walk.mjs +22 -0
- package/engine/runtime/inspection.mjs +1 -20
- 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 +4 -31
- package/package.json +1 -1
- package/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
- package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
- package/src/openpress/app/index.ts +2 -0
- package/src/openpress/core/Frame.tsx +9 -11
- package/src/openpress/core/FrameContext.tsx +8 -3
- package/src/openpress/core/MdxArea.tsx +11 -12
- package/src/openpress/core/cn.ts +4 -0
- package/src/openpress/core/index.tsx +2 -1
- package/src/openpress/core/primitives.tsx +29 -8
- package/src/openpress/core/types.ts +8 -0
- 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} +42 -0
- package/src/openpress/document-model/index.ts +6 -0
- package/src/openpress/document-model/objectEntityModel.ts +51 -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/manuscript/index.tsx +49 -7
- 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 +10 -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 +407 -0
- package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -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 +5 -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 +248 -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 +76 -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 +523 -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 +243 -55
- package/src/styles/openpress/responsive.css +145 -270
- package/src/styles/openpress/workbench-panels.css +214 -178
- package/src/styles/openpress/workbench.css +986 -451
- package/src/styles/openpress.css +1 -1
- package/vite.config.ts +50 -0
- 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
|
@@ -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) {
|
|
@@ -135,15 +135,30 @@ async function runChromiumMeasurement(html, viewport) {
|
|
|
135
135
|
try {
|
|
136
136
|
const page = await browser.newPage({ viewport });
|
|
137
137
|
await page.setContent(html, { waitUntil: "load" });
|
|
138
|
+
// Match the print-ready settle: fonts first (font metrics affect image
|
|
139
|
+
// alt-text fallback boxes), then await every image's `complete` AND
|
|
140
|
+
// `decode()` so intrinsic sizes are committed before layout, then two
|
|
141
|
+
// animation frames so the chromium layout pass observes the final box
|
|
142
|
+
// model. Without this, `getBoundingClientRect()` on figures that hold
|
|
143
|
+
// images can race the decode and return collapsed heights, causing the
|
|
144
|
+
// allocator to pack too many blocks per page.
|
|
138
145
|
await page.evaluate(async () => {
|
|
139
|
-
await Promise.all(Array.from(document.images).map((img) => {
|
|
140
|
-
if (img.complete) return Promise.resolve();
|
|
141
|
-
return new Promise((resolve) => {
|
|
142
|
-
img.addEventListener("load", resolve, { once: true });
|
|
143
|
-
img.addEventListener("error", resolve, { once: true });
|
|
144
|
-
});
|
|
145
|
-
}));
|
|
146
146
|
if (document.fonts?.ready) await document.fonts.ready;
|
|
147
|
+
await Promise.all(Array.from(document.images).map(async (img) => {
|
|
148
|
+
if (!img.complete) {
|
|
149
|
+
await new Promise((resolve) => {
|
|
150
|
+
const settle = () => {
|
|
151
|
+
img.removeEventListener("load", settle);
|
|
152
|
+
img.removeEventListener("error", settle);
|
|
153
|
+
resolve(undefined);
|
|
154
|
+
};
|
|
155
|
+
img.addEventListener("load", settle, { once: true });
|
|
156
|
+
img.addEventListener("error", settle, { once: true });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
await img.decode?.().catch(() => undefined);
|
|
160
|
+
}));
|
|
161
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
147
162
|
});
|
|
148
163
|
|
|
149
164
|
const mdxAreas = await page.evaluate((safety) => {
|
|
@@ -206,6 +221,8 @@ async function runChromiumMeasurement(html, viewport) {
|
|
|
206
221
|
const parentTop = chain.parentElement?.getBoundingClientRect().top ?? chain.getBoundingClientRect().top;
|
|
207
222
|
let previousBottom = parentTop;
|
|
208
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;
|
|
209
226
|
const rect = el.getBoundingClientRect();
|
|
210
227
|
out.push({
|
|
211
228
|
id: el.getAttribute("data-openpress-block-id"),
|
|
@@ -232,13 +249,27 @@ async function inlineMeasurementMediaUrls(html, mediaDir) {
|
|
|
232
249
|
if (!mediaDir || !html) return html;
|
|
233
250
|
let out = String(html);
|
|
234
251
|
const matches = new Set();
|
|
235
|
-
for (const match of out.matchAll(
|
|
236
|
-
|
|
252
|
+
for (const match of out.matchAll(/\bsrc=(['"])([^\1]*?)\1/g)) {
|
|
253
|
+
const src = match[2];
|
|
254
|
+
if (!src) continue;
|
|
255
|
+
if (src.startsWith('/openpress/media/')) {
|
|
256
|
+
matches.add(src.slice('/openpress/media/'.length));
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (src.startsWith('media/')) {
|
|
260
|
+
matches.add(src.slice('media/'.length));
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (src.startsWith('./media/')) {
|
|
264
|
+
matches.add(src.slice('./media/'.length));
|
|
265
|
+
}
|
|
237
266
|
}
|
|
238
267
|
for (const rawName of matches) {
|
|
239
268
|
const dataUrl = await mediaDataUrl(mediaDir, rawName);
|
|
240
269
|
if (!dataUrl) continue;
|
|
241
270
|
out = out.replaceAll(`/openpress/media/${rawName}`, dataUrl);
|
|
271
|
+
out = out.replaceAll(`media/${rawName}`, dataUrl);
|
|
272
|
+
out = out.replaceAll(`./media/${rawName}`, dataUrl);
|
|
242
273
|
}
|
|
243
274
|
return out;
|
|
244
275
|
}
|
|
@@ -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
|
-
}
|