@open-press/core 1.2.1 → 1.3.1
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 +2 -2
- package/engine/commands/typecheck.mjs +1 -1
- package/engine/document-export.mjs +1 -1
- package/engine/output/page-block.mjs +11 -2
- package/engine/output/public-assets.mjs +41 -6
- package/engine/output/static-server.mjs +68 -15
- package/engine/react/caption-numbering.mjs +2 -2
- package/engine/react/comment-marker.mjs +1 -2
- package/engine/react/document-entry.mjs +64 -11
- package/engine/react/document-export.d.mts +6 -0
- package/engine/react/document-export.mjs +158 -28
- package/engine/react/mdx-compile.mjs +4 -4
- package/engine/react/measurement-css.mjs +3 -3
- package/engine/react/page-folio.mjs +37 -0
- package/engine/react/pagination/allocator.mjs +4 -4
- package/engine/react/pipeline/frame-measurement.mjs +34 -16
- package/engine/react/press-tree-inspection.mjs +43 -13
- package/engine/react/project-asset-endpoint.mjs +45 -11
- package/engine/react/sources/heading-numbering.mjs +2 -2
- package/engine/react/sources/mdx-resolver.mjs +3 -3
- package/engine/react/style-discovery.mjs +60 -11
- package/engine/react/text-source-transform.mjs +18 -4
- package/engine/runtime/config.mjs +22 -22
- package/engine/runtime/file-utils.mjs +57 -13
- package/engine/runtime/inspection.mjs +40 -15
- package/engine/runtime/page-geometry.mjs +6 -6
- package/engine/runtime/source-text-tools.mjs +28 -4
- package/engine/runtime/source-workspace.mjs +6 -9
- package/engine/runtime/validation.mjs +42 -24
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +6 -15
- package/src/openpress/app/OpenPressRuntime.tsx +3 -3
- package/src/openpress/app/WorkspaceGalleryPage.tsx +1 -1
- package/src/openpress/core/PageFolio.tsx +115 -0
- package/src/openpress/core/Press.tsx +5 -10
- package/src/openpress/core/Slide.tsx +11 -0
- package/src/openpress/core/index.tsx +4 -0
- package/src/openpress/core/types.ts +21 -13
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +4 -9
- package/vite.config.ts +82 -16
|
@@ -75,9 +75,10 @@ async function renameProjectAsset({ config, kind, name, nextName }) {
|
|
|
75
75
|
throw new Error("Rename requires a different valid name.");
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const current = await resolveExistingAssetPath(config, kind, normalizedCurrentName);
|
|
79
|
+
if (!current) throw new Error(`${kind} asset not found: ${normalizedCurrentName}`);
|
|
80
|
+
const currentPath = current.path;
|
|
81
|
+
const nextPath = resolveAssetPathInRoot(current.root, kind, normalizedNextName);
|
|
81
82
|
if (await fileExists(nextPath)) throw new Error(`${kind} asset already exists: ${normalizedNextName}`);
|
|
82
83
|
|
|
83
84
|
await fs.rename(currentPath, nextPath);
|
|
@@ -113,8 +114,9 @@ async function deleteProjectAsset({ config, kind, name }) {
|
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
const
|
|
117
|
-
|
|
117
|
+
const target = await resolveExistingAssetPath(config, kind, normalizedName);
|
|
118
|
+
if (!target) throw new Error(`${kind} asset not found: ${normalizedName}`);
|
|
119
|
+
const targetPath = target.path;
|
|
118
120
|
await fs.rm(targetPath, { recursive: true, force: true });
|
|
119
121
|
|
|
120
122
|
return {
|
|
@@ -242,8 +244,15 @@ async function findProjectAssetReferences({ config, kind, name }) {
|
|
|
242
244
|
return references;
|
|
243
245
|
}
|
|
244
246
|
|
|
245
|
-
function
|
|
246
|
-
const root
|
|
247
|
+
async function resolveExistingAssetPath(config, kind, name) {
|
|
248
|
+
for (const root of await assetRoots(config, kind)) {
|
|
249
|
+
const target = resolveAssetPathInRoot(root, kind, name);
|
|
250
|
+
if (await fileExists(target)) return { root, path: target };
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function resolveAssetPathInRoot(root, kind, name) {
|
|
247
256
|
const target = path.resolve(root, name);
|
|
248
257
|
const resolvedRoot = path.resolve(root);
|
|
249
258
|
if (!target.startsWith(`${resolvedRoot}${path.sep}`) && target !== resolvedRoot) {
|
|
@@ -252,6 +261,35 @@ function resolveAssetPath(config, kind, name) {
|
|
|
252
261
|
return target;
|
|
253
262
|
}
|
|
254
263
|
|
|
264
|
+
async function assetRoots(config, kind) {
|
|
265
|
+
const childName = kind === "media" ? "media" : "components";
|
|
266
|
+
const roots = [
|
|
267
|
+
kind === "media" ? config.paths.mediaDir : config.paths.componentsDir,
|
|
268
|
+
];
|
|
269
|
+
try {
|
|
270
|
+
const entries = await fs.readdir(config.paths.documentRoot, { withFileTypes: true });
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "shared") continue;
|
|
273
|
+
roots.push(path.join(config.paths.documentRoot, entry.name, childName));
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// Missing press/ is reported by workspace validation/export.
|
|
277
|
+
}
|
|
278
|
+
return uniquePaths(roots);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function uniquePaths(paths) {
|
|
282
|
+
const out = [];
|
|
283
|
+
const seen = new Set();
|
|
284
|
+
for (const candidate of paths) {
|
|
285
|
+
const normalized = path.resolve(candidate);
|
|
286
|
+
if (seen.has(normalized)) continue;
|
|
287
|
+
seen.add(normalized);
|
|
288
|
+
out.push(normalized);
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
255
293
|
function normalizeAssetName(kind, value, currentName = "") {
|
|
256
294
|
if (kind === "media") return sanitizeMediaFileName(value, currentName);
|
|
257
295
|
return sanitizeComponentName(value);
|
|
@@ -347,10 +385,6 @@ function normalizePositiveInteger(value) {
|
|
|
347
385
|
return Number.isInteger(number) && number > 0 ? number : null;
|
|
348
386
|
}
|
|
349
387
|
|
|
350
|
-
async function assertPathExists(filePath, message) {
|
|
351
|
-
if (!(await fileExists(filePath))) throw new Error(message);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
388
|
async function fileExists(filePath) {
|
|
355
389
|
try {
|
|
356
390
|
await fs.access(filePath);
|
|
@@ -113,11 +113,11 @@ export function headingAttributesForBlock({
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Build a
|
|
116
|
+
* Build a section-title outline row when a section has no h2/h3 headings
|
|
117
117
|
* the resolver could pick up. The outline still needs one row so the TOC
|
|
118
118
|
* doesn't lose the section.
|
|
119
119
|
*/
|
|
120
|
-
export function
|
|
120
|
+
export function sectionTitleOutlineItems({ sourceId, section, chapterLabel, title, blocks }) {
|
|
121
121
|
const targetBlock = blocks[0];
|
|
122
122
|
return [{
|
|
123
123
|
id: `${sourceId}:${section.slug}`,
|
|
@@ -12,12 +12,12 @@ import path from "node:path";
|
|
|
12
12
|
import React from "react";
|
|
13
13
|
import { documentRelativePath, resolveDocumentRelativePath } from "../../runtime/path-utils.mjs";
|
|
14
14
|
import { compileMdx } from "../mdx-compile.mjs";
|
|
15
|
-
import { createHeadingState,
|
|
15
|
+
import { createHeadingState, headingAttributesForBlock, sectionTitleOutlineItems } from "./heading-numbering.mjs";
|
|
16
16
|
|
|
17
17
|
const MDX_EXT = ".mdx";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Resolve all sources registered in `press
|
|
20
|
+
* Resolve all sources registered in `press/<slug>/press.tsx`.
|
|
21
21
|
*
|
|
22
22
|
* @param {object} opts
|
|
23
23
|
* @param {Record<string, object>} opts.sources The raw `sources` export.
|
|
@@ -141,7 +141,7 @@ async function resolveSource({ sourceId, descriptor, documentRoot, globalCompone
|
|
|
141
141
|
title: resolvedSectionTitle,
|
|
142
142
|
meta: section.meta ?? {},
|
|
143
143
|
});
|
|
144
|
-
outline.push(...(outlineItems.length > 0 ? outlineItems :
|
|
144
|
+
outline.push(...(outlineItems.length > 0 ? outlineItems : sectionTitleOutlineItems({
|
|
145
145
|
sourceId,
|
|
146
146
|
section,
|
|
147
147
|
chapterLabel,
|
|
@@ -12,18 +12,19 @@ const COMPONENT_EXT = ".tsx";
|
|
|
12
12
|
export async function discoverSectionStyles(root = ".", config = {}, { sectionRoots } = {}) {
|
|
13
13
|
const workspaceRoot = path.resolve(root);
|
|
14
14
|
const documentRoot = config.paths?.documentRoot ?? path.join(workspaceRoot, "press");
|
|
15
|
-
const componentsRoot = config.paths?.componentsDir ?? path.join(documentRoot, "components");
|
|
16
|
-
const globalComponents = await
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
15
|
+
const componentsRoot = config.paths?.componentsDir ?? path.join(documentRoot, "shared", "components");
|
|
16
|
+
const globalComponents = await discoverComponentsInRoots(
|
|
17
|
+
[componentsRoot],
|
|
18
|
+
documentRoot,
|
|
19
|
+
"global",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// The caller usually passes each Press's resolved section-folders root.
|
|
23
|
+
// When it does not, discover conventional `press/<slug>/chapters` roots.
|
|
24
|
+
// Duplicate paths are de-duplicated by absolutePath.
|
|
24
25
|
const effectiveRoots = sectionRoots && sectionRoots.length > 0
|
|
25
26
|
? sectionRoots
|
|
26
|
-
:
|
|
27
|
+
: await discoverConventionalChapterRoots(documentRoot);
|
|
27
28
|
const seen = new Set();
|
|
28
29
|
const sections = [];
|
|
29
30
|
for (const sectionsRoot of effectiveRoots) {
|
|
@@ -40,11 +41,46 @@ export async function discoverSectionStyles(root = ".", config = {}, { sectionRo
|
|
|
40
41
|
documentRoot,
|
|
41
42
|
globalComponents,
|
|
42
43
|
sections,
|
|
43
|
-
// Back-compat: `chapters` alias for callers that still expect the old shape.
|
|
44
44
|
chapters: sections,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
async function discoverConventionalChapterRoots(documentRoot) {
|
|
49
|
+
const roots = [];
|
|
50
|
+
let entries = [];
|
|
51
|
+
try {
|
|
52
|
+
entries = await fs.readdir(documentRoot, { withFileTypes: true });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === "ENOENT") return roots;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (!entry.isDirectory() || entry.name === "shared" || entry.name.startsWith(".")) continue;
|
|
59
|
+
const chaptersRoot = path.join(documentRoot, entry.name, "chapters");
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.stat(chaptersRoot);
|
|
62
|
+
if (stat.isDirectory()) roots.push(chaptersRoot);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error?.code !== "ENOENT") throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return roots;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function discoverComponentsInRoots(componentRoots, documentRoot, scope = "global") {
|
|
71
|
+
const components = [];
|
|
72
|
+
const seenPaths = new Set();
|
|
73
|
+
for (const componentsDir of uniquePaths(componentRoots)) {
|
|
74
|
+
const found = await discoverComponents(componentsDir, documentRoot, scope);
|
|
75
|
+
for (const component of found) {
|
|
76
|
+
if (seenPaths.has(component.absolutePath)) continue;
|
|
77
|
+
seenPaths.add(component.absolutePath);
|
|
78
|
+
components.push(component);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return components;
|
|
82
|
+
}
|
|
83
|
+
|
|
48
84
|
async function discoverSections(documentRoot, sectionsDir) {
|
|
49
85
|
const entries = await readDirectoryEntries(sectionsDir);
|
|
50
86
|
const sectionDirs = entries.filter((entry) => entry.isDirectory()).sort(compareSectionDirectories);
|
|
@@ -149,6 +185,19 @@ async function readDirectoryEntries(directory) {
|
|
|
149
185
|
}
|
|
150
186
|
}
|
|
151
187
|
|
|
188
|
+
function uniquePaths(paths) {
|
|
189
|
+
const out = [];
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
for (const candidate of paths ?? []) {
|
|
192
|
+
if (!candidate) continue;
|
|
193
|
+
const normalized = path.resolve(candidate);
|
|
194
|
+
if (seen.has(normalized)) continue;
|
|
195
|
+
seen.add(normalized);
|
|
196
|
+
out.push(normalized);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
152
201
|
async function fileExists(filePath) {
|
|
153
202
|
try {
|
|
154
203
|
const stat = await fs.stat(filePath);
|
|
@@ -29,15 +29,14 @@ export function textSourceTransformPlugin({ workspaceRoot, documentRoot }) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export function addLiteralTextSourceProps(code, { filePath = "
|
|
32
|
+
export function addLiteralTextSourceProps(code, { filePath = "press/press.tsx", sourcePath = "press/<name>/press.tsx" } = {}) {
|
|
33
33
|
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
34
34
|
const textRefs = collectOpenPressTextRefs(sourceFile);
|
|
35
|
-
if (textRefs.identifiers.size === 0 && textRefs.namespaces.size === 0) return code;
|
|
36
35
|
|
|
37
36
|
const insertions = [];
|
|
38
37
|
|
|
39
38
|
const visit = (node) => {
|
|
40
|
-
if (ts.isJsxElement(node) &&
|
|
39
|
+
if (ts.isJsxElement(node) && isSourceBackedTextElement(node.openingElement, textRefs)) {
|
|
41
40
|
const opening = node.openingElement;
|
|
42
41
|
if (!hasJsxAttribute(opening, "source")) {
|
|
43
42
|
const literal = literalTextChildRange(node, sourceFile, code);
|
|
@@ -133,13 +132,28 @@ function sourcePropExpression({ sourcePath, objectId, range }) {
|
|
|
133
132
|
return `{ ${props.join(", ")} }`;
|
|
134
133
|
}
|
|
135
134
|
|
|
135
|
+
function isSourceBackedTextElement(opening, refs) {
|
|
136
|
+
if (isTextElementName(opening.tagName, refs)) return true;
|
|
137
|
+
return hasJsxAttribute(opening, "objectId") && isComponentElementName(opening.tagName);
|
|
138
|
+
}
|
|
139
|
+
|
|
136
140
|
function isTextElementName(name, refs) {
|
|
137
141
|
if (ts.isIdentifier(name)) return refs.identifiers.has(name.text);
|
|
138
|
-
if (!
|
|
142
|
+
if (!isJsxMemberExpression(name)) return false;
|
|
139
143
|
if (name.name.text !== "Text") return false;
|
|
140
144
|
return ts.isIdentifier(name.expression) && refs.namespaces.has(name.expression.text);
|
|
141
145
|
}
|
|
142
146
|
|
|
147
|
+
function isComponentElementName(name) {
|
|
148
|
+
if (ts.isIdentifier(name)) return /^[A-Z]/.test(name.text);
|
|
149
|
+
if (isJsxMemberExpression(name)) return isComponentElementName(name.expression);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isJsxMemberExpression(node) {
|
|
154
|
+
return ts.isPropertyAccessExpression(node) || node?.kind === ts.SyntaxKind.JsxMemberExpression;
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
function hasJsxAttribute(opening, name) {
|
|
144
158
|
return opening.attributes.properties.some((prop) =>
|
|
145
159
|
ts.isJsxAttribute(prop) && prop.name.text === name
|
|
@@ -8,11 +8,11 @@ const DEFAULT_CONFIG = {
|
|
|
8
8
|
organization: "",
|
|
9
9
|
workspaceLabel: "",
|
|
10
10
|
documentDir: ".",
|
|
11
|
-
sourceDir: "
|
|
12
|
-
mediaDir: "media",
|
|
13
|
-
themeDir: "theme",
|
|
11
|
+
sourceDir: ".",
|
|
12
|
+
mediaDir: "shared/media",
|
|
13
|
+
themeDir: "shared/theme",
|
|
14
14
|
designDoc: "design.md",
|
|
15
|
-
componentsDir: "components",
|
|
15
|
+
componentsDir: "shared/components",
|
|
16
16
|
publicDir: "public/openpress",
|
|
17
17
|
outputDir: "dist",
|
|
18
18
|
captionNumbering: {
|
|
@@ -66,10 +66,10 @@ async function readPackageOpenpressField(workspaceRoot) {
|
|
|
66
66
|
// project isn't an OpenPress workspace.
|
|
67
67
|
const CONVENTION = {
|
|
68
68
|
documentDir: "press",
|
|
69
|
-
sourceDir: "
|
|
70
|
-
mediaDir: "media",
|
|
71
|
-
themeDir: "theme",
|
|
72
|
-
componentsDir: "components",
|
|
69
|
+
sourceDir: ".",
|
|
70
|
+
mediaDir: "shared/media",
|
|
71
|
+
themeDir: "shared/theme",
|
|
72
|
+
componentsDir: "shared/components",
|
|
73
73
|
designDoc: "design.md",
|
|
74
74
|
publicDir: "public/openpress",
|
|
75
75
|
outputDir: "dist-react",
|
|
@@ -130,39 +130,39 @@ export function publicPdfHref(config) {
|
|
|
130
130
|
return `/${config.pdf.filename}`;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
function stringValue(value,
|
|
134
|
-
return typeof value === "string" && value.trim() ? value.trim() :
|
|
133
|
+
function stringValue(value, defaultValue) {
|
|
134
|
+
return typeof value === "string" && value.trim() ? value.trim() : defaultValue;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
function optionalStringValue(value,
|
|
137
|
+
function optionalStringValue(value, defaultValue) {
|
|
138
138
|
if (value === null) return null;
|
|
139
139
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
140
|
-
return
|
|
140
|
+
return defaultValue;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
function captionNumberingValue(value,
|
|
143
|
+
function captionNumberingValue(value, defaults) {
|
|
144
144
|
const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
145
145
|
return {
|
|
146
|
-
figure: optionalStringValue(input.figure,
|
|
147
|
-
table: optionalStringValue(input.table,
|
|
148
|
-
separator: typeof input.separator === "string" ? input.separator :
|
|
146
|
+
figure: optionalStringValue(input.figure, defaults.figure) ?? defaults.figure,
|
|
147
|
+
table: optionalStringValue(input.table, defaults.table) ?? defaults.table,
|
|
148
|
+
separator: typeof input.separator === "string" ? input.separator : defaults.separator,
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function booleanValue(value,
|
|
153
|
-
return typeof value === "boolean" ? value :
|
|
152
|
+
function booleanValue(value, defaultValue) {
|
|
153
|
+
return typeof value === "boolean" ? value : defaultValue;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
function fileNameValue(value,
|
|
157
|
-
const fileName = stringValue(value,
|
|
156
|
+
function fileNameValue(value, defaultValue) {
|
|
157
|
+
const fileName = stringValue(value, defaultValue);
|
|
158
158
|
if (fileName.includes("/") || fileName.includes("\\") || fileName === "." || fileName === "..") {
|
|
159
159
|
throw new Error(`OpenPress config pdf.filename must be a file name, got: ${fileName}`);
|
|
160
160
|
}
|
|
161
161
|
return fileName;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
function relativePathValue(value,
|
|
165
|
-
const raw = stringValue(value,
|
|
164
|
+
function relativePathValue(value, defaultValue) {
|
|
165
|
+
const raw = stringValue(value, defaultValue).replaceAll("\\", "/");
|
|
166
166
|
if (path.isAbsolute(raw)) throw new Error(`OpenPress config paths must be relative, got: ${raw}`);
|
|
167
167
|
const normalized = path.posix.normalize(raw).replace(/^\.\//, "");
|
|
168
168
|
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
@@ -21,55 +21,69 @@ export async function copyDirectory(src, dst) {
|
|
|
21
21
|
await fs.cp(src, dst, { recursive: true });
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function writeContentCss(root, targetDir, config) {
|
|
24
|
+
export async function writeContentCss(root, targetDir, config, options = {}) {
|
|
25
25
|
config ??= await loadConfig(root);
|
|
26
|
-
const css = await buildContentCss(root, config);
|
|
26
|
+
const css = await buildContentCss(root, config, options);
|
|
27
27
|
await fs.mkdir(targetDir, { recursive: true });
|
|
28
28
|
await fs.writeFile(path.join(targetDir, "content.css"), css, "utf8");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export async function buildContentCss(root, config) {
|
|
31
|
+
export async function buildContentCss(root, config, options = {}) {
|
|
32
32
|
config ??= await loadConfig(root);
|
|
33
|
-
const
|
|
33
|
+
const sharedThemeDir = config.paths.themeDir;
|
|
34
34
|
const parts = [];
|
|
35
35
|
for (const layer of CONTENT_CSS_LAYERS) {
|
|
36
36
|
if (typeof layer !== "string" && layer.type === "directory") {
|
|
37
|
-
await appendCssDirectory(parts, path.join(
|
|
37
|
+
await appendCssDirectory(parts, path.join(sharedThemeDir, layer.path), layer.path, {
|
|
38
38
|
exclude: new Set(layer.exclude ?? []),
|
|
39
39
|
});
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
42
|
const relativePath = typeof layer === "string" ? layer : layer.path;
|
|
43
|
-
const cssPath = path.join(
|
|
43
|
+
const cssPath = path.join(sharedThemeDir, relativePath);
|
|
44
44
|
let css;
|
|
45
45
|
try {
|
|
46
46
|
css = await fs.readFile(cssPath, "utf8");
|
|
47
47
|
} catch (error) {
|
|
48
|
-
if (
|
|
48
|
+
if (error.code === "ENOENT") continue;
|
|
49
49
|
throw error;
|
|
50
50
|
}
|
|
51
51
|
parts.push(`/* === ${relativePath} === */\n`);
|
|
52
52
|
parts.push(css.trimEnd());
|
|
53
53
|
parts.push("\n\n");
|
|
54
54
|
}
|
|
55
|
+
const themeRoots = uniquePaths([
|
|
56
|
+
...(await discoverPressChildRoots(config.paths.documentRoot, "theme")),
|
|
57
|
+
...(options.themeRoots ?? []),
|
|
58
|
+
]);
|
|
59
|
+
for (const themeRoot of themeRoots) {
|
|
60
|
+
await appendCssDirectory(parts, themeRoot, documentRelativeLabel(themeRoot, config.paths.documentRoot));
|
|
61
|
+
}
|
|
55
62
|
parts.push("/* === engine/katex.css === */\n");
|
|
56
63
|
parts.push((await readKatexCss()).trimEnd());
|
|
57
64
|
parts.push("\n\n");
|
|
58
65
|
return parts.join("");
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
export async function writeComponentsCss(root, targetDir, config) {
|
|
68
|
+
export async function writeComponentsCss(root, targetDir, config, options = {}) {
|
|
62
69
|
config ??= await loadConfig(root);
|
|
63
|
-
const css = await buildComponentsCss(root, config);
|
|
70
|
+
const css = await buildComponentsCss(root, config, options);
|
|
64
71
|
await fs.mkdir(targetDir, { recursive: true });
|
|
65
72
|
await fs.writeFile(path.join(targetDir, "components.css"), css, "utf8");
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
export async function buildComponentsCss(root, config) {
|
|
75
|
+
export async function buildComponentsCss(root, config, options = {}) {
|
|
69
76
|
config ??= await loadConfig(root);
|
|
70
77
|
const parts = [];
|
|
71
78
|
await appendCssDirectory(parts, path.join(config.paths.themeDir, "patterns"), "theme/patterns");
|
|
72
|
-
|
|
79
|
+
const componentRoots = uniquePaths([
|
|
80
|
+
config.paths.componentsDir,
|
|
81
|
+
...(await discoverPressChildRoots(config.paths.documentRoot, "components")),
|
|
82
|
+
...(options.componentRoots ?? []),
|
|
83
|
+
]);
|
|
84
|
+
for (const componentsDir of componentRoots) {
|
|
85
|
+
await appendComponentScopedCss(parts, componentsDir, documentRelativeLabel(componentsDir, config.paths.documentRoot));
|
|
86
|
+
}
|
|
73
87
|
return parts.join("");
|
|
74
88
|
}
|
|
75
89
|
|
|
@@ -88,7 +102,7 @@ async function appendCssDirectory(parts, directory, labelPrefix, options = {}) {
|
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
async function appendComponentScopedCss(parts, componentsDir) {
|
|
105
|
+
async function appendComponentScopedCss(parts, componentsDir, labelPrefix = "components") {
|
|
92
106
|
let entries;
|
|
93
107
|
try {
|
|
94
108
|
entries = await fs.readdir(componentsDir, { withFileTypes: true });
|
|
@@ -107,8 +121,38 @@ async function appendComponentScopedCss(parts, componentsDir) {
|
|
|
107
121
|
}
|
|
108
122
|
throw error;
|
|
109
123
|
}
|
|
110
|
-
parts.push(`/* ===
|
|
124
|
+
parts.push(`/* === ${labelPrefix}/${entry.name}/style.css === */\n`);
|
|
111
125
|
parts.push(css.trimEnd());
|
|
112
126
|
parts.push("\n\n");
|
|
113
127
|
}
|
|
114
128
|
}
|
|
129
|
+
|
|
130
|
+
async function discoverPressChildRoots(documentRoot, childName) {
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = await fs.readdir(documentRoot, { withFileTypes: true });
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
return entries
|
|
138
|
+
.filter((entry) => entry.isDirectory() && entry.name !== "shared" && !entry.name.startsWith("."))
|
|
139
|
+
.map((entry) => path.join(documentRoot, entry.name, childName));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function uniquePaths(paths) {
|
|
143
|
+
const out = [];
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
for (const candidate of paths ?? []) {
|
|
146
|
+
if (!candidate) continue;
|
|
147
|
+
const normalized = path.resolve(candidate);
|
|
148
|
+
if (seen.has(normalized)) continue;
|
|
149
|
+
seen.add(normalized);
|
|
150
|
+
out.push(normalized);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function documentRelativeLabel(filePath, documentRoot) {
|
|
156
|
+
const relative = path.relative(documentRoot, filePath).split(path.sep).join("/");
|
|
157
|
+
return relative && !relative.startsWith("..") ? relative : path.basename(filePath);
|
|
158
|
+
}
|
|
@@ -5,6 +5,7 @@ import { buildReactStatic, startStaticServer } from "../commands/_shared.mjs";
|
|
|
5
5
|
import { walkFiles } from "./file-walk.mjs";
|
|
6
6
|
import { createIssue, createIssueReport } from "./issue-report.mjs";
|
|
7
7
|
import { collectActiveContentFiles, resolveActiveSourceWorkspace } from "./source-workspace.mjs";
|
|
8
|
+
import { collectSourceTextFiles } from "./source-text-tools.mjs";
|
|
8
9
|
|
|
9
10
|
const MEDIA_EXTENSIONS = new Set([".avif", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".webp"]);
|
|
10
11
|
const SOURCE_EXTENSIONS = new Set([".css", ".html", ".js", ".json", ".md", ".mjs", ".ts", ".tsx"]);
|
|
@@ -27,7 +28,7 @@ export async function inspectWorkspace({ root, config, options = {}, recurse = n
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
checked.push("media");
|
|
30
|
-
const mediaFiles = await readMediaFiles(sourceScan.config
|
|
31
|
+
const mediaFiles = await readMediaFiles(await collectInspectionMediaRoots(sourceScan.config));
|
|
31
32
|
const sourceText = sourceScan.sourceFiles.map((file) => file.text).join("\n");
|
|
32
33
|
const unusedMedia = mediaFiles.filter((file) => !sourceText.includes(file.name) && !sourceText.includes(file.relativePath));
|
|
33
34
|
unusedMedia.forEach((file) => {
|
|
@@ -99,11 +100,7 @@ export async function collectInspectionSources(config) {
|
|
|
99
100
|
const sourceWorkspace = await resolveActiveSourceWorkspace(config);
|
|
100
101
|
const sourceConfig = sourceWorkspace.config;
|
|
101
102
|
const contentFiles = await collectActiveContentFiles(sourceWorkspace);
|
|
102
|
-
const sourceFiles =
|
|
103
|
-
...contentFiles,
|
|
104
|
-
...await readSourceFiles(sourceConfig.paths.componentsDir),
|
|
105
|
-
...await readSingleFile(sourceConfig.paths.designDoc),
|
|
106
|
-
];
|
|
103
|
+
const sourceFiles = await collectSourceTextFiles(sourceConfig, { scope: "all" });
|
|
107
104
|
const componentUsage = summarizeComponentUsage(contentFiles);
|
|
108
105
|
|
|
109
106
|
return {
|
|
@@ -260,20 +257,48 @@ async function readSingleFile(absolutePath) {
|
|
|
260
257
|
}
|
|
261
258
|
}
|
|
262
259
|
|
|
263
|
-
async function readMediaFiles(
|
|
260
|
+
async function readMediaFiles(directories) {
|
|
264
261
|
const files = [];
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
262
|
+
for (const directory of directories) {
|
|
263
|
+
await walkFiles(directory, async (absolutePath) => {
|
|
264
|
+
if (!MEDIA_EXTENSIONS.has(path.extname(absolutePath).toLowerCase())) return;
|
|
265
|
+
files.push({
|
|
266
|
+
absolutePath,
|
|
267
|
+
name: path.basename(absolutePath),
|
|
268
|
+
relativePath: path.relative(directory, absolutePath).replaceAll("\\", "/"),
|
|
269
|
+
});
|
|
271
270
|
});
|
|
272
|
-
}
|
|
271
|
+
}
|
|
273
272
|
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
274
273
|
return files;
|
|
275
274
|
}
|
|
276
275
|
|
|
276
|
+
async function collectInspectionMediaRoots(config) {
|
|
277
|
+
const roots = [config.paths.mediaDir];
|
|
278
|
+
try {
|
|
279
|
+
const entries = await fs.readdir(config.paths.documentRoot, { withFileTypes: true });
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "shared") continue;
|
|
282
|
+
roots.push(path.join(config.paths.documentRoot, entry.name, "media"));
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Missing press/ is reported by validation/export.
|
|
286
|
+
}
|
|
287
|
+
return uniquePaths(roots);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function uniquePaths(paths) {
|
|
291
|
+
const out = [];
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
for (const candidate of paths) {
|
|
294
|
+
const normalized = path.resolve(candidate);
|
|
295
|
+
if (seen.has(normalized)) continue;
|
|
296
|
+
seen.add(normalized);
|
|
297
|
+
out.push(normalized);
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
277
302
|
function summarizeComponentUsage(contentFiles) {
|
|
278
303
|
const usages = new Map();
|
|
279
304
|
for (const file of contentFiles) {
|
|
@@ -373,7 +398,7 @@ function inspectionExpression() {
|
|
|
373
398
|
});
|
|
374
399
|
return {
|
|
375
400
|
pageNumber: index + 1,
|
|
376
|
-
title: page.getAttribute('data-page-title') || wrapper.getAttribute('aria-label') || '',
|
|
401
|
+
title: page.getAttribute('data-page-title') || page.getAttribute('data-openpress-frame-key') || wrapper.getAttribute('aria-label') || '',
|
|
377
402
|
source: sourcePath ? { file: sourceFile, path: sourcePath } : undefined,
|
|
378
403
|
overflows,
|
|
379
404
|
};
|
|
@@ -85,8 +85,8 @@ function pageGeometry({ id, label, width, height }) {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function cssLengthValue(value,
|
|
89
|
-
if (value == null) return
|
|
88
|
+
function cssLengthValue(value, defaultValue) {
|
|
89
|
+
if (value == null) return defaultValue ?? null;
|
|
90
90
|
if (typeof value !== "string" || !value.trim()) {
|
|
91
91
|
throw new Error("OpenPress page width/height must be CSS length strings.");
|
|
92
92
|
}
|
|
@@ -97,16 +97,16 @@ function cssLengthValue(value, fallback) {
|
|
|
97
97
|
return trimmed;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function optionalId(value,
|
|
101
|
-
if (value == null) return
|
|
100
|
+
function optionalId(value, defaultValue) {
|
|
101
|
+
if (value == null) return defaultValue;
|
|
102
102
|
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/i.test(value)) {
|
|
103
103
|
throw new Error("OpenPress page id must be a simple slug.");
|
|
104
104
|
}
|
|
105
105
|
return value;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function optionalLabel(value,
|
|
109
|
-
if (value == null) return
|
|
108
|
+
function optionalLabel(value, defaultValue) {
|
|
109
|
+
if (value == null) return defaultValue;
|
|
110
110
|
if (typeof value !== "string" || !value.trim()) {
|
|
111
111
|
throw new Error("OpenPress page label must be a non-empty string.");
|
|
112
112
|
}
|