@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
|
@@ -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
|
-
}
|
|
@@ -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);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function walkFiles(directory, visit) {
|
|
5
|
+
let entries;
|
|
6
|
+
try {
|
|
7
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error?.code === "ENOENT") return;
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (entry.name.startsWith(".")) continue;
|
|
15
|
+
const absolutePath = path.join(directory, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
await walkFiles(absolutePath, visit);
|
|
18
|
+
} else if (entry.isFile()) {
|
|
19
|
+
await visit(absolutePath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { evaluateUrlWithChrome, stopChildProcess } from "../output/chrome-pdf.mjs";
|
|
4
4
|
import { buildReactStatic, startStaticServer } from "../commands/_shared.mjs";
|
|
5
|
+
import { walkFiles } from "./file-walk.mjs";
|
|
5
6
|
import { createIssue, createIssueReport } from "./issue-report.mjs";
|
|
6
7
|
import { collectActiveContentFiles, resolveActiveSourceWorkspace } from "./source-workspace.mjs";
|
|
7
8
|
|
|
@@ -217,26 +218,6 @@ async function readMediaFiles(directory) {
|
|
|
217
218
|
return files;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
async function walkFiles(directory, visit) {
|
|
221
|
-
let entries;
|
|
222
|
-
try {
|
|
223
|
-
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
224
|
-
} catch (error) {
|
|
225
|
-
if (error?.code === "ENOENT") return;
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
for (const entry of entries) {
|
|
230
|
-
if (entry.name.startsWith(".")) continue;
|
|
231
|
-
const absolutePath = path.join(directory, entry.name);
|
|
232
|
-
if (entry.isDirectory()) {
|
|
233
|
-
await walkFiles(absolutePath, visit);
|
|
234
|
-
} else if (entry.isFile()) {
|
|
235
|
-
await visit(absolutePath);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
221
|
function summarizeComponentUsage(contentFiles) {
|
|
241
222
|
const usages = new Map();
|
|
242
223
|
for (const file of contentFiles) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function rootRelativePath(config, absolutePath) {
|
|
4
|
+
return path.relative(config.root, absolutePath).replaceAll("\\", "/");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function documentRelativePath(absolutePath, documentRoot) {
|
|
8
|
+
return path.relative(documentRoot, absolutePath).split(path.sep).join("/");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveDocumentRelativePath(documentRoot, rel, label) {
|
|
12
|
+
if (typeof rel !== "string" || !rel.trim()) throw new Error(`${label} must be a non-empty document-relative path.`);
|
|
13
|
+
if (rel.includes("..")) throw new Error(`${label} contains "..", rejected.`);
|
|
14
|
+
const absolutePath = path.resolve(documentRoot, rel);
|
|
15
|
+
const relCheck = path.relative(documentRoot, absolutePath);
|
|
16
|
+
if (relCheck.startsWith("..") || path.isAbsolute(relCheck)) {
|
|
17
|
+
throw new Error(`${label} escapes the document root.`);
|
|
18
|
+
}
|
|
19
|
+
return absolutePath;
|
|
20
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
export type SourceTextScope = "content" | "all";
|
|
4
|
+
|
|
5
|
+
export type SourceTextMatch = {
|
|
6
|
+
id: string;
|
|
7
|
+
scope: string;
|
|
8
|
+
file: string;
|
|
9
|
+
path: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
index: number;
|
|
13
|
+
text: string;
|
|
14
|
+
preview: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SourceTextFileSummary = {
|
|
18
|
+
scope: string;
|
|
19
|
+
file: string;
|
|
20
|
+
path: string;
|
|
21
|
+
matchCount: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SourceSearchReport = {
|
|
25
|
+
kind: "search";
|
|
26
|
+
query: string;
|
|
27
|
+
scope: SourceTextScope;
|
|
28
|
+
caseSensitive: boolean;
|
|
29
|
+
matchCount: number;
|
|
30
|
+
files: Array<SourceTextFileSummary>;
|
|
31
|
+
matches: Array<SourceTextMatch>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type SourceBlockTextEditInput = {
|
|
35
|
+
config: ResolvedConfig;
|
|
36
|
+
path: string;
|
|
37
|
+
source: {
|
|
38
|
+
line: number;
|
|
39
|
+
column?: number;
|
|
40
|
+
endLine?: number;
|
|
41
|
+
endColumn?: number;
|
|
42
|
+
};
|
|
43
|
+
text: string;
|
|
44
|
+
kind?: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
blockId?: string;
|
|
47
|
+
cellIndex?: number;
|
|
48
|
+
sourceMode?: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type SourceBlockTextEdit = {
|
|
52
|
+
blockId?: string;
|
|
53
|
+
path: string;
|
|
54
|
+
requestedPath: string;
|
|
55
|
+
file: string;
|
|
56
|
+
line: number;
|
|
57
|
+
column: number;
|
|
58
|
+
endLine: number;
|
|
59
|
+
endColumn: number;
|
|
60
|
+
before: string;
|
|
61
|
+
after: string;
|
|
62
|
+
text: string;
|
|
63
|
+
cellIndex?: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function searchSourceText(options: {
|
|
67
|
+
config: ResolvedConfig;
|
|
68
|
+
query: string;
|
|
69
|
+
scope?: SourceTextScope;
|
|
70
|
+
caseSensitive?: boolean;
|
|
71
|
+
}): Promise<SourceSearchReport>;
|
|
72
|
+
|
|
73
|
+
export function applySourceBlockTextEdit(options: SourceBlockTextEditInput): Promise<SourceBlockTextEdit>;
|
|
74
|
+
|
|
75
|
+
export function applySourceBlockTextEditToText(documentText: string, options: Omit<SourceBlockTextEditInput, "config" | "path">): {
|
|
76
|
+
text: string;
|
|
77
|
+
edit: Omit<SourceBlockTextEdit, "path" | "requestedPath" | "file">;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function readSourceBlockText(options: Pick<SourceBlockTextEditInput, "config" | "path" | "source">): Promise<{
|
|
81
|
+
path: string;
|
|
82
|
+
requestedPath: string;
|
|
83
|
+
file: string;
|
|
84
|
+
text: string;
|
|
85
|
+
}>;
|
|
86
|
+
|
|
87
|
+
export function readSourceBlockTextFromText(documentText: string, options: Pick<SourceBlockTextEditInput, "source">): string;
|
|
88
|
+
|
|
89
|
+
export function applySourceBlockSourceEditToText(documentText: string, options: Pick<SourceBlockTextEditInput, "source" | "text" | "blockId">): {
|
|
90
|
+
text: string;
|
|
91
|
+
edit: Omit<SourceBlockTextEdit, "path" | "requestedPath" | "file">;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function collectSourceTextFiles(config: ResolvedConfig, options?: {
|
|
95
|
+
scope?: SourceTextScope;
|
|
96
|
+
}): Promise<Array<{
|
|
97
|
+
scope: string;
|
|
98
|
+
name: string;
|
|
99
|
+
absolutePath: string;
|
|
100
|
+
relativePath: string;
|
|
101
|
+
text: string;
|
|
102
|
+
}>>;
|