@open-press/cli 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/package.json +1 -1
- package/template/core/CHANGELOG.md +45 -0
- package/template/core/engine/commands/dev.mjs +2 -2
- package/template/core/engine/output/chrome-pdf.mjs +18 -3
- package/template/core/engine/output/static-server.mjs +39 -0
- package/template/core/engine/react/comment-endpoint.mjs +13 -39
- package/template/core/engine/react/comment-marker.mjs +30 -6
- package/template/core/engine/react/document-entry.mjs +11 -0
- package/template/core/engine/react/document-export.mjs +30 -5
- package/template/core/engine/react/http-json.mjs +24 -0
- package/template/core/engine/react/mdx-compile.mjs +96 -3
- package/template/core/engine/react/measurement-css.mjs +93 -1
- package/template/core/engine/react/object-entities.mjs +119 -0
- package/template/core/engine/react/pipeline/allocate.mjs +10 -7
- package/template/core/engine/react/pipeline/frame-measurement.mjs +2 -0
- package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
- package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
- package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
- package/template/core/engine/react/sources/mdx-resolver.mjs +12 -14
- package/template/core/engine/react/style-discovery.mjs +1 -4
- package/template/core/engine/runtime/file-walk.mjs +22 -0
- package/template/core/engine/runtime/inspection.mjs +1 -20
- package/template/core/engine/runtime/path-utils.mjs +20 -0
- package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
- package/template/core/engine/runtime/source-text-tools.mjs +551 -16
- package/template/core/engine/runtime/source-workspace.mjs +4 -31
- package/template/core/package.json +1 -1
- package/template/core/src/main.tsx +2 -2
- package/template/core/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
- package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
- package/template/core/src/openpress/app/index.ts +2 -0
- package/template/core/src/openpress/core/Frame.tsx +9 -11
- package/template/core/src/openpress/core/FrameContext.tsx +8 -3
- package/template/core/src/openpress/core/MdxArea.tsx +11 -12
- package/template/core/src/openpress/core/cn.ts +4 -0
- package/template/core/src/openpress/core/index.tsx +2 -1
- package/template/core/src/openpress/core/primitives.tsx +29 -8
- package/template/core/src/openpress/core/types.ts +8 -0
- package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
- package/template/core/src/openpress/document-model/index.ts +6 -0
- package/template/core/src/openpress/document-model/objectEntityModel.ts +51 -0
- package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/template/core/src/openpress/manuscript/index.tsx +49 -7
- package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/template/core/src/openpress/reader/index.ts +10 -0
- package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/template/core/src/openpress/reader/readerTypes.ts +4 -0
- package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/template/core/src/openpress/reader/usePanelState.ts +56 -0
- package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/template/core/src/openpress/shared/Panel.tsx +77 -0
- package/template/core/src/openpress/shared/index.ts +4 -0
- package/template/core/src/openpress/shared/numberUtils.ts +3 -0
- package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/template/core/src/openpress/workbench/Workbench.tsx +407 -0
- package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/template/core/src/openpress/workbench/actions/index.ts +5 -0
- package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
- package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/template/core/src/openpress/workbench/document/index.ts +10 -0
- package/template/core/src/openpress/workbench/index.ts +2 -0
- package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
- package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
- package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
- package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
- package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/template/core/src/openpress/workbench/panels/index.ts +3 -0
- package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
- package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/template/core/src/openpress/workbench/project/index.ts +2 -0
- package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/template/core/src/openpress/workbench/shell/index.ts +1 -0
- package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/template/core/src/styles/openpress/print-route.css +0 -2
- package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/template/core/src/styles/openpress/public-viewer.css +25 -320
- package/template/core/src/styles/openpress/reader-runtime.css +243 -55
- package/template/core/src/styles/openpress/responsive.css +145 -270
- package/template/core/src/styles/openpress/workbench-panels.css +214 -178
- package/template/core/src/styles/openpress/workbench.css +986 -451
- package/template/core/src/styles/openpress.css +1 -1
- package/template/core/vite.config.ts +50 -0
- package/template/core/src/openpress/inspector.ts +0 -282
- package/template/core/src/openpress/projectWorkspace.tsx +0 -919
- package/template/core/src/openpress/readerRuntime.ts +0 -230
- package/template/core/src/openpress/workbench.tsx +0 -1265
- package/template/core/src/openpress/workbenchTypes.ts +0 -4
- /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /package/template/core/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
|
@@ -112,6 +112,7 @@ export function rehypeBlockIds(options = {}) {
|
|
|
112
112
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
113
113
|
|
|
114
114
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
115
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
115
116
|
const extraAttributes = blockAttributes.get(id);
|
|
116
117
|
if (extraAttributes) {
|
|
117
118
|
for (const [name, value] of Object.entries(extraAttributes)) {
|
|
@@ -142,9 +143,19 @@ function applyTableRowBlocks({
|
|
|
142
143
|
includeBlockIds,
|
|
143
144
|
}) {
|
|
144
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));
|
|
145
155
|
if (rows.length === 0) {
|
|
146
156
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
147
157
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
158
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
148
159
|
blocks.push({
|
|
149
160
|
id,
|
|
150
161
|
kind: "element",
|
|
@@ -165,15 +176,56 @@ function applyTableRowBlocks({
|
|
|
165
176
|
const selected = includeBlockIds
|
|
166
177
|
? rowRecords.filter((row) => includeBlockIds.has(row.id))
|
|
167
178
|
: rowRecords;
|
|
168
|
-
if (selected.length === 0) return false;
|
|
179
|
+
if (selected.length === 0 && !selectedCaption && !selectedHeader) return false;
|
|
169
180
|
|
|
170
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
|
+
}
|
|
171
222
|
const selectedNodes = new Set(selected.map((row) => row.node));
|
|
172
223
|
pruneUnselectedTableRows(node, new Set(rowRecords.map((row) => row.node)), selectedNodes);
|
|
173
|
-
if (
|
|
224
|
+
if (!renderHeader) stripTableHeader(node);
|
|
174
225
|
|
|
175
226
|
for (const row of selected) {
|
|
176
227
|
setDataAttribute(row.node, "data-openpress-block-id", row.id);
|
|
228
|
+
setDataAttribute(row.node, "data-openpress-object-id", createBlockObjectEntityId(row.id));
|
|
177
229
|
blocks.push({
|
|
178
230
|
id: row.id,
|
|
179
231
|
kind: "table-row",
|
|
@@ -229,6 +281,7 @@ function normalizeTableCaptions(node) {
|
|
|
229
281
|
type: "element",
|
|
230
282
|
tagName: "caption",
|
|
231
283
|
properties: {},
|
|
284
|
+
position: child.position,
|
|
232
285
|
children: [{ type: "text", value: captionText }],
|
|
233
286
|
});
|
|
234
287
|
}
|
|
@@ -272,8 +325,10 @@ function wrapMdxComponents(components) {
|
|
|
272
325
|
if (typeof Component !== "function") continue;
|
|
273
326
|
wrapped[name] = function ComponentBlock(props = {}) {
|
|
274
327
|
const blockId = props["data-openpress-block-id"];
|
|
328
|
+
const objectId = props["data-openpress-object-id"] || (blockId ? createBlockObjectEntityId(blockId) : undefined);
|
|
275
329
|
const rest = { ...props };
|
|
276
330
|
delete rest["data-openpress-block-id"];
|
|
331
|
+
delete rest["data-openpress-object-id"];
|
|
277
332
|
|
|
278
333
|
if (!blockId) return React.createElement(Component, rest);
|
|
279
334
|
|
|
@@ -281,6 +336,7 @@ function wrapMdxComponents(components) {
|
|
|
281
336
|
"div",
|
|
282
337
|
{
|
|
283
338
|
"data-openpress-block-id": blockId,
|
|
339
|
+
"data-openpress-object-id": objectId,
|
|
284
340
|
"data-openpress-component-block": name,
|
|
285
341
|
},
|
|
286
342
|
React.createElement(Component, rest),
|
|
@@ -336,6 +392,7 @@ function applyListItemBlocks({
|
|
|
336
392
|
if (items.length === 0) {
|
|
337
393
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
338
394
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
395
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
339
396
|
blocks.push({
|
|
340
397
|
id,
|
|
341
398
|
kind: "element",
|
|
@@ -375,6 +432,7 @@ function applyListItemBlocks({
|
|
|
375
432
|
|
|
376
433
|
for (const item of selected) {
|
|
377
434
|
setDataAttribute(item.node, "data-openpress-block-id", item.id);
|
|
435
|
+
setDataAttribute(item.node, "data-openpress-object-id", createBlockObjectEntityId(item.id));
|
|
378
436
|
blocks.push({
|
|
379
437
|
id: item.id,
|
|
380
438
|
kind: "list-item",
|
|
@@ -419,6 +477,28 @@ function tableBodyRows(table) {
|
|
|
419
477
|
return (table.children ?? []).filter((child) => child?.type === "element" && child.tagName === "tr");
|
|
420
478
|
}
|
|
421
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
|
+
|
|
422
502
|
function pruneUnselectedTableRows(node, rowNodes, selectedNodes) {
|
|
423
503
|
if (!Array.isArray(node?.children)) return;
|
|
424
504
|
node.children = node.children.filter((child) => {
|
|
@@ -432,10 +512,15 @@ function stripTableHeader(table) {
|
|
|
432
512
|
if (!Array.isArray(table?.children)) return;
|
|
433
513
|
table.children = table.children.filter((child) => {
|
|
434
514
|
if (child?.type !== "element") return true;
|
|
435
|
-
return child.tagName !== "
|
|
515
|
+
return child.tagName !== "thead";
|
|
436
516
|
});
|
|
437
517
|
}
|
|
438
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
|
+
|
|
439
524
|
function headingText(node) {
|
|
440
525
|
if (!/^h[1-6]$/.test(String(node?.tagName ?? ""))) return undefined;
|
|
441
526
|
return textContent(node).trim() || undefined;
|
|
@@ -470,6 +555,14 @@ function setDataAttribute(node, name, value) {
|
|
|
470
555
|
node.properties[name] = value;
|
|
471
556
|
}
|
|
472
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
|
+
|
|
473
566
|
function visit(node, visitor) {
|
|
474
567
|
visitor(node);
|
|
475
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) {
|
|
@@ -221,6 +221,8 @@ async function runChromiumMeasurement(html, viewport) {
|
|
|
221
221
|
const parentTop = chain.parentElement?.getBoundingClientRect().top ?? chain.getBoundingClientRect().top;
|
|
222
222
|
let previousBottom = parentTop;
|
|
223
223
|
for (const el of Array.from(chain.querySelectorAll("[data-openpress-block-id]"))) {
|
|
224
|
+
if (el.tagName.toLowerCase() === "caption") continue;
|
|
225
|
+
if (el.getAttribute("data-openpress-block-layout") === "attached") continue;
|
|
224
226
|
const rect = el.getBoundingClientRect();
|
|
225
227
|
out.push({
|
|
226
228
|
id: el.getAttribute("data-openpress-block-id"),
|
|
@@ -3,8 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { loadConfig } from "../runtime/config.mjs";
|
|
4
4
|
import { collectSourceTextFiles } from "../runtime/source-text-tools.mjs";
|
|
5
5
|
import { insertCommentMarker } from "./comment-marker.mjs";
|
|
6
|
-
|
|
7
|
-
const MAX_PROJECT_ASSET_BODY_BYTES = 64 * 1024;
|
|
6
|
+
import { readJsonBody, writeJson } from "./http-json.mjs";
|
|
8
7
|
|
|
9
8
|
export async function handleProjectAssetRequest(req, res, {
|
|
10
9
|
root = ".",
|
|
@@ -16,7 +15,7 @@ export async function handleProjectAssetRequest(req, res, {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
try {
|
|
19
|
-
const body = await readJsonBody(req);
|
|
18
|
+
const body = await readJsonBody(req, { bodyLabel: "Project asset request" });
|
|
20
19
|
const config = await loadConfig(root);
|
|
21
20
|
const action = stringValue(body?.action);
|
|
22
21
|
const kind = stringValue(body?.kind);
|
|
@@ -53,6 +52,7 @@ export async function handleProjectAssetRequest(req, res, {
|
|
|
53
52
|
note: body?.note,
|
|
54
53
|
commentTarget: body?.commentTarget,
|
|
55
54
|
currentSource: body?.currentSource,
|
|
55
|
+
objectEntity: body?.objectEntity,
|
|
56
56
|
timestamp,
|
|
57
57
|
});
|
|
58
58
|
writeJson(res, 200, { ok: true, ...result });
|
|
@@ -133,6 +133,7 @@ async function createProjectAssetComment({
|
|
|
133
133
|
note,
|
|
134
134
|
commentTarget,
|
|
135
135
|
currentSource,
|
|
136
|
+
objectEntity,
|
|
136
137
|
timestamp,
|
|
137
138
|
}) {
|
|
138
139
|
const normalizedName = normalizeAssetName(kind, name);
|
|
@@ -146,13 +147,14 @@ async function createProjectAssetComment({
|
|
|
146
147
|
commentTarget: stringValue(commentTarget),
|
|
147
148
|
currentSource,
|
|
148
149
|
});
|
|
150
|
+
const objectHint = stringValue(objectEntity?.id) ? ` object=${stringValue(objectEntity.id)}` : "";
|
|
149
151
|
|
|
150
152
|
const result = await insertCommentMarker({
|
|
151
153
|
root: config.root,
|
|
152
154
|
path: target.path,
|
|
153
155
|
source: { line: target.line, column: 1 },
|
|
154
156
|
note: `${assetLabel(kind, normalizedName)}:${noteText}`,
|
|
155
|
-
hint: `openpress-project-asset kind=${kind} action=comment target=${target.reason} asset=${normalizedName}`,
|
|
157
|
+
hint: `openpress-project-asset kind=${kind} action=comment target=${target.reason} asset=${normalizedName}${objectHint}`,
|
|
156
158
|
timestamp,
|
|
157
159
|
});
|
|
158
160
|
|
|
@@ -357,23 +359,3 @@ async function fileExists(filePath) {
|
|
|
357
359
|
return false;
|
|
358
360
|
}
|
|
359
361
|
}
|
|
360
|
-
|
|
361
|
-
async function readJsonBody(req) {
|
|
362
|
-
let body = "";
|
|
363
|
-
for await (const chunk of req) {
|
|
364
|
-
body += String(chunk);
|
|
365
|
-
if (Buffer.byteLength(body, "utf8") > MAX_PROJECT_ASSET_BODY_BYTES) {
|
|
366
|
-
throw new Error("Project asset request body is too large.");
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
try {
|
|
370
|
-
return JSON.parse(body || "{}");
|
|
371
|
-
} catch {
|
|
372
|
-
throw new Error("Project asset request body must be valid JSON.");
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function writeJson(res, status, body) {
|
|
377
|
-
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
378
|
-
res.end(`${JSON.stringify(body, null, 2)}\n`);
|
|
379
|
-
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { loadConfig } from "../runtime/config.mjs";
|
|
2
|
+
import { applySourceBlockTextEdit, readSourceBlockText } from "../runtime/source-text-tools.mjs";
|
|
3
|
+
import { exportReactDocument } from "./document-export.mjs";
|
|
4
|
+
import { readJsonBody, writeJson } from "./http-json.mjs";
|
|
5
|
+
|
|
6
|
+
export async function handleSourceEditRequest(req, res, {
|
|
7
|
+
root = ".",
|
|
8
|
+
refreshDocument = true,
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (req.method === "GET") {
|
|
11
|
+
try {
|
|
12
|
+
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
13
|
+
const config = await loadConfig(root);
|
|
14
|
+
const sourceText = await readSourceBlockText({
|
|
15
|
+
config,
|
|
16
|
+
path: requestUrl.searchParams.get("path"),
|
|
17
|
+
source: {
|
|
18
|
+
line: Number(requestUrl.searchParams.get("line")),
|
|
19
|
+
column: Number(requestUrl.searchParams.get("column") || 1),
|
|
20
|
+
endLine: Number(requestUrl.searchParams.get("endLine") || requestUrl.searchParams.get("line")),
|
|
21
|
+
endColumn: Number(requestUrl.searchParams.get("endColumn") || requestUrl.searchParams.get("column") || 1),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
writeJson(res, 200, { ok: true, source: sourceText });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
writeJson(res, 400, {
|
|
27
|
+
ok: false,
|
|
28
|
+
message: error instanceof Error ? error.message : String(error),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (req.method !== "POST") {
|
|
35
|
+
writeJson(res, 405, { ok: false, message: "OpenPress source edit endpoint requires GET or POST." });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const body = await readJsonBody(req, {
|
|
41
|
+
bodyLabel: "OpenPress source edit request",
|
|
42
|
+
maxBytes: 256 * 1024,
|
|
43
|
+
});
|
|
44
|
+
const config = await loadConfig(root);
|
|
45
|
+
const edit = await applySourceBlockTextEdit({
|
|
46
|
+
config,
|
|
47
|
+
path: body?.path,
|
|
48
|
+
source: body?.source,
|
|
49
|
+
text: body?.text,
|
|
50
|
+
kind: body?.kind,
|
|
51
|
+
name: body?.name,
|
|
52
|
+
blockId: body?.blockId,
|
|
53
|
+
sourceMode: body?.sourceMode === true,
|
|
54
|
+
});
|
|
55
|
+
const exported = refreshDocument && body?.refreshDocument !== false
|
|
56
|
+
? await exportReactDocument(root, { syncAssets: false })
|
|
57
|
+
: null;
|
|
58
|
+
|
|
59
|
+
writeJson(res, 200, {
|
|
60
|
+
ok: true,
|
|
61
|
+
edit,
|
|
62
|
+
document: exported
|
|
63
|
+
? {
|
|
64
|
+
path: exported.documentPath,
|
|
65
|
+
pageCount: exported.pageCount,
|
|
66
|
+
}
|
|
67
|
+
: undefined,
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
writeJson(res, 400, {
|
|
71
|
+
ok: false,
|
|
72
|
+
message: error instanceof Error ? error.message : String(error),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import fs from "node:fs/promises";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import React from "react";
|
|
13
|
+
import { documentRelativePath, resolveDocumentRelativePath } from "../../runtime/path-utils.mjs";
|
|
13
14
|
import { compileMdx } from "../mdx-compile.mjs";
|
|
14
15
|
import { createHeadingState, fallbackOutlineItems, headingAttributesForBlock } from "./heading-numbering.mjs";
|
|
15
16
|
|
|
@@ -103,6 +104,7 @@ async function resolveSource({ sourceId, descriptor, documentRoot, globalCompone
|
|
|
103
104
|
kind: block.kind,
|
|
104
105
|
name: block.name,
|
|
105
106
|
text: block.text,
|
|
107
|
+
layout: block.layout,
|
|
106
108
|
chainId,
|
|
107
109
|
sectionSlug: section.slug,
|
|
108
110
|
path: documentRelative(file.absolutePath, documentRoot),
|
|
@@ -323,6 +325,7 @@ function compileTocBlocks({ tocBlocks, chainId, blockIds, toc }) {
|
|
|
323
325
|
{
|
|
324
326
|
className,
|
|
325
327
|
"data-openpress-block-id": block.id,
|
|
328
|
+
"data-openpress-object-id": createBlockObjectEntityId(block.id),
|
|
326
329
|
"data-openpress-toc-entry": block.sectionSlug,
|
|
327
330
|
},
|
|
328
331
|
React.createElement(
|
|
@@ -352,6 +355,14 @@ function locateSection(renderData, chainId) {
|
|
|
352
355
|
throw new Error(`No section found for chainId "${chainId}" in source "${renderData.sourceId}".`);
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
function createObjectEntityId(kind, ...parts) {
|
|
359
|
+
return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function createBlockObjectEntityId(blockId) {
|
|
363
|
+
return createObjectEntityId("mdx-block", blockId);
|
|
364
|
+
}
|
|
365
|
+
|
|
355
366
|
// ---------------------------------------------------------------------------
|
|
356
367
|
// Validation
|
|
357
368
|
// ---------------------------------------------------------------------------
|
|
@@ -425,17 +436,4 @@ function deriveTitleFromDirName(name) {
|
|
|
425
436
|
.join(" ");
|
|
426
437
|
}
|
|
427
438
|
|
|
428
|
-
|
|
429
|
-
return path.relative(documentRoot, absolutePath).split(path.sep).join("/");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function resolveDocumentRelativePath(documentRoot, rel, label) {
|
|
433
|
-
if (typeof rel !== "string" || !rel.trim()) throw new Error(`${label} must be a non-empty document-relative path.`);
|
|
434
|
-
if (rel.includes("..")) throw new Error(`${label} contains "..", rejected.`);
|
|
435
|
-
const absolutePath = path.resolve(documentRoot, rel);
|
|
436
|
-
const relCheck = path.relative(documentRoot, absolutePath);
|
|
437
|
-
if (relCheck.startsWith("..") || path.isAbsolute(relCheck)) {
|
|
438
|
-
throw new Error(`${label} escapes the document root.`);
|
|
439
|
-
}
|
|
440
|
-
return absolutePath;
|
|
441
|
-
}
|
|
439
|
+
const documentRelative = documentRelativePath;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { documentRelativePath } from "../runtime/path-utils.mjs";
|
|
3
4
|
|
|
4
5
|
// Style discovery — only used to find per-section CSS files for the
|
|
5
6
|
// section-folders preset. MDX content discovery lives in `sources/mdx-resolver`.
|
|
@@ -102,10 +103,6 @@ function pathRecord(absolutePath, documentRoot) {
|
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
function documentRelativePath(absolutePath, documentRoot) {
|
|
106
|
-
return path.relative(documentRoot, absolutePath).split(path.sep).join("/");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
106
|
function compareSectionDirectories(a, b) {
|
|
110
107
|
const left = sectionSortKey(a.name);
|
|
111
108
|
const right = sectionSortKey(b.name);
|