@open-press/core 0.7.1 → 1.0.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 -3
- package/engine/cli.mjs +8 -8
- package/engine/commands/_shared.mjs +37 -15
- package/engine/commands/dev.mjs +2 -2
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +63 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +110 -3
- package/engine/output/static-server.mjs +87 -9
- package/engine/react/comment-endpoint.mjs +13 -39
- package/engine/react/comment-marker.mjs +43 -19
- package/engine/react/document-entry.mjs +46 -28
- package/engine/react/document-export.mjs +328 -164
- package/engine/react/http-json.mjs +24 -0
- package/engine/react/mdx-compile.mjs +126 -3
- package/engine/react/measurement-css.mjs +114 -1
- package/engine/react/object-entities.mjs +204 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +41 -72
- package/engine/react/pipeline/frame-measurement.mjs +6 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- 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 +13 -15
- package/engine/react/style-discovery.mjs +23 -8
- package/engine/runtime/config.d.mts +8 -0
- package/engine/runtime/config.mjs +57 -60
- package/engine/runtime/file-utils.mjs +9 -1
- package/engine/runtime/file-walk.mjs +22 -0
- package/engine/runtime/inspection.mjs +1 -20
- package/engine/runtime/page-geometry.mjs +131 -0
- 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 +16 -34
- package/engine/runtime/validation.mjs +19 -10
- package/package.json +3 -5
- package/src/openpress/app/OpenPressApp.tsx +296 -0
- package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/app/index.ts +2 -0
- package/src/openpress/core/Frame.tsx +26 -15
- package/src/openpress/core/FrameContext.tsx +10 -3
- package/src/openpress/core/MdxArea.tsx +11 -12
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/cn.ts +4 -0
- package/src/openpress/core/index.tsx +11 -3
- package/src/openpress/core/primitives.tsx +74 -6
- package/src/openpress/core/types.ts +94 -41
- package/src/openpress/core/useSource.ts +1 -1
- 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} +51 -0
- package/src/openpress/document-model/index.ts +7 -0
- package/src/openpress/document-model/objectEntityModel.ts +55 -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/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/manuscript/index.tsx +49 -7
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- 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 +11 -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 +506 -0
- package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -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 +6 -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 +254 -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 +80 -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 +525 -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 +252 -55
- package/src/styles/openpress/responsive.css +145 -270
- package/src/styles/openpress/workbench-panels.css +327 -178
- package/src/styles/openpress/workbench.css +986 -451
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +2 -1
- package/tsconfig.json +1 -1
- package/vite.config.ts +50 -0
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
- package/src/openpress/App.tsx +0 -127
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { walkFiles } from "./file-walk.mjs";
|
|
3
4
|
import { resolveActiveSourceWorkspace } from "./source-workspace.mjs";
|
|
4
5
|
|
|
5
6
|
const MARKDOWN_EXTENSIONS = new Set([".md"]);
|
|
@@ -79,6 +80,327 @@ export async function replaceSourceText({
|
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
export async function applySourceBlockTextEdit({
|
|
84
|
+
config,
|
|
85
|
+
path: sourcePath,
|
|
86
|
+
source,
|
|
87
|
+
text,
|
|
88
|
+
kind,
|
|
89
|
+
name,
|
|
90
|
+
blockId,
|
|
91
|
+
cellIndex,
|
|
92
|
+
sourceMode = false,
|
|
93
|
+
}) {
|
|
94
|
+
const requestedPath = stringValue(sourcePath);
|
|
95
|
+
if (!requestedPath) throw new Error("Source edit requires a source path.");
|
|
96
|
+
const files = await collectSourceTextFiles(config, { scope: "content" });
|
|
97
|
+
const file = files.find((candidate) => sourceTextPathMatches(candidate.relativePath, requestedPath));
|
|
98
|
+
if (!file) throw new Error(`Editable source file not found: ${requestedPath}`);
|
|
99
|
+
|
|
100
|
+
const result = sourceMode
|
|
101
|
+
? applySourceBlockSourceEditToText(file.text, { source, text, blockId })
|
|
102
|
+
: applySourceBlockTextEditToText(file.text, {
|
|
103
|
+
source,
|
|
104
|
+
text,
|
|
105
|
+
kind,
|
|
106
|
+
name,
|
|
107
|
+
blockId,
|
|
108
|
+
cellIndex,
|
|
109
|
+
});
|
|
110
|
+
await fs.writeFile(file.absolutePath, result.text, "utf8");
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...result.edit,
|
|
114
|
+
path: file.relativePath,
|
|
115
|
+
requestedPath,
|
|
116
|
+
file: file.name,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function readSourceBlockText({ config, path: sourcePath, source }) {
|
|
121
|
+
const requestedPath = stringValue(sourcePath);
|
|
122
|
+
if (!requestedPath) throw new Error("Source read requires a source path.");
|
|
123
|
+
const files = await collectSourceTextFiles(config, { scope: "content" });
|
|
124
|
+
const file = files.find((candidate) => sourceTextPathMatches(candidate.relativePath, requestedPath));
|
|
125
|
+
if (!file) throw new Error(`Editable source file not found: ${requestedPath}`);
|
|
126
|
+
return {
|
|
127
|
+
path: file.relativePath,
|
|
128
|
+
requestedPath,
|
|
129
|
+
file: file.name,
|
|
130
|
+
text: readSourceBlockTextFromText(file.text, { source }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function readSourceBlockTextFromText(documentText, { source } = {}) {
|
|
135
|
+
const sourceRange = normalizeSourceRange(source);
|
|
136
|
+
const lines = splitTextLines(documentText);
|
|
137
|
+
const startIndex = sourceRange.line - 1;
|
|
138
|
+
const endIndex = sourceRange.endLine - 1;
|
|
139
|
+
if (!lines[startIndex]) throw new Error(`Source read line ${sourceRange.line} is outside the source file.`);
|
|
140
|
+
if (!lines[endIndex]) throw new Error(`Source read end line ${sourceRange.endLine} is outside the source file.`);
|
|
141
|
+
return lines.slice(startIndex, endIndex + 1).map((line) => line.line).join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function applySourceBlockSourceEditToText(documentText, {
|
|
145
|
+
source,
|
|
146
|
+
text,
|
|
147
|
+
blockId,
|
|
148
|
+
} = {}) {
|
|
149
|
+
const sourceRange = normalizeSourceRange(source);
|
|
150
|
+
const replacementText = normalizeRawSourceText(text);
|
|
151
|
+
const lines = splitTextLines(documentText);
|
|
152
|
+
const startIndex = sourceRange.line - 1;
|
|
153
|
+
const endIndex = sourceRange.endLine - 1;
|
|
154
|
+
if (!lines[startIndex]) throw new Error(`Source edit line ${sourceRange.line} is outside the source file.`);
|
|
155
|
+
if (!lines[endIndex]) throw new Error(`Source edit end line ${sourceRange.endLine} is outside the source file.`);
|
|
156
|
+
|
|
157
|
+
const selectedLines = lines.slice(startIndex, endIndex + 1);
|
|
158
|
+
const replacementLines = replacementText.split("\n");
|
|
159
|
+
const ending = selectedLines[selectedLines.length - 1].ending;
|
|
160
|
+
const nextLines = [
|
|
161
|
+
...lines.slice(0, startIndex),
|
|
162
|
+
...replacementLines.map((line, index) => ({
|
|
163
|
+
line,
|
|
164
|
+
ending: index === replacementLines.length - 1 ? ending : "\n",
|
|
165
|
+
})),
|
|
166
|
+
...lines.slice(endIndex + 1),
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
text: joinTextLines(nextLines),
|
|
171
|
+
edit: {
|
|
172
|
+
blockId,
|
|
173
|
+
line: sourceRange.line,
|
|
174
|
+
column: sourceRange.column,
|
|
175
|
+
endLine: sourceRange.endLine,
|
|
176
|
+
endColumn: sourceRange.endColumn,
|
|
177
|
+
before: selectedLines.map((line) => line.line).join("\n"),
|
|
178
|
+
after: replacementText,
|
|
179
|
+
text: replacementText,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function applySourceBlockTextEditToText(documentText, {
|
|
185
|
+
source,
|
|
186
|
+
text,
|
|
187
|
+
kind,
|
|
188
|
+
name,
|
|
189
|
+
blockId,
|
|
190
|
+
cellIndex,
|
|
191
|
+
} = {}) {
|
|
192
|
+
if (kind === "table-cell") {
|
|
193
|
+
return applySourceBlockTableCellEditToText(documentText, {
|
|
194
|
+
source,
|
|
195
|
+
text,
|
|
196
|
+
blockId,
|
|
197
|
+
cellIndex,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (kind === "component-caption") {
|
|
201
|
+
return applySourceBlockComponentCaptionEditToText(documentText, {
|
|
202
|
+
source,
|
|
203
|
+
text,
|
|
204
|
+
blockId,
|
|
205
|
+
name,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (kind === "element" && name === "pre") {
|
|
209
|
+
return applySourceBlockCodeEditToText(documentText, {
|
|
210
|
+
source,
|
|
211
|
+
text,
|
|
212
|
+
blockId,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (kind === "element" && (name === "caption" || name === "figcaption")) {
|
|
216
|
+
return applySourceBlockCaptionEditToText(documentText, {
|
|
217
|
+
source,
|
|
218
|
+
text,
|
|
219
|
+
blockId,
|
|
220
|
+
name,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
assertEditableSourceBlock({ kind, name, blockId });
|
|
225
|
+
const sourceRange = normalizeSourceRange(source);
|
|
226
|
+
const replacementText = normalizeEditedText(text);
|
|
227
|
+
const lines = splitTextLines(documentText);
|
|
228
|
+
const startIndex = sourceRange.line - 1;
|
|
229
|
+
const endIndex = sourceRange.endLine - 1;
|
|
230
|
+
|
|
231
|
+
if (!lines[startIndex]) {
|
|
232
|
+
throw new Error(`Source edit line ${sourceRange.line} is outside the source file.`);
|
|
233
|
+
}
|
|
234
|
+
if (!lines[endIndex]) {
|
|
235
|
+
throw new Error(`Source edit end line ${sourceRange.endLine} is outside the source file.`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const selectedLines = lines.slice(startIndex, endIndex + 1);
|
|
239
|
+
const firstLine = selectedLines[0].line;
|
|
240
|
+
if (!isEditableMarkdownLine(firstLine, { kind, name })) {
|
|
241
|
+
throw new Error("Only rendered text blocks can be edited from the document surface.");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const prefix = markdownTextPrefix(firstLine, { kind, name });
|
|
245
|
+
const after = `${prefix}${replacementText}`;
|
|
246
|
+
const ending = selectedLines[selectedLines.length - 1].ending;
|
|
247
|
+
const nextLines = [
|
|
248
|
+
...lines.slice(0, startIndex),
|
|
249
|
+
{ line: after, ending },
|
|
250
|
+
...lines.slice(endIndex + 1),
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
text: joinTextLines(nextLines),
|
|
255
|
+
edit: {
|
|
256
|
+
blockId,
|
|
257
|
+
line: sourceRange.line,
|
|
258
|
+
column: sourceRange.column,
|
|
259
|
+
endLine: sourceRange.endLine,
|
|
260
|
+
endColumn: sourceRange.endColumn,
|
|
261
|
+
before: selectedLines.map((line) => line.line).join("\n"),
|
|
262
|
+
after,
|
|
263
|
+
text: replacementText,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function applySourceBlockCodeEditToText(documentText, {
|
|
269
|
+
source,
|
|
270
|
+
text,
|
|
271
|
+
blockId,
|
|
272
|
+
} = {}) {
|
|
273
|
+
const sourceRange = normalizeSourceRange(source);
|
|
274
|
+
const replacementText = normalizeCodeBlockText(text);
|
|
275
|
+
const lines = splitTextLines(documentText);
|
|
276
|
+
const startIndex = sourceRange.line - 1;
|
|
277
|
+
const endIndex = sourceRange.endLine - 1;
|
|
278
|
+
|
|
279
|
+
if (!lines[startIndex]) throw new Error(`Source edit line ${sourceRange.line} is outside the source file.`);
|
|
280
|
+
if (!lines[endIndex]) throw new Error(`Source edit end line ${sourceRange.endLine} is outside the source file.`);
|
|
281
|
+
|
|
282
|
+
const selectedLines = lines.slice(startIndex, endIndex + 1);
|
|
283
|
+
const openingFence = selectedLines[0]?.line ?? "";
|
|
284
|
+
const closingFence = selectedLines.at(-1)?.line ?? "";
|
|
285
|
+
if (selectedLines.length < 2 || !isFenceLine(openingFence) || !isFenceLine(closingFence)) {
|
|
286
|
+
throw new Error("Code block edits require a fenced markdown code block source range.");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const replacementLines = replacementText ? replacementText.split("\n") : [];
|
|
290
|
+
const afterLines = [openingFence, ...replacementLines, closingFence];
|
|
291
|
+
const ending = selectedLines[selectedLines.length - 1].ending;
|
|
292
|
+
const nextLines = [
|
|
293
|
+
...lines.slice(0, startIndex),
|
|
294
|
+
...afterLines.map((line, index) => ({
|
|
295
|
+
line,
|
|
296
|
+
ending: index === afterLines.length - 1 ? ending : "\n",
|
|
297
|
+
})),
|
|
298
|
+
...lines.slice(endIndex + 1),
|
|
299
|
+
];
|
|
300
|
+
const after = afterLines.join("\n");
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
text: joinTextLines(nextLines),
|
|
304
|
+
edit: {
|
|
305
|
+
blockId,
|
|
306
|
+
line: sourceRange.line,
|
|
307
|
+
column: sourceRange.column,
|
|
308
|
+
endLine: sourceRange.endLine,
|
|
309
|
+
endColumn: sourceRange.endColumn,
|
|
310
|
+
before: selectedLines.map((line) => line.line).join("\n"),
|
|
311
|
+
after,
|
|
312
|
+
text: replacementText,
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function applySourceBlockCaptionEditToText(documentText, {
|
|
318
|
+
source,
|
|
319
|
+
text,
|
|
320
|
+
blockId,
|
|
321
|
+
name,
|
|
322
|
+
} = {}) {
|
|
323
|
+
const sourceRange = normalizeSourceRange(source);
|
|
324
|
+
const replacementText = normalizeEditedText(text);
|
|
325
|
+
const lines = splitTextLines(documentText);
|
|
326
|
+
const startIndex = sourceRange.line - 1;
|
|
327
|
+
const endIndex = sourceRange.endLine - 1;
|
|
328
|
+
|
|
329
|
+
if (!lines[startIndex]) throw new Error(`Source edit line ${sourceRange.line} is outside the source file.`);
|
|
330
|
+
if (!lines[endIndex]) throw new Error(`Source edit end line ${sourceRange.endLine} is outside the source file.`);
|
|
331
|
+
|
|
332
|
+
const selectedLines = lines.slice(startIndex, endIndex + 1);
|
|
333
|
+
const before = selectedLines.map((line) => line.line).join("\n");
|
|
334
|
+
const after = replaceCaptionText(before, replacementText, name);
|
|
335
|
+
const replacementLines = after.split("\n");
|
|
336
|
+
const ending = selectedLines[selectedLines.length - 1].ending;
|
|
337
|
+
const nextLines = [
|
|
338
|
+
...lines.slice(0, startIndex),
|
|
339
|
+
...replacementLines.map((line, index) => ({
|
|
340
|
+
line,
|
|
341
|
+
ending: index === replacementLines.length - 1 ? ending : "\n",
|
|
342
|
+
})),
|
|
343
|
+
...lines.slice(endIndex + 1),
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
text: joinTextLines(nextLines),
|
|
348
|
+
edit: {
|
|
349
|
+
blockId,
|
|
350
|
+
line: sourceRange.line,
|
|
351
|
+
column: sourceRange.column,
|
|
352
|
+
endLine: sourceRange.endLine,
|
|
353
|
+
endColumn: sourceRange.endColumn,
|
|
354
|
+
before,
|
|
355
|
+
after,
|
|
356
|
+
text: replacementText,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function applySourceBlockTableCellEditToText(documentText, {
|
|
362
|
+
source,
|
|
363
|
+
text,
|
|
364
|
+
blockId,
|
|
365
|
+
cellIndex,
|
|
366
|
+
} = {}) {
|
|
367
|
+
const sourceRange = normalizeSourceRange(source);
|
|
368
|
+
if (sourceRange.endLine !== sourceRange.line) {
|
|
369
|
+
throw new Error("Table cell edits must target a single markdown table row.");
|
|
370
|
+
}
|
|
371
|
+
const replacementText = normalizeEditedText(text);
|
|
372
|
+
const targetCellIndex = nonNegativeInteger(cellIndex, "cellIndex");
|
|
373
|
+
const lines = splitTextLines(documentText);
|
|
374
|
+
const rowIndex = sourceRange.line - 1;
|
|
375
|
+
const row = lines[rowIndex];
|
|
376
|
+
if (!row) throw new Error(`Source edit line ${sourceRange.line} is outside the source file.`);
|
|
377
|
+
if (!isMarkdownTableRow(row.line)) {
|
|
378
|
+
throw new Error("Table cell edits require a markdown table row source line.");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const after = replaceMarkdownTableCell(row.line, targetCellIndex, replacementText);
|
|
382
|
+
const nextLines = [
|
|
383
|
+
...lines.slice(0, rowIndex),
|
|
384
|
+
{ line: after, ending: row.ending },
|
|
385
|
+
...lines.slice(rowIndex + 1),
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
text: joinTextLines(nextLines),
|
|
390
|
+
edit: {
|
|
391
|
+
blockId,
|
|
392
|
+
line: sourceRange.line,
|
|
393
|
+
column: sourceRange.column,
|
|
394
|
+
endLine: sourceRange.endLine,
|
|
395
|
+
endColumn: sourceRange.endColumn,
|
|
396
|
+
before: row.line,
|
|
397
|
+
after,
|
|
398
|
+
text: replacementText,
|
|
399
|
+
cellIndex: targetCellIndex,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
82
404
|
export async function collectSourceTextFiles(config, { scope = "content" } = {}) {
|
|
83
405
|
const roots = await sourceRoots(config, scope);
|
|
84
406
|
const files = [];
|
|
@@ -214,22 +536,6 @@ function implementationRoots(sourceWorkspace) {
|
|
|
214
536
|
return roots;
|
|
215
537
|
}
|
|
216
538
|
|
|
217
|
-
async function walkFiles(directory, visit) {
|
|
218
|
-
let entries;
|
|
219
|
-
try {
|
|
220
|
-
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (error?.code === "ENOENT") return;
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
225
|
-
for (const entry of entries) {
|
|
226
|
-
if (entry.name.startsWith(".")) continue;
|
|
227
|
-
const absolutePath = path.join(directory, entry.name);
|
|
228
|
-
if (entry.isDirectory()) await walkFiles(absolutePath, visit);
|
|
229
|
-
else if (entry.isFile()) await visit(absolutePath);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
539
|
function forEachLine(text, visit) {
|
|
234
540
|
const lineRe = /([^\r\n]*)(\r\n|\n|\r|$)/g;
|
|
235
541
|
let lineNumber = 1;
|
|
@@ -295,3 +601,232 @@ function summarizeFiles(matches) {
|
|
|
295
601
|
function isFenceLine(line) {
|
|
296
602
|
return /^\s*(```|~~~)/.test(line);
|
|
297
603
|
}
|
|
604
|
+
|
|
605
|
+
function assertEditableSourceBlock({ kind, name, blockId }) {
|
|
606
|
+
if (kind === "list-item") return;
|
|
607
|
+
if (kind === "element" && isEditableElementName(name)) return;
|
|
608
|
+
throw new Error(`Only rendered text blocks can be edited from the document surface${blockId ? `: ${blockId}` : ""}.`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function isEditableElementName(name) {
|
|
612
|
+
return typeof name === "string" && /^(h[1-6]|p|blockquote)$/.test(name);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function normalizeSourceRange(source) {
|
|
616
|
+
const line = positiveInteger(source?.line, "line");
|
|
617
|
+
const column = positiveInteger(source?.column ?? 1, "column");
|
|
618
|
+
const endLine = positiveInteger(source?.endLine ?? line, "endLine");
|
|
619
|
+
const endColumn = positiveInteger(source?.endColumn ?? column, "endColumn");
|
|
620
|
+
if (endLine < line) throw new Error("Source edit endLine must be greater than or equal to line.");
|
|
621
|
+
return { line, column, endLine, endColumn };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function positiveInteger(value, label) {
|
|
625
|
+
const number = Number(value);
|
|
626
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
627
|
+
throw new Error(`Source edit ${label} must be a positive integer.`);
|
|
628
|
+
}
|
|
629
|
+
return number;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function nonNegativeInteger(value, label) {
|
|
633
|
+
const number = Number(value);
|
|
634
|
+
if (!Number.isInteger(number) || number < 0) {
|
|
635
|
+
throw new Error(`Source edit ${label} must be a non-negative integer.`);
|
|
636
|
+
}
|
|
637
|
+
return number;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function normalizeEditedText(value) {
|
|
641
|
+
if (typeof value !== "string") throw new Error("Source edit text must be a string.");
|
|
642
|
+
return value.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function normalizeCodeBlockText(value) {
|
|
646
|
+
if (typeof value !== "string") throw new Error("Source edit text must be a string.");
|
|
647
|
+
return value.replace(/\r\n?/g, "\n").replace(/^\n+|\n+$/g, "");
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function normalizeRawSourceText(value) {
|
|
651
|
+
if (typeof value !== "string") throw new Error("Source edit text must be a string.");
|
|
652
|
+
const normalized = value.replace(/\r\n?/g, "\n").trim();
|
|
653
|
+
if (!normalized) throw new Error("Source edit text must not be empty.");
|
|
654
|
+
return normalized;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function splitTextLines(text) {
|
|
658
|
+
const rows = [];
|
|
659
|
+
const lineRe = /([^\r\n]*)(\r\n|\n|\r|$)/g;
|
|
660
|
+
let match;
|
|
661
|
+
while ((match = lineRe.exec(text))) {
|
|
662
|
+
const [full, line, ending] = match;
|
|
663
|
+
if (full === "") break;
|
|
664
|
+
rows.push({ line, ending });
|
|
665
|
+
}
|
|
666
|
+
return rows.length > 0 ? rows : [{ line: "", ending: "" }];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function joinTextLines(lines) {
|
|
670
|
+
return lines.map(({ line, ending }) => `${line}${ending}`).join("");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function isEditableMarkdownLine(line, { kind, name }) {
|
|
674
|
+
if (isFenceLine(line)) return false;
|
|
675
|
+
if (/^\s*(?:import|export)\b/.test(line)) return false;
|
|
676
|
+
if (/^\s*[<{}]/.test(line)) return false;
|
|
677
|
+
if (/^\s*\|/.test(line)) return false;
|
|
678
|
+
if (kind === "list-item") return /^(\s*(?:[-*+]|\d+[.)])\s+)/.test(line);
|
|
679
|
+
if (name && /^h[1-6]$/.test(name)) return /^(\s{0,3}#{1,6}\s+)/.test(line);
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function isMarkdownTableRow(line) {
|
|
684
|
+
return /^\s*\|.*\|\s*$/.test(line);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function replaceMarkdownTableCell(line, cellIndex, replacementText) {
|
|
688
|
+
const parts = splitMarkdownTableRow(line);
|
|
689
|
+
const firstCellPartIndex = parts[0] === "" ? 1 : 0;
|
|
690
|
+
const lastCellPartIndex = parts.at(-1) === "" ? parts.length - 2 : parts.length - 1;
|
|
691
|
+
const targetPartIndex = firstCellPartIndex + cellIndex;
|
|
692
|
+
if (targetPartIndex > lastCellPartIndex) {
|
|
693
|
+
throw new Error(`Markdown table row does not contain cell index ${cellIndex}.`);
|
|
694
|
+
}
|
|
695
|
+
parts[targetPartIndex] = replaceTableCellContent(parts[targetPartIndex], replacementText);
|
|
696
|
+
return parts.join("|");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function splitMarkdownTableRow(line) {
|
|
700
|
+
const parts = [];
|
|
701
|
+
let current = "";
|
|
702
|
+
let escaped = false;
|
|
703
|
+
for (const char of line) {
|
|
704
|
+
if (char === "|" && !escaped) {
|
|
705
|
+
parts.push(current);
|
|
706
|
+
current = "";
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
current += char;
|
|
710
|
+
escaped = char === "\\" && !escaped;
|
|
711
|
+
if (char !== "\\") escaped = false;
|
|
712
|
+
}
|
|
713
|
+
parts.push(current);
|
|
714
|
+
return parts;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function replaceTableCellContent(cell, replacementText) {
|
|
718
|
+
const match = cell.match(/^(\s*)(.*?)(\s*)$/);
|
|
719
|
+
if (!match) return replacementText;
|
|
720
|
+
const leading = match[1] || " ";
|
|
721
|
+
const trailing = match[3] || " ";
|
|
722
|
+
return `${leading}${replacementText}${trailing}`;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function markdownTextPrefix(line, { kind, name }) {
|
|
726
|
+
if (kind === "list-item") return line.match(/^(\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?)/)?.[1] ?? "- ";
|
|
727
|
+
if (name && /^h[1-6]$/.test(name)) return line.match(/^(\s{0,3}#{1,6}\s+)/)?.[1] ?? `${"#".repeat(Number(name.slice(1)))} `;
|
|
728
|
+
if (name === "blockquote") return line.match(/^(\s{0,3}>\s*)/)?.[1] ?? "> ";
|
|
729
|
+
return "";
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function replaceCaptionText(sourceText, replacementText, name) {
|
|
733
|
+
const componentMatch = sourceText.match(/^(\s*<TableCaption(?:\s[^>]*)?>)([\s\S]*?)(<\/TableCaption>\s*)$/);
|
|
734
|
+
if (componentMatch) return `${componentMatch[1]}${replacementText}${componentMatch[3]}`;
|
|
735
|
+
|
|
736
|
+
const tagName = name === "figcaption" ? "figcaption" : "caption";
|
|
737
|
+
const tagRe = new RegExp(`^(\\s*<${tagName}(?:\\s[^>]*)?>)([\\s\\S]*?)(<\\/${tagName}>\\s*)$`);
|
|
738
|
+
const tagMatch = sourceText.match(tagRe);
|
|
739
|
+
if (tagMatch) return `${tagMatch[1]}${replacementText}${tagMatch[3]}`;
|
|
740
|
+
|
|
741
|
+
throw new Error("Caption edits require a source-mapped TableCaption or caption element.");
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function applySourceBlockComponentCaptionEditToText(documentText, {
|
|
745
|
+
source,
|
|
746
|
+
text,
|
|
747
|
+
blockId,
|
|
748
|
+
name,
|
|
749
|
+
} = {}) {
|
|
750
|
+
assertEditableComponentCaption({ name, blockId });
|
|
751
|
+
const sourceRange = normalizeSourceRange(source);
|
|
752
|
+
const replacementText = normalizeEditedText(text);
|
|
753
|
+
const lines = splitTextLines(documentText);
|
|
754
|
+
const startIndex = sourceRange.line - 1;
|
|
755
|
+
const endIndex = sourceRange.endLine - 1;
|
|
756
|
+
|
|
757
|
+
if (!lines[startIndex]) {
|
|
758
|
+
throw new Error(`Source edit line ${sourceRange.line} is outside the source file.`);
|
|
759
|
+
}
|
|
760
|
+
if (!lines[endIndex]) {
|
|
761
|
+
throw new Error(`Source edit end line ${sourceRange.endLine} is outside the source file.`);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const selectedLines = lines.slice(startIndex, endIndex + 1);
|
|
765
|
+
const before = selectedLines.map((line) => line.line).join("\n");
|
|
766
|
+
const after = replaceComponentCaptionProp(before, replacementText);
|
|
767
|
+
const replacementLines = after.split("\n");
|
|
768
|
+
const ending = selectedLines[selectedLines.length - 1].ending;
|
|
769
|
+
const nextLines = [
|
|
770
|
+
...lines.slice(0, startIndex),
|
|
771
|
+
...replacementLines.map((line, index) => ({
|
|
772
|
+
line,
|
|
773
|
+
ending: index === replacementLines.length - 1 ? ending : "\n",
|
|
774
|
+
})),
|
|
775
|
+
...lines.slice(endIndex + 1),
|
|
776
|
+
];
|
|
777
|
+
|
|
778
|
+
return {
|
|
779
|
+
text: joinTextLines(nextLines),
|
|
780
|
+
edit: {
|
|
781
|
+
blockId,
|
|
782
|
+
line: sourceRange.line,
|
|
783
|
+
column: sourceRange.column,
|
|
784
|
+
endLine: sourceRange.endLine,
|
|
785
|
+
endColumn: sourceRange.endColumn,
|
|
786
|
+
before,
|
|
787
|
+
after,
|
|
788
|
+
text: replacementText,
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function assertEditableComponentCaption({ name, blockId }) {
|
|
794
|
+
if (name === "MediaFigure" || name === "ImageFigure") return;
|
|
795
|
+
throw new Error(`Only MediaFigure and ImageFigure caption props can be edited inline${blockId ? `: ${blockId}` : ""}.`);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function replaceComponentCaptionProp(sourceText, replacementText) {
|
|
799
|
+
const attrRe = /(\bcaption\s*=\s*)(["'])([\s\S]*?)(\2)/m;
|
|
800
|
+
const attrMatch = sourceText.match(attrRe);
|
|
801
|
+
if (!attrMatch) {
|
|
802
|
+
throw new Error("Figure caption edits require a quoted caption prop.");
|
|
803
|
+
}
|
|
804
|
+
const quote = attrMatch[2];
|
|
805
|
+
const escapedReplacement = escapeJsxAttributeValue(replacementText, quote);
|
|
806
|
+
return sourceText.replace(attrRe, `$1${quote}${escapedReplacement}${quote}`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function escapeJsxAttributeValue(value, quote) {
|
|
810
|
+
const quoted = String(value ?? "")
|
|
811
|
+
.replaceAll("&", "&")
|
|
812
|
+
.replaceAll("<", "<")
|
|
813
|
+
.replaceAll(">", ">");
|
|
814
|
+
return quote === '"'
|
|
815
|
+
? quoted.replaceAll('"', """)
|
|
816
|
+
: quoted.replaceAll("'", "'");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function sourceTextPathMatches(candidatePath, requestedPath) {
|
|
820
|
+
return normalizeSourceTextPath(candidatePath) === normalizeSourceTextPath(requestedPath);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function normalizeSourceTextPath(value) {
|
|
824
|
+
return String(value ?? "")
|
|
825
|
+
.replaceAll("\\", "/")
|
|
826
|
+
.replace(/^\.\//, "")
|
|
827
|
+
.replace(/^press\//, "");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function stringValue(value) {
|
|
831
|
+
return typeof value === "string" ? value.trim() : "";
|
|
832
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { loadReactDocumentEntry } from "../react/document-entry.mjs";
|
|
4
|
+
import { walkFiles } from "./file-walk.mjs";
|
|
5
|
+
import { resolveDocumentRelativePath, rootRelativePath } from "./path-utils.mjs";
|
|
6
|
+
|
|
7
|
+
export { rootRelativePath };
|
|
4
8
|
|
|
5
9
|
export const REACT_MDX_CONTENT_EXTENSIONS = new Set([".mdx"]);
|
|
6
10
|
|
|
@@ -8,10 +12,19 @@ export async function resolveActiveSourceWorkspace(config) {
|
|
|
8
12
|
const reactEntry = await loadReactDocumentEntry(config.root);
|
|
9
13
|
if (!reactEntry) {
|
|
10
14
|
throw new Error(
|
|
11
|
-
"React/MDX document entry not found. Expected
|
|
15
|
+
"React/MDX document entry not found. Expected press/index.tsx with a Press default export before using workspace source tools.",
|
|
12
16
|
);
|
|
13
17
|
}
|
|
14
|
-
|
|
18
|
+
// Aggregate sources across every Press in the Workspace. Workspace
|
|
19
|
+
// tooling (validate, search, replace, inspect) walks the union — a
|
|
20
|
+
// multi-Press project's sources all live under the same press/ tree.
|
|
21
|
+
const aggregateSources = {};
|
|
22
|
+
for (const press of reactEntry.presses ?? []) {
|
|
23
|
+
if (press.sources && typeof press.sources === "object") {
|
|
24
|
+
Object.assign(aggregateSources, press.sources);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const contentRoots = contentRootsFromSources(aggregateSources, reactEntry.config);
|
|
15
28
|
const sourceDir = firstDirectoryRoot(contentRoots) ?? reactEntry.config.paths.documentRoot;
|
|
16
29
|
|
|
17
30
|
return {
|
|
@@ -25,7 +38,7 @@ export async function resolveActiveSourceWorkspace(config) {
|
|
|
25
38
|
contentLabel: "React MDX chapter source",
|
|
26
39
|
missingCode: "react-source.missing",
|
|
27
40
|
emptyCode: "react-source.empty",
|
|
28
|
-
missingMessage: "Registered React MDX sources do not exist yet; create the files or roots declared in
|
|
41
|
+
missingMessage: "Registered React MDX sources do not exist yet; create the files or roots declared in press/index.tsx `sources` before running export.",
|
|
29
42
|
emptyMessage: "Registered React MDX sources contain no `*.mdx` files; the document will export with zero source blocks.",
|
|
30
43
|
};
|
|
31
44
|
}
|
|
@@ -77,26 +90,6 @@ export async function sourceDirectoryExists(sourceWorkspace) {
|
|
|
77
90
|
return false;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
export function rootRelativePath(config, absolutePath) {
|
|
81
|
-
return path.relative(config.root, absolutePath).replaceAll("\\", "/");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function walkFiles(directory, visit) {
|
|
85
|
-
let entries;
|
|
86
|
-
try {
|
|
87
|
-
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
88
|
-
} catch (error) {
|
|
89
|
-
if (error?.code === "ENOENT") return;
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
if (entry.name.startsWith(".")) continue;
|
|
94
|
-
const absolutePath = path.join(directory, entry.name);
|
|
95
|
-
if (entry.isDirectory()) await walkFiles(absolutePath, visit);
|
|
96
|
-
else if (entry.isFile()) await visit(absolutePath);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
93
|
function contentRootsFromSources(sources, config) {
|
|
101
94
|
const entries = Object.entries(sources ?? {});
|
|
102
95
|
if (entries.length === 0) {
|
|
@@ -158,17 +151,6 @@ function fileRoot(config, rel, sourceId, preset) {
|
|
|
158
151
|
};
|
|
159
152
|
}
|
|
160
153
|
|
|
161
|
-
function resolveDocumentRelativePath(documentRoot, rel, label) {
|
|
162
|
-
if (typeof rel !== "string" || !rel.trim()) throw new Error(`${label} must be a non-empty document-relative path.`);
|
|
163
|
-
if (rel.includes("..")) throw new Error(`${label} contains "..", rejected.`);
|
|
164
|
-
const absolutePath = path.resolve(documentRoot, rel);
|
|
165
|
-
const relCheck = path.relative(documentRoot, absolutePath);
|
|
166
|
-
if (relCheck.startsWith("..") || path.isAbsolute(relCheck)) {
|
|
167
|
-
throw new Error(`${label} escapes the document root.`);
|
|
168
|
-
}
|
|
169
|
-
return absolutePath;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
154
|
function dedupeRoots(roots) {
|
|
173
155
|
const seen = new Set();
|
|
174
156
|
const out = [];
|