@open-press/core 0.3.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/LICENSE +21 -0
- package/README.md +36 -0
- package/engine/chrome-pdf.d.mts +34 -0
- package/engine/chrome-pdf.mjs +344 -0
- package/engine/cli.mjs +93 -0
- package/engine/commands/_shared.mjs +170 -0
- package/engine/commands/deploy.mjs +31 -0
- package/engine/commands/dev.mjs +26 -0
- package/engine/commands/export.mjs +8 -0
- package/engine/commands/init.mjs +24 -0
- package/engine/commands/inspect.mjs +35 -0
- package/engine/commands/migrate-to-react.mjs +27 -0
- package/engine/commands/pdf.mjs +26 -0
- package/engine/commands/preview.mjs +26 -0
- package/engine/commands/render.mjs +17 -0
- package/engine/commands/replace.mjs +41 -0
- package/engine/commands/search.mjs +33 -0
- package/engine/commands/typecheck.mjs +5 -0
- package/engine/commands/validate.mjs +17 -0
- package/engine/config.d.mts +40 -0
- package/engine/config.mjs +160 -0
- package/engine/deploy-sync.mjs +15 -0
- package/engine/document-export.mjs +15 -0
- package/engine/file-utils.mjs +106 -0
- package/engine/fonts.mjs +62 -0
- package/engine/init.mjs +90 -0
- package/engine/inspection.mjs +348 -0
- package/engine/issue-report.mjs +44 -0
- package/engine/katex-assets.mjs +45 -0
- package/engine/page-block.mjs +30 -0
- package/engine/page-renderer.mjs +217 -0
- package/engine/pdf-media.mjs +45 -0
- package/engine/public-assets.mjs +19 -0
- package/engine/react/chapter-css.mjs +53 -0
- package/engine/react/comment-endpoint.d.mts +11 -0
- package/engine/react/comment-endpoint.mjs +128 -0
- package/engine/react/comment-marker.mjs +306 -0
- package/engine/react/document-entry.mjs +253 -0
- package/engine/react/document-export.mjs +392 -0
- package/engine/react/mdx-compile.mjs +295 -0
- package/engine/react/measurement-css.mjs +44 -0
- package/engine/react/migrate-to-react.mjs +355 -0
- package/engine/react/pagination-constants.mjs +3 -0
- package/engine/react/pagination.mjs +121 -0
- package/engine/react/project-asset-endpoint.d.mts +10 -0
- package/engine/react/project-asset-endpoint.mjs +379 -0
- package/engine/react/workspace-discovery.mjs +156 -0
- package/engine/source-text-tools.mjs +280 -0
- package/engine/source-workspace.mjs +76 -0
- package/engine/static-server.mjs +493 -0
- package/engine/validation.mjs +172 -0
- package/index.html +13 -0
- package/package.json +86 -0
- package/src/openpress/App.tsx +127 -0
- package/src/openpress/composerMentions.ts +188 -0
- package/src/openpress/core/basePages.tsx +87 -0
- package/src/openpress/core/index.tsx +20 -0
- package/src/openpress/core/types.ts +71 -0
- package/src/openpress/frameScheduler.ts +32 -0
- package/src/openpress/indexes.ts +329 -0
- package/src/openpress/inspector.ts +282 -0
- package/src/openpress/pageRoute.ts +21 -0
- package/src/openpress/pagination.ts +845 -0
- package/src/openpress/projectIdentity.ts +15 -0
- package/src/openpress/projectSources.ts +24 -0
- package/src/openpress/projectWorkspace.tsx +919 -0
- package/src/openpress/publicPage.tsx +469 -0
- package/src/openpress/reactDocumentMetadata.ts +41 -0
- package/src/openpress/readerPageRegistry.ts +41 -0
- package/src/openpress/readerRuntime.ts +230 -0
- package/src/openpress/readerScroll.ts +92 -0
- package/src/openpress/readerState.ts +15 -0
- package/src/openpress/renderer.tsx +91 -0
- package/src/openpress/runtimeMode.ts +22 -0
- package/src/openpress/types.ts +112 -0
- package/src/openpress/workbench.tsx +1299 -0
- package/src/openpress/workbenchPanels.tsx +122 -0
- package/src/openpress/workbenchTypes.ts +4 -0
- package/src/styles/openpress/app-shell.css +251 -0
- package/src/styles/openpress/media-workspace.css +230 -0
- package/src/styles/openpress/print-route.css +186 -0
- package/src/styles/openpress/project-workspace.css +1318 -0
- package/src/styles/openpress/public-viewer.css +983 -0
- package/src/styles/openpress/reader-runtime.css +792 -0
- package/src/styles/openpress/responsive.css +384 -0
- package/src/styles/openpress/workbench-panels.css +558 -0
- package/src/styles/openpress/workbench.css +720 -0
- package/src/styles/openpress.css +14 -0
- package/tsconfig.json +37 -0
- package/vite.config.ts +512 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveActiveSourceWorkspace } from "./source-workspace.mjs";
|
|
4
|
+
|
|
5
|
+
const MARKDOWN_EXTENSIONS = new Set([".md"]);
|
|
6
|
+
const ALL_SOURCE_EXTENSIONS = new Set([".css", ".html", ".js", ".json", ".md", ".mdx", ".mjs", ".ts", ".tsx"]);
|
|
7
|
+
const REACT_IMPLEMENTATION_EXTENSIONS = new Set([".css", ".html", ".js", ".json", ".mjs", ".ts", ".tsx"]);
|
|
8
|
+
|
|
9
|
+
export async function searchSourceText({ config, query, scope = "content", caseSensitive = false }) {
|
|
10
|
+
const files = await collectSourceTextFiles(config, { scope });
|
|
11
|
+
const rawMatches = [];
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
rawMatches.push(...findLiteralMatches(file.text, query, { caseSensitive }).map((match) => ({
|
|
14
|
+
...match,
|
|
15
|
+
scope: file.scope,
|
|
16
|
+
file: file.name,
|
|
17
|
+
path: file.relativePath,
|
|
18
|
+
})));
|
|
19
|
+
}
|
|
20
|
+
const matches = rawMatches.map((match, index) => ({
|
|
21
|
+
...match,
|
|
22
|
+
id: `match-${String(index + 1).padStart(4, "0")}`,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
kind: "search",
|
|
27
|
+
query,
|
|
28
|
+
scope,
|
|
29
|
+
caseSensitive,
|
|
30
|
+
matchCount: matches.length,
|
|
31
|
+
files: summarizeFiles(matches),
|
|
32
|
+
matches,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function replaceSourceText({
|
|
37
|
+
config,
|
|
38
|
+
from,
|
|
39
|
+
to,
|
|
40
|
+
scope = "content",
|
|
41
|
+
caseSensitive = false,
|
|
42
|
+
includeCode = false,
|
|
43
|
+
apply = false,
|
|
44
|
+
}) {
|
|
45
|
+
const files = await collectSourceTextFiles(config, { scope });
|
|
46
|
+
const changes = [];
|
|
47
|
+
let matchCount = 0;
|
|
48
|
+
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
const result = replaceLiteralMatches(file.text, from, to, { caseSensitive, includeCode });
|
|
51
|
+
if (result.replacements.length === 0) continue;
|
|
52
|
+
matchCount += result.replacements.length;
|
|
53
|
+
changes.push({
|
|
54
|
+
scope: file.scope,
|
|
55
|
+
file: file.name,
|
|
56
|
+
path: file.relativePath,
|
|
57
|
+
absolutePath: file.absolutePath,
|
|
58
|
+
replacements: result.replacements.map((replacement, index) => ({
|
|
59
|
+
...replacement,
|
|
60
|
+
id: `replace-${String(matchCount - result.replacements.length + index + 1).padStart(4, "0")}`,
|
|
61
|
+
})),
|
|
62
|
+
});
|
|
63
|
+
if (apply) {
|
|
64
|
+
await fs.writeFile(file.absolutePath, result.text, "utf8");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
kind: "replace",
|
|
70
|
+
from,
|
|
71
|
+
to,
|
|
72
|
+
scope,
|
|
73
|
+
caseSensitive,
|
|
74
|
+
includeCode,
|
|
75
|
+
applied: apply,
|
|
76
|
+
matchCount,
|
|
77
|
+
fileCount: changes.length,
|
|
78
|
+
changes,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function collectSourceTextFiles(config, { scope = "content" } = {}) {
|
|
83
|
+
const roots = await sourceRoots(config, scope);
|
|
84
|
+
const files = [];
|
|
85
|
+
for (const rootInfo of roots) {
|
|
86
|
+
const visit = async (absolutePath) => {
|
|
87
|
+
const extension = path.extname(absolutePath);
|
|
88
|
+
if (!rootInfo.extensions.has(extension)) return;
|
|
89
|
+
const relativePath = path.relative(config.root, absolutePath).replaceAll("\\", "/");
|
|
90
|
+
files.push({
|
|
91
|
+
scope: rootInfo.scope,
|
|
92
|
+
name: path.basename(absolutePath),
|
|
93
|
+
absolutePath,
|
|
94
|
+
relativePath,
|
|
95
|
+
text: await fs.readFile(absolutePath, "utf8"),
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
if (rootInfo.kind === "file") {
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(rootInfo.absolutePath);
|
|
101
|
+
await visit(rootInfo.absolutePath);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error?.code !== "ENOENT") throw error;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
await walkFiles(rootInfo.absolutePath, visit);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
110
|
+
return files;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function findLiteralMatches(text, query, { caseSensitive = false } = {}) {
|
|
114
|
+
if (!query) return [];
|
|
115
|
+
const matches = [];
|
|
116
|
+
forEachLine(text, ({ line, lineNumber, lineOffset }) => {
|
|
117
|
+
for (const range of findLineMatches(line, query, { caseSensitive })) {
|
|
118
|
+
matches.push({
|
|
119
|
+
line: lineNumber,
|
|
120
|
+
column: range.start + 1,
|
|
121
|
+
index: lineOffset + range.start,
|
|
122
|
+
text: line.slice(range.start, range.end),
|
|
123
|
+
preview: previewLine(line, range.start, range.end),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return matches;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function replaceLiteralMatches(text, from, to, { caseSensitive = false, includeCode = false } = {}) {
|
|
131
|
+
if (!from) return { text, replacements: [] };
|
|
132
|
+
let output = "";
|
|
133
|
+
const replacements = [];
|
|
134
|
+
let inFence = false;
|
|
135
|
+
|
|
136
|
+
forEachLine(text, ({ line, ending, lineNumber, lineOffset }) => {
|
|
137
|
+
const fenceLine = isFenceLine(line);
|
|
138
|
+
const replaceLine = includeCode || (!inFence && !fenceLine);
|
|
139
|
+
if (!replaceLine) {
|
|
140
|
+
output += line + ending;
|
|
141
|
+
if (fenceLine) inFence = !inFence;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const ranges = findLineMatches(line, from, { caseSensitive });
|
|
146
|
+
if (ranges.length === 0) {
|
|
147
|
+
output += line + ending;
|
|
148
|
+
if (fenceLine) inFence = !inFence;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let nextLine = "";
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
for (const range of ranges) {
|
|
155
|
+
nextLine += line.slice(cursor, range.start) + to;
|
|
156
|
+
replacements.push({
|
|
157
|
+
line: lineNumber,
|
|
158
|
+
column: range.start + 1,
|
|
159
|
+
index: lineOffset + range.start,
|
|
160
|
+
before: line,
|
|
161
|
+
after: replaceRangesInLine(line, ranges, to),
|
|
162
|
+
text: line.slice(range.start, range.end),
|
|
163
|
+
replacement: to,
|
|
164
|
+
previewBefore: previewLine(line, range.start, range.end),
|
|
165
|
+
previewAfter: previewLine(replaceRangesInLine(line, ranges, to), range.start, range.start + to.length),
|
|
166
|
+
});
|
|
167
|
+
cursor = range.end;
|
|
168
|
+
}
|
|
169
|
+
nextLine += line.slice(cursor);
|
|
170
|
+
output += nextLine + ending;
|
|
171
|
+
if (fenceLine) inFence = !inFence;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return { text: output, replacements };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function sourceRoots(config, scope) {
|
|
178
|
+
const sourceWorkspace = await resolveActiveSourceWorkspace(config);
|
|
179
|
+
const sourceConfig = sourceWorkspace.config;
|
|
180
|
+
const contentRoot = {
|
|
181
|
+
scope: "content",
|
|
182
|
+
kind: "dir",
|
|
183
|
+
absolutePath: sourceWorkspace.sourceDir,
|
|
184
|
+
extensions: sourceWorkspace.contentExtensions,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (scope === "all") {
|
|
188
|
+
const roots = [
|
|
189
|
+
contentRoot,
|
|
190
|
+
{ scope: "design-doc", kind: "file", absolutePath: sourceConfig.paths.designDoc, extensions: MARKDOWN_EXTENSIONS },
|
|
191
|
+
{ scope: "components", kind: "dir", absolutePath: sourceConfig.paths.componentsDir, extensions: ALL_SOURCE_EXTENSIONS },
|
|
192
|
+
{ scope: "document-entry", kind: "file", absolutePath: sourceWorkspace.entryPath, extensions: REACT_IMPLEMENTATION_EXTENSIONS },
|
|
193
|
+
{ scope: "chapters", kind: "dir", absolutePath: sourceWorkspace.sourceDir, extensions: REACT_IMPLEMENTATION_EXTENSIONS },
|
|
194
|
+
];
|
|
195
|
+
return roots;
|
|
196
|
+
}
|
|
197
|
+
return [contentRoot];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function walkFiles(directory, visit) {
|
|
201
|
+
let entries;
|
|
202
|
+
try {
|
|
203
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error?.code === "ENOENT") return;
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (entry.name.startsWith(".")) continue;
|
|
210
|
+
const absolutePath = path.join(directory, entry.name);
|
|
211
|
+
if (entry.isDirectory()) await walkFiles(absolutePath, visit);
|
|
212
|
+
else if (entry.isFile()) await visit(absolutePath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function forEachLine(text, visit) {
|
|
217
|
+
const lineRe = /([^\r\n]*)(\r\n|\n|\r|$)/g;
|
|
218
|
+
let lineNumber = 1;
|
|
219
|
+
let offset = 0;
|
|
220
|
+
let match;
|
|
221
|
+
while ((match = lineRe.exec(text))) {
|
|
222
|
+
const [full, line, ending] = match;
|
|
223
|
+
if (full === "") break;
|
|
224
|
+
visit({ line, ending, lineNumber, lineOffset: offset });
|
|
225
|
+
offset += full.length;
|
|
226
|
+
lineNumber += 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function findLineMatches(line, query, { caseSensitive }) {
|
|
231
|
+
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
232
|
+
const needle = caseSensitive ? query : query.toLowerCase();
|
|
233
|
+
const ranges = [];
|
|
234
|
+
let cursor = 0;
|
|
235
|
+
while (needle && cursor <= haystack.length) {
|
|
236
|
+
const start = haystack.indexOf(needle, cursor);
|
|
237
|
+
if (start < 0) break;
|
|
238
|
+
const end = start + needle.length;
|
|
239
|
+
ranges.push({ start, end });
|
|
240
|
+
cursor = end;
|
|
241
|
+
}
|
|
242
|
+
return ranges;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function replaceRangesInLine(line, ranges, replacement) {
|
|
246
|
+
let output = "";
|
|
247
|
+
let cursor = 0;
|
|
248
|
+
for (const range of ranges) {
|
|
249
|
+
output += line.slice(cursor, range.start) + replacement;
|
|
250
|
+
cursor = range.end;
|
|
251
|
+
}
|
|
252
|
+
return output + line.slice(cursor);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function previewLine(line, start, end) {
|
|
256
|
+
const previewStart = Math.max(0, start - 40);
|
|
257
|
+
const previewEnd = Math.min(line.length, end + 40);
|
|
258
|
+
const prefix = previewStart > 0 ? "..." : "";
|
|
259
|
+
const suffix = previewEnd < line.length ? "..." : "";
|
|
260
|
+
return `${prefix}${line.slice(previewStart, previewEnd)}${suffix}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function summarizeFiles(matches) {
|
|
264
|
+
const summaries = new Map();
|
|
265
|
+
for (const match of matches) {
|
|
266
|
+
const current = summaries.get(match.path) ?? {
|
|
267
|
+
scope: match.scope,
|
|
268
|
+
file: match.file,
|
|
269
|
+
path: match.path,
|
|
270
|
+
matchCount: 0,
|
|
271
|
+
};
|
|
272
|
+
current.matchCount += 1;
|
|
273
|
+
summaries.set(match.path, current);
|
|
274
|
+
}
|
|
275
|
+
return Array.from(summaries.values());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isFenceLine(line) {
|
|
279
|
+
return /^\s*(```|~~~)/.test(line);
|
|
280
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadReactDocumentEntry } from "./react/document-entry.mjs";
|
|
4
|
+
|
|
5
|
+
export const REACT_MDX_CONTENT_EXTENSIONS = new Set([".mdx"]);
|
|
6
|
+
|
|
7
|
+
export async function resolveActiveSourceWorkspace(config) {
|
|
8
|
+
const reactEntry = await loadReactDocumentEntry(config.root);
|
|
9
|
+
if (!reactEntry) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"React/MDX document entry not found. Expected document/index.tsx; run `node engine/cli.mjs migrate-to-react .` before using workspace source tools.",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
kind: "react-mdx",
|
|
17
|
+
checkedName: "react-source",
|
|
18
|
+
config: reactEntry.config,
|
|
19
|
+
entryPath: reactEntry.entryPath,
|
|
20
|
+
sourceDir: reactEntry.config.paths.sourceDir,
|
|
21
|
+
contentExtensions: REACT_MDX_CONTENT_EXTENSIONS,
|
|
22
|
+
contentLabel: "React MDX chapter source",
|
|
23
|
+
missingCode: "react-source.missing",
|
|
24
|
+
emptyCode: "react-source.empty",
|
|
25
|
+
missingMessage: `React chapter source directory does not exist yet; create ${reactEntry.config.sourceDir}/ before running export.`,
|
|
26
|
+
emptyMessage: "React chapter source directory has no `*.mdx` files; the document will export with zero chapter pages.",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function collectActiveContentFiles(sourceWorkspace, { skipUnderscoreFiles = false } = {}) {
|
|
31
|
+
const files = [];
|
|
32
|
+
await walkFiles(sourceWorkspace.sourceDir, async (absolutePath) => {
|
|
33
|
+
if (!sourceWorkspace.contentExtensions.has(path.extname(absolutePath))) return;
|
|
34
|
+
const name = path.basename(absolutePath);
|
|
35
|
+
if (skipUnderscoreFiles && name.startsWith("_")) return;
|
|
36
|
+
files.push({
|
|
37
|
+
absolutePath,
|
|
38
|
+
name,
|
|
39
|
+
relativePath: rootRelativePath(sourceWorkspace.config, absolutePath),
|
|
40
|
+
sourceRelativePath: path.relative(sourceWorkspace.sourceDir, absolutePath).replaceAll("\\", "/"),
|
|
41
|
+
text: await fs.readFile(absolutePath, "utf8"),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function sourceDirectoryExists(sourceWorkspace) {
|
|
49
|
+
try {
|
|
50
|
+
const stat = await fs.stat(sourceWorkspace.sourceDir);
|
|
51
|
+
return stat.isDirectory();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error?.code === "ENOENT") return false;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function rootRelativePath(config, absolutePath) {
|
|
59
|
+
return path.relative(config.root, absolutePath).replaceAll("\\", "/");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function walkFiles(directory, visit) {
|
|
63
|
+
let entries;
|
|
64
|
+
try {
|
|
65
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error?.code === "ENOENT") return;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (entry.name.startsWith(".")) continue;
|
|
72
|
+
const absolutePath = path.join(directory, entry.name);
|
|
73
|
+
if (entry.isDirectory()) await walkFiles(absolutePath, visit);
|
|
74
|
+
else if (entry.isFile()) await visit(absolutePath);
|
|
75
|
+
}
|
|
76
|
+
}
|