@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,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizePageGeometry } from "./page-geometry.mjs";
|
|
4
4
|
|
|
5
5
|
const DEFAULT_CONFIG = {
|
|
6
6
|
title: "OpenPress Document",
|
|
@@ -30,33 +30,72 @@ const DEFAULT_CONFIG = {
|
|
|
30
30
|
commitDirty: false,
|
|
31
31
|
requiresConfirmation: true,
|
|
32
32
|
},
|
|
33
|
+
page: null,
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
// 1.0 contract: the only user-writable config lives in package.json
|
|
37
|
+
// under the "openpress" field. The engine reads it synchronously so
|
|
38
|
+
// the deploy command can resolve its adapter before any React render.
|
|
39
|
+
//
|
|
40
|
+
// Everything else is convention (path layout) or declared on
|
|
41
|
+
// <Press> / <Workspace> JSX props (document metadata).
|
|
35
42
|
export async function loadConfig(root = ".") {
|
|
36
43
|
const workspaceRoot = path.resolve(root);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const { config, sourceConfigPath } = await resolveUserConfig(workspaceRoot, rootConfig, configPath);
|
|
40
|
-
return normalizeConfig(workspaceRoot, config, sourceConfigPath);
|
|
44
|
+
const packageOpenpress = await readPackageOpenpressField(workspaceRoot);
|
|
45
|
+
return normalizeConfig(workspaceRoot, packageOpenpress ?? {});
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
async function readPackageOpenpressField(workspaceRoot) {
|
|
49
|
+
const pkgPath = path.join(workspaceRoot, "package.json");
|
|
50
|
+
try {
|
|
51
|
+
const raw = await fs.readFile(pkgPath, "utf8");
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
const field = parsed?.openpress;
|
|
54
|
+
return (field && typeof field === "object" && !Array.isArray(field)) ? field : null;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error?.code === "ENOENT") return null;
|
|
57
|
+
if (error instanceof SyntaxError) {
|
|
58
|
+
throw new Error(`Malformed package.json at ${pkgPath}: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convention-only fields. The user can't override these — they're part
|
|
65
|
+
// of OpenPress's product surface. If you don't like the layout, your
|
|
66
|
+
// project isn't an OpenPress workspace.
|
|
67
|
+
const CONVENTION = {
|
|
68
|
+
documentDir: "press",
|
|
69
|
+
sourceDir: "chapters",
|
|
70
|
+
mediaDir: "media",
|
|
71
|
+
themeDir: "theme",
|
|
72
|
+
componentsDir: "components",
|
|
73
|
+
designDoc: "design.md",
|
|
74
|
+
publicDir: "public/openpress",
|
|
75
|
+
outputDir: "dist-react",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function normalizeConfig(root, userConfig = {}, configPath = path.join(root, "package.json")) {
|
|
44
79
|
const config = {
|
|
45
80
|
root: path.resolve(root),
|
|
46
81
|
configPath,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
// Document metadata defaults — actual values are merged in by
|
|
83
|
+
// loadReactDocumentEntry from <Press> / <Workspace> JSX props.
|
|
84
|
+
title: DEFAULT_CONFIG.title,
|
|
85
|
+
subtitle: "",
|
|
86
|
+
organization: "",
|
|
87
|
+
workspaceLabel: "",
|
|
88
|
+
// Paths — fixed conventions, not user-configurable.
|
|
89
|
+
documentDir: CONVENTION.documentDir,
|
|
90
|
+
sourceDir: CONVENTION.sourceDir,
|
|
91
|
+
mediaDir: CONVENTION.mediaDir,
|
|
92
|
+
themeDir: CONVENTION.themeDir,
|
|
93
|
+
designDoc: CONVENTION.designDoc,
|
|
94
|
+
componentsDir: CONVENTION.componentsDir,
|
|
95
|
+
publicDir: CONVENTION.publicDir,
|
|
96
|
+
outputDir: CONVENTION.outputDir,
|
|
59
97
|
captionNumbering: captionNumberingValue(userConfig.captionNumbering, DEFAULT_CONFIG.captionNumbering),
|
|
98
|
+
page: normalizePageGeometry(userConfig.page ?? DEFAULT_CONFIG.page),
|
|
60
99
|
pdf: {
|
|
61
100
|
filename: fileNameValue(userConfig.pdf?.filename, DEFAULT_CONFIG.pdf.filename),
|
|
62
101
|
},
|
|
@@ -91,32 +130,6 @@ export function publicPdfHref(config) {
|
|
|
91
130
|
return `/${config.pdf.filename}`;
|
|
92
131
|
}
|
|
93
132
|
|
|
94
|
-
async function readUserConfig(configPath) {
|
|
95
|
-
try {
|
|
96
|
-
const stat = await fs.stat(configPath);
|
|
97
|
-
const configUrl = `${pathToFileURL(configPath).href}?mtime=${stat.mtimeMs}`;
|
|
98
|
-
const mod = await import(configUrl);
|
|
99
|
-
return mod.default ?? {};
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error?.code === "ENOENT") return {};
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function resolveUserConfig(root, rootConfig, configPath) {
|
|
107
|
-
const documentConfigPath = configPathValue(rootConfig.config ?? rootConfig.documentConfig);
|
|
108
|
-
if (!documentConfigPath) {
|
|
109
|
-
return { config: rootConfig, sourceConfigPath: configPath };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const sourceConfigPath = path.resolve(root, documentConfigPath);
|
|
113
|
-
const documentConfig = await readUserConfig(sourceConfigPath);
|
|
114
|
-
return {
|
|
115
|
-
config: { ...documentConfig, ...rootConfig },
|
|
116
|
-
sourceConfigPath,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
133
|
function stringValue(value, fallback) {
|
|
121
134
|
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
122
135
|
}
|
|
@@ -148,22 +161,6 @@ function fileNameValue(value, fallback) {
|
|
|
148
161
|
return fileName;
|
|
149
162
|
}
|
|
150
163
|
|
|
151
|
-
function configPathValue(value) {
|
|
152
|
-
if (typeof value !== "string" || !value.trim()) return null;
|
|
153
|
-
return relativePathValue(value, null);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function documentPathValue(value, fallback) {
|
|
157
|
-
const raw = stringValue(value, fallback).replaceAll("\\", "/");
|
|
158
|
-
if (path.isAbsolute(raw)) throw new Error(`OpenPress config paths must be relative, got: ${raw}`);
|
|
159
|
-
const normalized = path.posix.normalize(raw).replace(/^\.\//, "");
|
|
160
|
-
if (normalized === ".") return ".";
|
|
161
|
-
if (!normalized || normalized === ".." || normalized.startsWith("../")) {
|
|
162
|
-
throw new Error(`OpenPress config path escapes workspace: ${raw}`);
|
|
163
|
-
}
|
|
164
|
-
return normalized;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
164
|
function relativePathValue(value, fallback) {
|
|
168
165
|
const raw = stringValue(value, fallback).replaceAll("\\", "/");
|
|
169
166
|
if (path.isAbsolute(raw)) throw new Error(`OpenPress config paths must be relative, got: ${raw}`);
|
|
@@ -10,6 +10,7 @@ const CONTENT_CSS_LAYERS = [
|
|
|
10
10
|
{ path: "page-surfaces/chapter-opener.css", optional: true },
|
|
11
11
|
"page-surfaces/back-cover.css",
|
|
12
12
|
"page-surfaces/toc.css",
|
|
13
|
+
{ path: "page-surfaces", type: "directory", exclude: ["cover.css", "chapter-opener.css", "back-cover.css", "toc.css"] },
|
|
13
14
|
"shell/reader-controls.css",
|
|
14
15
|
"base/print.css",
|
|
15
16
|
];
|
|
@@ -32,6 +33,12 @@ export async function buildContentCss(root, config) {
|
|
|
32
33
|
const contentAssetsDir = config.paths.themeDir;
|
|
33
34
|
const parts = [];
|
|
34
35
|
for (const layer of CONTENT_CSS_LAYERS) {
|
|
36
|
+
if (typeof layer !== "string" && layer.type === "directory") {
|
|
37
|
+
await appendCssDirectory(parts, path.join(contentAssetsDir, layer.path), layer.path, {
|
|
38
|
+
exclude: new Set(layer.exclude ?? []),
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
35
42
|
const relativePath = typeof layer === "string" ? layer : layer.path;
|
|
36
43
|
const cssPath = path.join(contentAssetsDir, relativePath);
|
|
37
44
|
let css;
|
|
@@ -66,7 +73,7 @@ export async function buildComponentsCss(root, config) {
|
|
|
66
73
|
return parts.join("");
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
async function appendCssDirectory(parts, directory, labelPrefix) {
|
|
76
|
+
async function appendCssDirectory(parts, directory, labelPrefix, options = {}) {
|
|
70
77
|
let entries;
|
|
71
78
|
try {
|
|
72
79
|
entries = await fs.readdir(directory);
|
|
@@ -74,6 +81,7 @@ async function appendCssDirectory(parts, directory, labelPrefix) {
|
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
76
83
|
for (const name of entries.filter((entry) => entry.endsWith(".css")).sort()) {
|
|
84
|
+
if (options.exclude?.has(name)) continue;
|
|
77
85
|
parts.push(`/* === ${labelPrefix}/${name} === */\n`);
|
|
78
86
|
parts.push((await fs.readFile(path.join(directory, name), "utf8")).trimEnd());
|
|
79
87
|
parts.push("\n\n");
|
|
@@ -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,131 @@
|
|
|
1
|
+
const CSS_LENGTH_RE = /^(\d+(?:\.\d+)?)(px|mm|cm|in|pt|pc)$/i;
|
|
2
|
+
|
|
3
|
+
export const PAGE_GEOMETRY_PRESETS = {
|
|
4
|
+
a4: {
|
|
5
|
+
id: "a4",
|
|
6
|
+
label: "A4 Page",
|
|
7
|
+
width: "210mm",
|
|
8
|
+
height: "297mm",
|
|
9
|
+
},
|
|
10
|
+
"social-square": {
|
|
11
|
+
id: "social-square",
|
|
12
|
+
label: "Social Square",
|
|
13
|
+
width: "1080px",
|
|
14
|
+
height: "1080px",
|
|
15
|
+
},
|
|
16
|
+
"slide-16-9": {
|
|
17
|
+
id: "slide-16-9",
|
|
18
|
+
label: "Slide 16:9",
|
|
19
|
+
width: "1920px",
|
|
20
|
+
height: "1080px",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function normalizePageGeometry(value) {
|
|
25
|
+
if (value == null || value === false) return null;
|
|
26
|
+
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
return presetPageGeometry(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
32
|
+
throw new Error("OpenPress config page must be a preset name or an object.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const preset = typeof value.preset === "string" ? presetPageGeometry(value.preset) : null;
|
|
36
|
+
const width = cssLengthValue(value.width, preset?.width);
|
|
37
|
+
const height = cssLengthValue(value.height, preset?.height);
|
|
38
|
+
if (!width || !height) {
|
|
39
|
+
throw new Error("OpenPress config page requires width and height when no preset is provided.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const id = optionalId(value.id, preset?.id ?? "custom");
|
|
43
|
+
const label = optionalLabel(value.label, preset?.label ?? "Custom Page");
|
|
44
|
+
|
|
45
|
+
return pageGeometry({
|
|
46
|
+
id,
|
|
47
|
+
label,
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function pageGeometryToTheme(page) {
|
|
54
|
+
if (!page) return undefined;
|
|
55
|
+
return {
|
|
56
|
+
pagePreset: page.id,
|
|
57
|
+
pageLabel: page.label,
|
|
58
|
+
pageWidth: page.width,
|
|
59
|
+
pageHeight: page.height,
|
|
60
|
+
pageAspectRatio: page.aspectRatio,
|
|
61
|
+
pageHeightRatio: page.heightRatio,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function presetPageGeometry(id) {
|
|
66
|
+
const preset = PAGE_GEOMETRY_PRESETS[id];
|
|
67
|
+
if (!preset) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Unknown OpenPress page preset: "${id}". ` +
|
|
70
|
+
`Available presets: ${Object.keys(PAGE_GEOMETRY_PRESETS).join(", ")}.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return pageGeometry(preset);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pageGeometry({ id, label, width, height }) {
|
|
77
|
+
const ratio = sameUnitRatio(width, height);
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
label,
|
|
81
|
+
width,
|
|
82
|
+
height,
|
|
83
|
+
aspectRatio: ratio ? `${trimNumber(ratio.width)} / ${trimNumber(ratio.height)}` : undefined,
|
|
84
|
+
heightRatio: ratio ? trimNumber(ratio.height / ratio.width) : undefined,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function cssLengthValue(value, fallback) {
|
|
89
|
+
if (value == null) return fallback ?? null;
|
|
90
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
91
|
+
throw new Error("OpenPress page width/height must be CSS length strings.");
|
|
92
|
+
}
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
if (!CSS_LENGTH_RE.test(trimmed)) {
|
|
95
|
+
throw new Error(`OpenPress page size must be an absolute CSS length, got: ${trimmed}`);
|
|
96
|
+
}
|
|
97
|
+
return trimmed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function optionalId(value, fallback) {
|
|
101
|
+
if (value == null) return fallback;
|
|
102
|
+
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/i.test(value)) {
|
|
103
|
+
throw new Error("OpenPress page id must be a simple slug.");
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function optionalLabel(value, fallback) {
|
|
109
|
+
if (value == null) return fallback;
|
|
110
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
111
|
+
throw new Error("OpenPress page label must be a non-empty string.");
|
|
112
|
+
}
|
|
113
|
+
return value.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sameUnitRatio(width, height) {
|
|
117
|
+
const w = parseCssLength(width);
|
|
118
|
+
const h = parseCssLength(height);
|
|
119
|
+
if (!w || !h || w.unit.toLowerCase() !== h.unit.toLowerCase()) return null;
|
|
120
|
+
return { width: w.value, height: h.value };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseCssLength(value) {
|
|
124
|
+
const match = CSS_LENGTH_RE.exec(value);
|
|
125
|
+
if (!match) return null;
|
|
126
|
+
return { value: Number(match[1]), unit: match[2] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function trimNumber(value) {
|
|
130
|
+
return Number(value.toFixed(6)).toString();
|
|
131
|
+
}
|
|
@@ -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
|
+
}>>;
|