@open-press/cli 0.5.0 → 0.7.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.
Files changed (113) hide show
  1. package/dist/cli.js +114 -23
  2. package/package.json +1 -1
  3. package/template/core/CHANGELOG.md +97 -1
  4. package/template/core/README.md +9 -5
  5. package/template/core/engine/cli.mjs +2 -5
  6. package/template/core/engine/commands/_shared.mjs +4 -4
  7. package/template/core/engine/commands/deploy.mjs +1 -1
  8. package/template/core/engine/commands/inspect.mjs +3 -3
  9. package/template/core/engine/commands/replace.mjs +1 -1
  10. package/template/core/engine/commands/search.mjs +1 -1
  11. package/template/core/engine/commands/validate.mjs +2 -2
  12. package/template/core/engine/document-export.mjs +1 -1
  13. package/template/core/engine/{chrome-pdf.mjs → output/chrome-pdf.mjs} +1 -2
  14. package/template/core/engine/{deploy-sync.mjs → output/deploy-sync.mjs} +2 -2
  15. package/template/core/engine/{fonts.mjs → output/fonts.mjs} +1 -1
  16. package/template/core/engine/{public-assets.mjs → output/public-assets.mjs} +2 -2
  17. package/template/core/engine/{static-server.mjs → output/static-server.mjs} +2 -2
  18. package/template/core/engine/react/caption-numbering.mjs +73 -0
  19. package/template/core/engine/react/comment-marker.mjs +54 -10
  20. package/template/core/engine/react/document-entry.mjs +124 -64
  21. package/template/core/engine/react/document-export.mjs +252 -311
  22. package/template/core/engine/react/mdx-compile.mjs +123 -3
  23. package/template/core/engine/react/measurement-css.mjs +3 -3
  24. package/template/core/engine/react/pagination/allocator.mjs +122 -0
  25. package/template/core/engine/react/pagination/regions.mjs +81 -0
  26. package/template/core/engine/react/pagination.mjs +9 -121
  27. package/template/core/engine/react/pipeline/allocate.mjs +248 -0
  28. package/template/core/engine/react/pipeline/final-render.mjs +94 -0
  29. package/template/core/engine/react/pipeline/frame-measurement.mjs +271 -0
  30. package/template/core/engine/react/pipeline/press-tree.mjs +135 -0
  31. package/template/core/engine/react/project-asset-endpoint.mjs +2 -2
  32. package/template/core/engine/react/{chapter-css.mjs → section-css.mjs} +12 -9
  33. package/template/core/engine/react/sources/heading-numbering.mjs +132 -0
  34. package/template/core/engine/react/sources/mdx-resolver.mjs +441 -0
  35. package/template/core/engine/react/{workspace-discovery.mjs → style-discovery.mjs} +29 -40
  36. package/template/core/engine/{config.mjs → runtime/config.mjs} +15 -0
  37. package/template/core/engine/{file-utils.mjs → runtime/file-utils.mjs} +1 -1
  38. package/template/core/engine/{inspection.mjs → runtime/inspection.mjs} +3 -4
  39. package/template/core/engine/{source-text-tools.mjs → runtime/source-text-tools.mjs} +24 -7
  40. package/template/core/engine/runtime/source-workspace.mjs +186 -0
  41. package/template/core/engine/{validation.mjs → runtime/validation.mjs} +19 -17
  42. package/template/core/package.json +5 -2
  43. package/template/core/src/openpress/anchorMap.ts +27 -0
  44. package/template/core/src/openpress/core/Frame.tsx +80 -0
  45. package/template/core/src/openpress/core/FrameContext.tsx +19 -0
  46. package/template/core/src/openpress/core/MdxArea.tsx +35 -0
  47. package/template/core/src/openpress/core/Press.tsx +34 -0
  48. package/template/core/src/openpress/core/index.tsx +34 -15
  49. package/template/core/src/openpress/core/primitives.tsx +23 -0
  50. package/template/core/src/openpress/core/types.ts +131 -19
  51. package/template/core/src/openpress/core/useSource.ts +28 -0
  52. package/template/core/src/openpress/manuscript/index.tsx +196 -0
  53. package/template/core/src/openpress/mdx/index.ts +88 -0
  54. package/template/core/src/openpress/numbering/index.ts +294 -0
  55. package/template/core/src/openpress/publicPage.tsx +4 -186
  56. package/template/core/src/openpress/reactDocumentMetadata.ts +2 -16
  57. package/template/core/src/openpress/types.ts +0 -16
  58. package/template/core/src/openpress/workbench.tsx +2 -36
  59. package/template/core/src/styles/openpress/responsive.css +0 -14
  60. package/template/core/tsconfig.json +4 -1
  61. package/template/core/vite.config.ts +10 -3
  62. package/template/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +21 -0
  63. package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +30 -0
  64. package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +29 -0
  65. package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +12 -0
  66. package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +27 -0
  67. package/template/packs/academic-paper/document/components/ChapterOpenerVisual/index.tsx +76 -0
  68. package/template/packs/academic-paper/document/components/Page.tsx +37 -0
  69. package/template/packs/academic-paper/document/components/TokenSwatchGrid/index.tsx +46 -0
  70. package/template/packs/academic-paper/document/components/TokenSwatchGrid/style.css +63 -0
  71. package/template/packs/academic-paper/document/components/TypeSpecimen/index.tsx +38 -0
  72. package/template/packs/academic-paper/document/components/TypeSpecimen/style.css +111 -0
  73. package/template/packs/academic-paper/document/design.md +279 -0
  74. package/template/packs/academic-paper/document/index.tsx +131 -0
  75. package/template/packs/academic-paper/document/media/README.md +13 -0
  76. package/template/packs/academic-paper/document/openpress.config.mjs +26 -0
  77. package/template/packs/academic-paper/document/theme/README.md +11 -0
  78. package/template/packs/academic-paper/document/theme/base/page-contract.css +505 -0
  79. package/template/packs/academic-paper/document/theme/base/print.css +93 -0
  80. package/template/packs/academic-paper/document/theme/base/typography.css +336 -0
  81. package/template/packs/academic-paper/document/theme/fonts.css +3 -0
  82. package/template/packs/academic-paper/document/theme/page-surfaces/back-cover.css +43 -0
  83. package/template/packs/academic-paper/document/theme/page-surfaces/chapter-opener.css +205 -0
  84. package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +267 -0
  85. package/template/packs/academic-paper/document/theme/page-surfaces/toc.css +149 -0
  86. package/template/packs/academic-paper/document/theme/patterns/_chart-frame.css +49 -0
  87. package/template/packs/academic-paper/document/theme/patterns/figure-grid.css +68 -0
  88. package/template/packs/academic-paper/document/theme/patterns/table-utilities.css +66 -0
  89. package/template/packs/academic-paper/document/theme/shell/reader-controls.css +761 -0
  90. package/template/packs/academic-paper/document/theme/tokens.css +80 -0
  91. package/template/packs/academic-paper/openpress.config.mjs +5 -0
  92. package/template/packs/claude-document/document/components/Page.tsx +24 -14
  93. package/template/packs/claude-document/document/design.md +2 -2
  94. package/template/packs/claude-document/document/index.tsx +67 -62
  95. package/template/packs/claude-document/document/theme/page-surfaces/toc.css +19 -7
  96. package/template/packs/editorial-monograph/document/components/Page.tsx +24 -14
  97. package/template/packs/editorial-monograph/document/design.md +2 -2
  98. package/template/packs/editorial-monograph/document/index.tsx +71 -47
  99. package/template/packs/editorial-monograph/document/theme/page-surfaces/toc.css +19 -9
  100. package/template/core/engine/commands/migrate-to-react.mjs +0 -27
  101. package/template/core/engine/page-renderer.mjs +0 -217
  102. package/template/core/engine/react/migrate-to-react.mjs +0 -355
  103. package/template/core/engine/source-workspace.mjs +0 -76
  104. package/template/core/src/openpress/core/basePages.tsx +0 -87
  105. package/template/core/src/openpress/pagination.ts +0 -845
  106. package/template/packs/claude-document/document/chapters/01-document-shape/chapter.tsx +0 -30
  107. package/template/packs/claude-document/document/chapters/02-review-loop/chapter.tsx +0 -30
  108. /package/template/core/engine/{chrome-pdf.d.mts → output/chrome-pdf.d.mts} +0 -0
  109. /package/template/core/engine/{katex-assets.mjs → output/katex-assets.mjs} +0 -0
  110. /package/template/core/engine/{page-block.mjs → output/page-block.mjs} +0 -0
  111. /package/template/core/engine/{pdf-media.mjs → output/pdf-media.mjs} +0 -0
  112. /package/template/core/engine/{config.d.mts → runtime/config.d.mts} +0 -0
  113. /package/template/core/engine/{issue-report.mjs → runtime/issue-report.mjs} +0 -0
@@ -1,217 +0,0 @@
1
- const TOC_ENTRIES_PER_PAGE = 24;
2
-
3
- function renderPageShell(sectionClass, bodyHtml, attrs = "", { kind, footer = true } = {}) {
4
- const className = footer === false ? addClass(sectionClass, "no-footer") : sectionClass;
5
- const attrsPart = pageAttrs(attrs, { kind, footer });
6
- const footerHtml = footer === false ? "" : `
7
- <footer class="page-footer" aria-hidden="true"></footer>`;
8
- return `<section class="${className}"${attrsPart}>
9
- <div class="page-frame">
10
- <header class="page-header" aria-hidden="true"></header>
11
- <main class="page-body">
12
- ${bodyHtml.trim()}
13
- </main>${footerHtml}
14
- </div>
15
- </section>`;
16
- }
17
-
18
- export function renderToc({ title, items, className } = {}) {
19
- const headingText = typeof title === "string" && title.trim() ? title.trim() : "Contents";
20
- const tocItems = Array.isArray(items) ? items : [];
21
- const tocChunks = tocItems.length > 0 ? chunkArray(tocItems, TOC_ENTRIES_PER_PAGE) : [[]];
22
-
23
- return tocChunks.map((chunk, pageIndex) => {
24
- const isContinuation = pageIndex > 0;
25
- const pageId = pageIndex === 0 ? "toc" : `toc-${String(pageIndex + 1).padStart(2, "0")}`;
26
- const headingId = pageIndex === 0 ? "toc-title" : `${pageId}-title`;
27
- const pageHeadingText = isContinuation ? tocContinuationTitle(headingText) : headingText;
28
- const headingClass = isContinuation ? ` class="toc-heading toc-heading--continuation"` : ` class="toc-heading"`;
29
- const tocList = chunk.length > 0
30
- ? `
31
- <ol class="toc-list">
32
- ${chunk.map((item, index) => {
33
- const level = item.level === 3 ? 3 : 2;
34
- const absoluteIndex = pageIndex * TOC_ENTRIES_PER_PAGE + index;
35
- const label = item.label || (level === 2 ? `#${absoluteIndex + 1}` : "");
36
- const targetPageIndex = Math.max(0, Number(item.pageNumber || 1) - 1);
37
- return ` <li class="toc-level-${level}"><a href="#${escapeAttr(item.id)}" data-openpress-anchor="${escapeAttr(item.id)}" data-openpress-target-page-index="${targetPageIndex}"><span class="toc-index" data-toc-index="${escapeAttr(label)}">${escapeHtml(label)}</span><span class="toc-title">${escapeHtml(item.title)}</span><span class="toc-page">${String(item.pageNumber).padStart(2, "0")}</span></a></li>`;
38
- }).join("\n")}
39
- </ol>
40
- `
41
- : "";
42
- return renderPageShell(
43
- tocPageClassName(className, isContinuation),
44
- `
45
- <h2 id="${headingId}"${headingClass}>${escapeHtml(pageHeadingText)}</h2>
46
- ${tocList}
47
- `,
48
- [
49
- `id="${pageId}"`,
50
- `data-page-title="${escapeAttr(headingText)}"`,
51
- `data-toc-continuation="${isContinuation ? "true" : "false"}"`,
52
- `aria-labelledby="${headingId}"`,
53
- ].filter(Boolean).join(" "),
54
- { kind: "toc", footer: false },
55
- );
56
- }).join("\n\n");
57
- }
58
-
59
- function tocContinuationTitle(title) {
60
- return title === "目錄" ? "目錄續" : `${title} continued`;
61
- }
62
-
63
- function pageAttrs(attrs, { kind, footer } = {}) {
64
- const parts = [];
65
- if (attrs.trim()) parts.push(attrs.trim());
66
- if (kind && !hasAttr(attrs, "data-page-kind")) parts.push(`data-page-kind="${escapeAttr(kind)}"`);
67
- if (footer === false && !hasAttr(attrs, "data-page-footer")) parts.push('data-page-footer="false"');
68
- return parts.length ? ` ${parts.join(" ")}` : "";
69
- }
70
-
71
- function hasAttr(attrs, name) {
72
- return new RegExp(`\\b${name}=`).test(attrs);
73
- }
74
-
75
- function addClass(className, extraClass) {
76
- const classes = className.split(/\s+/).filter(Boolean);
77
- if (!classes.includes(extraClass)) classes.push(extraClass);
78
- return classes.join(" ");
79
- }
80
-
81
- export function injectStaticToc(pages) {
82
- const tocItems = collectTocItems(pages);
83
- if (tocItems.length === 0) return pages;
84
- const tocIndex = pages.findIndex((page) => hasPageKind(page.match(/^<section[^>]*>/i)?.[0] ?? "", "toc"));
85
- const tocPageCount = Math.max(1, Math.ceil(tocItems.length / TOC_ENTRIES_PER_PAGE));
86
- const tocPageNumber = tocIndex + 1;
87
- const adjustedTocItems = tocPageCount > 1 && tocIndex >= 0
88
- ? tocItems.map((item) => ({
89
- ...item,
90
- pageNumber: item.pageNumber > tocPageNumber ? item.pageNumber + tocPageCount - 1 : item.pageNumber,
91
- }))
92
- : tocItems;
93
-
94
- return pages.map((page) => {
95
- const openingTag = page.match(/^<section[^>]*>/i)?.[0] ?? "";
96
- if (!hasPageKind(openingTag, "toc")) return page;
97
- const title = extractAttr(openingTag, "data-page-title");
98
- return renderToc({ title, items: adjustedTocItems, className: extractAttr(openingTag, "class") });
99
- });
100
- }
101
-
102
- function tocPageClassName(className, isContinuation) {
103
- const classes = new Set(String(className || "reader-page reader-page--toc").split(/\s+/).filter(Boolean));
104
- classes.delete("toc");
105
- classes.add("reader-page");
106
- classes.add("reader-page--toc");
107
- if (isContinuation) classes.add("toc-continuation");
108
- else classes.delete("toc-continuation");
109
- return [...classes].join(" ");
110
- }
111
-
112
- function extractAttr(openingTag, name) {
113
- const re = new RegExp(`${name}="([^"]*)"`);
114
- return openingTag.match(re)?.[1];
115
- }
116
-
117
- function collectTocItems(pages) {
118
- const items = [];
119
- let chapterIndex = 0;
120
- let sectionIndex = 0;
121
- let pendingChapterOpener;
122
-
123
- pages.forEach((page, index) => {
124
- const openingTag = page.match(/^<section[^>]*>/i)?.[0] ?? "";
125
- if (hasPageKind(openingTag, "chapter-opener")) {
126
- pendingChapterOpener = extractChapterOpenerTarget(page, index);
127
- return;
128
- }
129
-
130
- if (!hasContentPageKind(openingTag)) return;
131
-
132
- let pageStartedChapter = false;
133
- const headings = [...page.matchAll(/<h([23])\b[^>]*\bid="([^"]+)"[^>]*>([\s\S]*?)<\/h\1>/gi)];
134
- headings.forEach((heading) => {
135
- const level = Number(heading[1]);
136
- if (level === 2) {
137
- const opener = pendingChapterOpener;
138
- pendingChapterOpener = undefined;
139
- pageStartedChapter = true;
140
- chapterIndex += 1;
141
- sectionIndex = 0;
142
- items.push({
143
- id: opener?.id ?? heading[2],
144
- title: htmlToText(heading[3]),
145
- pageNumber: opener?.pageNumber ?? index + 1,
146
- level: 2,
147
- label: `#${chapterIndex}`,
148
- });
149
- return;
150
- }
151
-
152
- if (level === 3 && chapterIndex > 0) {
153
- sectionIndex += 1;
154
- items.push({
155
- id: heading[2],
156
- title: htmlToText(heading[3]),
157
- pageNumber: index + 1,
158
- level: 3,
159
- label: `${chapterIndex}.${sectionIndex}`,
160
- });
161
- }
162
- });
163
- if (!pageStartedChapter) pendingChapterOpener = undefined;
164
- });
165
- return items;
166
- }
167
-
168
- function extractChapterOpenerTarget(page, index) {
169
- const heading = page.match(/<h2\b[^>]*\bid="([^"]+)"[^>]*>([\s\S]*?)<\/h2>/i);
170
- if (!heading?.[1]) return undefined;
171
- return {
172
- id: heading[1],
173
- pageNumber: index + 1,
174
- };
175
- }
176
-
177
- function htmlToText(html) {
178
- return html
179
- .replace(/<[^>]+>/g, "")
180
- .replaceAll("&nbsp;", " ")
181
- .replaceAll("&amp;", "&")
182
- .replaceAll("&lt;", "<")
183
- .replaceAll("&gt;", ">")
184
- .replaceAll("&quot;", '"')
185
- .trim();
186
- }
187
-
188
- function hasPageKind(openingTag, kind) {
189
- return extractAttr(openingTag, "data-page-kind") === kind;
190
- }
191
-
192
- function hasContentPageKind(openingTag) {
193
- return extractAttr(openingTag, "data-page-kind") === "content";
194
- }
195
-
196
- function escapeAttr(value) {
197
- return String(value)
198
- .replaceAll("&", "&amp;")
199
- .replaceAll('"', "&quot;")
200
- .replaceAll("<", "&lt;")
201
- .replaceAll(">", "&gt;");
202
- }
203
-
204
- function escapeHtml(value) {
205
- return String(value)
206
- .replaceAll("&", "&amp;")
207
- .replaceAll("<", "&lt;")
208
- .replaceAll(">", "&gt;");
209
- }
210
-
211
- function chunkArray(items, size) {
212
- const chunks = [];
213
- for (let index = 0; index < items.length; index += size) {
214
- chunks.push(items.slice(index, index + size));
215
- }
216
- return chunks;
217
- }
@@ -1,355 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import yaml from "js-yaml";
4
-
5
- const SHELL_KINDS = new Set(["cover", "toc", "back-cover"]);
6
-
7
- export async function migrateLegacyWorkspaceToReact(root, config, { dryRun = false, force = false } = {}) {
8
- const migration = await planLegacyWorkspaceMigration(root, config, { force });
9
- if (!dryRun) {
10
- await applyMigrationPlan(migration, { force });
11
- }
12
- return {
13
- kind: "migrate-to-react",
14
- dryRun,
15
- sourceFiles: migration.sourceFiles.length,
16
- files: migration.actions.map(({ absolutePath, action }) => ({
17
- path: rootRelative(root, absolutePath),
18
- action,
19
- })),
20
- };
21
- }
22
-
23
- async function planLegacyWorkspaceMigration(root, config, { force = false } = {}) {
24
- const sourceFiles = await collectLegacyContentFiles(config.paths.sourceDir);
25
- const parsedFiles = await Promise.all(sourceFiles.map(async (filePath) => {
26
- const text = await fs.readFile(filePath, "utf8");
27
- const [meta, body] = parseFrontmatter(text);
28
- const fileName = path.basename(filePath);
29
- return {
30
- filePath,
31
- fileName,
32
- meta,
33
- body,
34
- kind: normalizedKind(meta.kind),
35
- slug: slugValue(meta.slug) ?? slugFromFileName(fileName),
36
- title: stringValue(meta.title),
37
- chapter: numberValue(meta.chapter),
38
- };
39
- }));
40
-
41
- const documentRoot = path.join(path.resolve(root), "document");
42
- const chaptersRoot = path.join(documentRoot, "chapters");
43
- const shell = Object.fromEntries(SHELL_KINDS.values().map((kind) => [kind, null]));
44
- const chapterStates = new Map();
45
- let chapterCounter = 0;
46
-
47
- for (const file of parsedFiles) {
48
- if (SHELL_KINDS.has(file.kind)) {
49
- shell[file.kind] ??= file;
50
- continue;
51
- }
52
-
53
- const chapter = file.chapter ?? (chapterCounter + 1);
54
- if (file.kind !== "chapter-opener") {
55
- chapterCounter = Math.max(chapterCounter, chapter);
56
- }
57
- const slug = file.slug || `chapter-${chapter}`;
58
- const key = `${chapter}:${slug}`;
59
- const current = chapterStates.get(key) ?? {
60
- chapter,
61
- slug,
62
- title: file.title ?? titleFromSlug(slug),
63
- opener: null,
64
- contentFiles: [],
65
- };
66
- current.title = file.title ?? current.title;
67
- if (file.kind === "chapter-opener") current.opener = file;
68
- else current.contentFiles.push(file);
69
- chapterStates.set(key, current);
70
- }
71
-
72
- const actions = [];
73
- const add = (absolutePath, content, action = "write") => actions.push({ absolutePath, content, action });
74
- const addCopy = (source, absolutePath) => {
75
- if (samePath(source, absolutePath)) return;
76
- actions.push({ source, absolutePath, action: "copy" });
77
- };
78
- const addDir = (absolutePath) => actions.push({ absolutePath, action: "mkdir" });
79
-
80
- addDir(documentRoot);
81
- add(path.join(documentRoot, "index.tsx"), renderDocumentIndex(config, shell));
82
- addCopy(config.paths.designDoc, path.join(documentRoot, "design.md"));
83
- addCopy(config.paths.themeDir, path.join(documentRoot, "theme"));
84
- addCopy(config.paths.mediaDir, path.join(documentRoot, "media"));
85
- addCopy(config.paths.componentsDir, path.join(documentRoot, "components"));
86
-
87
- const chapters = Array.from(chapterStates.values()).sort((a, b) => {
88
- if (a.chapter !== b.chapter) return a.chapter - b.chapter;
89
- return a.slug.localeCompare(b.slug);
90
- });
91
-
92
- for (const chapterState of chapters) {
93
- const chapterDir = path.join(chaptersRoot, chapterDirectoryName(chapterState.chapter, chapterState.slug));
94
- const contentDir = path.join(chapterDir, "content");
95
- addDir(contentDir);
96
- if (chapterState.opener) {
97
- add(path.join(chapterDir, "chapter.tsx"), renderChapterFile(chapterState));
98
- }
99
- chapterState.contentFiles.forEach((file, index) => {
100
- const contentName = `${String(index + 1).padStart(2, "0")}-${file.slug || chapterState.slug}.mdx`;
101
- add(path.join(contentDir, contentName), normalizeMdxBody(file.body));
102
- });
103
- }
104
-
105
- await assertWritable(actions, { force });
106
- return { sourceFiles: parsedFiles, actions };
107
- }
108
-
109
- async function applyMigrationPlan(migration, { force = false } = {}) {
110
- for (const item of migration.actions) {
111
- if (item.action === "mkdir") {
112
- await fs.mkdir(item.absolutePath, { recursive: true });
113
- } else if (item.action === "write") {
114
- await fs.mkdir(path.dirname(item.absolutePath), { recursive: true });
115
- if (!force) await assertMissing(item.absolutePath);
116
- await fs.writeFile(item.absolutePath, item.content, "utf8");
117
- } else if (item.action === "copy") {
118
- await copyPathIfExists(item.source, item.absolutePath, { force });
119
- }
120
- }
121
- }
122
-
123
- async function collectLegacyContentFiles(sourceDir) {
124
- let entries;
125
- try {
126
- entries = await fs.readdir(sourceDir, { withFileTypes: true });
127
- } catch (error) {
128
- if (error?.code === "ENOENT") return [];
129
- throw error;
130
- }
131
-
132
- return entries
133
- .filter((entry) => entry.isFile() && entry.name.endsWith(".md") && !entry.name.startsWith("_"))
134
- .map((entry) => path.join(sourceDir, entry.name))
135
- .sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
136
- }
137
-
138
- function renderDocumentIndex(config, shell) {
139
- const configLines = [
140
- ` title: ${jsString(config.title)},`,
141
- config.subtitle ? ` subtitle: ${jsString(config.subtitle)},` : null,
142
- config.organization ? ` organization: ${jsString(config.organization)},` : null,
143
- config.workspaceLabel ? ` workspaceLabel: ${jsString(config.workspaceLabel)},` : null,
144
- ` sourceDir: "chapters",`,
145
- ` mediaDir: "media",`,
146
- ` themeDir: "theme",`,
147
- ` designDoc: "design.md",`,
148
- ` componentsDir: "components",`,
149
- ` publicDir: ${jsString(config.publicDir)},`,
150
- ` outputDir: ${jsString(config.outputDir)},`,
151
- ` pdf: { filename: ${jsString(config.pdf.filename)} },`,
152
- ` deploy: {`,
153
- ` adapter: ${jsString(config.deploy.adapter)},`,
154
- ` source: ${jsString(config.deploy.source)},`,
155
- ` projectName: ${config.deploy.projectName == null ? "null" : jsString(config.deploy.projectName)},`,
156
- ` commitDirty: ${config.deploy.commitDirty ? "true" : "false"},`,
157
- ` requiresConfirmation: ${config.deploy.requiresConfirmation ? "true" : "false"},`,
158
- ` },`,
159
- ].filter(Boolean).join("\n");
160
-
161
- return [
162
- 'import type { Manifest } from "@openpress/core";',
163
- 'import { BaseBackCoverPage, BaseCoverPage, BaseTocPage } from "@openpress/core";',
164
- "",
165
- "export const config: Manifest = {",
166
- configLines,
167
- "};",
168
- "",
169
- renderShellExport("cover", shell.cover, "cover", "OpenPress"),
170
- "",
171
- renderTocExport(shell.toc),
172
- "",
173
- renderShellExport("backCover", shell["back-cover"], "back-cover", "End"),
174
- "",
175
- ].join("\n");
176
- }
177
-
178
- function renderShellExport(exportName, file, pageKind, fallbackTitle) {
179
- const title = file?.title ?? fallbackTitle;
180
- const bodyText = summaryText(file?.body);
181
- const Page = pageKind === "back-cover" ? "BaseBackCoverPage" : "BaseCoverPage";
182
- const mainClass = pageKind === "back-cover" ? "back-cover-main" : "cover-main";
183
- const titleClass = pageKind === "back-cover" ? "back-cover-statement" : "cover-title";
184
- const summaryClass = pageKind === "back-cover" ? "back-cover-summary" : "cover-summary";
185
-
186
- return `export const ${exportName} = (
187
- <${Page} data-page-title="${jsxAttr(title)}">
188
- <div className="${mainClass}">
189
- <h1 className="${titleClass}">${jsxText(title)}</h1>
190
- ${bodyText ? `<p className="${summaryClass}">${jsxText(bodyText)}</p>` : ""}
191
- </div>
192
- </${Page}>
193
- );`;
194
- }
195
-
196
- function renderTocExport(file) {
197
- const title = file?.title ?? "Contents";
198
- return `export const toc = (
199
- <BaseTocPage data-page-title="${jsxAttr(title)}" id="toc">
200
- <div className="page-frame">
201
- <header className="page-header" aria-hidden="true"></header>
202
- <main className="page-body">
203
- <h2 id="toc-title" className="toc-heading">${jsxText(title)}</h2>
204
- </main>
205
- </div>
206
- </BaseTocPage>
207
- );`;
208
- }
209
-
210
- function renderChapterFile(chapterState) {
211
- const title = chapterState.opener?.title ?? chapterState.title;
212
- const label = `Chapter ${chapterState.chapter}`;
213
- const summary = summaryText(chapterState.opener?.body);
214
- return [
215
- "export const meta = {",
216
- ` slug: ${jsString(chapterState.slug)},`,
217
- ` title: ${jsString(title)},`,
218
- ` chapter: ${chapterState.chapter},`,
219
- "};",
220
- "",
221
- "export const opener = (",
222
- ` <section className="reader-page reader-page--chapter-opener no-footer" data-page-kind="chapter-opener" data-page-footer="false" data-page-title="${jsxAttr(title)}">`,
223
- ' <div className="page-frame">',
224
- ' <header className="page-header" aria-hidden="true"></header>',
225
- ' <main className="page-body">',
226
- ` <p className="chapter-opener-kicker">${jsxText(label)}</p>`,
227
- ` <h2 className="chapter-opener-title">${jsxText(title)}</h2>`,
228
- summary ? ` <p className="chapter-opener-summary">${jsxText(summary)}</p>` : null,
229
- " </main>",
230
- " </div>",
231
- " </section>",
232
- ");",
233
- "",
234
- ].filter(Boolean).join("\n");
235
- }
236
-
237
- function parseFrontmatter(text) {
238
- const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
239
- if (!match) return [{}, text];
240
- return [yaml.load(match[1]) ?? {}, text.slice(match[0].length)];
241
- }
242
-
243
- function normalizeMdxBody(body) {
244
- const normalized = String(body ?? "").replace(/^\s+/, "").replace(/\s+$/, "");
245
- return `${normalized}\n`;
246
- }
247
-
248
- function normalizedKind(value) {
249
- if (typeof value !== "string" || !value.trim()) return "chapter";
250
- return value.trim();
251
- }
252
-
253
- function stringValue(value) {
254
- return typeof value === "string" && value.trim() ? value.trim() : null;
255
- }
256
-
257
- function slugValue(value) {
258
- return typeof value === "string" && value.trim() ? safeSlug(value.trim()) : null;
259
- }
260
-
261
- function numberValue(value) {
262
- return Number.isFinite(value) ? Number(value) : null;
263
- }
264
-
265
- function slugFromFileName(fileName) {
266
- return safeSlug(fileName.replace(/\.md$/i, "").replace(/^\d+[-_]?/, "").replace(/-opener$/, ""));
267
- }
268
-
269
- function safeSlug(value) {
270
- const slug = value
271
- .normalize("NFKD")
272
- .replace(/[^\p{Letter}\p{Number}]+/gu, "-")
273
- .replace(/^-+|-+$/g, "")
274
- .toLowerCase();
275
- return slug || "chapter";
276
- }
277
-
278
- function titleFromSlug(slug) {
279
- return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
280
- }
281
-
282
- function chapterDirectoryName(chapter, slug) {
283
- return `${String(chapter).padStart(2, "0")}-${safeSlug(slug)}`;
284
- }
285
-
286
- function summaryText(markdown = "") {
287
- const text = String(markdown)
288
- .replace(/<[^>]+>/g, " ")
289
- .replace(/```[\s\S]*?```/g, " ")
290
- .replace(/`([^`]+)`/g, "$1")
291
- .replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
292
- .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
293
- .replace(/^#{1,6}\s+/gm, "")
294
- .replace(/^[-*+]\s+/gm, "")
295
- .replace(/\s+/g, " ")
296
- .trim();
297
- return text.length > 160 ? `${text.slice(0, 157)}...` : text;
298
- }
299
-
300
- function jsString(value) {
301
- return JSON.stringify(String(value ?? ""));
302
- }
303
-
304
- function jsxText(value) {
305
- return String(value ?? "")
306
- .replaceAll("&", "&amp;")
307
- .replaceAll("<", "&lt;")
308
- .replaceAll(">", "&gt;")
309
- .replaceAll("{", "&#123;")
310
- .replaceAll("}", "&#125;");
311
- }
312
-
313
- function jsxAttr(value) {
314
- return jsxText(value).replaceAll('"', "&quot;");
315
- }
316
-
317
- async function assertWritable(actions, { force = false } = {}) {
318
- if (force) return;
319
- const writeTargets = actions.filter((item) => item.action === "write" || item.action === "copy");
320
- for (const item of writeTargets) {
321
- await assertMissing(item.absolutePath);
322
- }
323
- }
324
-
325
- async function assertMissing(filePath) {
326
- try {
327
- await fs.access(filePath);
328
- throw new Error(`Refusing to overwrite existing path: ${filePath}. Re-run with --force if this is intentional.`);
329
- } catch (error) {
330
- if (error?.code === "ENOENT") return;
331
- throw error;
332
- }
333
- }
334
-
335
- async function copyPathIfExists(source, destination, { force = false } = {}) {
336
- let stat;
337
- try {
338
- stat = await fs.stat(source);
339
- } catch (error) {
340
- if (error?.code === "ENOENT") return;
341
- throw error;
342
- }
343
- await fs.mkdir(path.dirname(destination), { recursive: true });
344
- if (force) await fs.rm(destination, { recursive: true, force: true });
345
- else await assertMissing(destination);
346
- await fs.cp(source, destination, { recursive: stat.isDirectory(), force: true });
347
- }
348
-
349
- function rootRelative(root, absolutePath) {
350
- return path.relative(root, absolutePath).replaceAll("\\", "/");
351
- }
352
-
353
- function samePath(left, right) {
354
- return path.resolve(left) === path.resolve(right);
355
- }
@@ -1,76 +0,0 @@
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
- }