@open-press/core 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 (70) hide show
  1. package/README.md +9 -5
  2. package/engine/cli.mjs +2 -5
  3. package/engine/commands/_shared.mjs +4 -4
  4. package/engine/commands/deploy.mjs +1 -1
  5. package/engine/commands/inspect.mjs +3 -3
  6. package/engine/commands/replace.mjs +1 -1
  7. package/engine/commands/search.mjs +1 -1
  8. package/engine/commands/validate.mjs +2 -2
  9. package/engine/document-export.mjs +1 -1
  10. package/engine/{chrome-pdf.mjs → output/chrome-pdf.mjs} +1 -2
  11. package/engine/{deploy-sync.mjs → output/deploy-sync.mjs} +2 -2
  12. package/engine/{fonts.mjs → output/fonts.mjs} +1 -1
  13. package/engine/{public-assets.mjs → output/public-assets.mjs} +2 -2
  14. package/engine/{static-server.mjs → output/static-server.mjs} +2 -2
  15. package/engine/react/caption-numbering.mjs +73 -0
  16. package/engine/react/comment-marker.mjs +54 -10
  17. package/engine/react/document-entry.mjs +124 -64
  18. package/engine/react/document-export.mjs +252 -311
  19. package/engine/react/mdx-compile.mjs +123 -3
  20. package/engine/react/measurement-css.mjs +3 -3
  21. package/engine/react/pagination/allocator.mjs +122 -0
  22. package/engine/react/pagination/regions.mjs +81 -0
  23. package/engine/react/pagination.mjs +9 -121
  24. package/engine/react/pipeline/allocate.mjs +248 -0
  25. package/engine/react/pipeline/final-render.mjs +94 -0
  26. package/engine/react/pipeline/frame-measurement.mjs +271 -0
  27. package/engine/react/pipeline/press-tree.mjs +135 -0
  28. package/engine/react/project-asset-endpoint.mjs +2 -2
  29. package/engine/react/{chapter-css.mjs → section-css.mjs} +12 -9
  30. package/engine/react/sources/heading-numbering.mjs +132 -0
  31. package/engine/react/sources/mdx-resolver.mjs +441 -0
  32. package/engine/react/{workspace-discovery.mjs → style-discovery.mjs} +29 -40
  33. package/engine/{config.mjs → runtime/config.mjs} +15 -0
  34. package/engine/{file-utils.mjs → runtime/file-utils.mjs} +1 -1
  35. package/engine/{inspection.mjs → runtime/inspection.mjs} +3 -4
  36. package/engine/{source-text-tools.mjs → runtime/source-text-tools.mjs} +24 -7
  37. package/engine/runtime/source-workspace.mjs +186 -0
  38. package/engine/{validation.mjs → runtime/validation.mjs} +19 -17
  39. package/package.json +5 -2
  40. package/src/openpress/anchorMap.ts +27 -0
  41. package/src/openpress/core/Frame.tsx +80 -0
  42. package/src/openpress/core/FrameContext.tsx +19 -0
  43. package/src/openpress/core/MdxArea.tsx +35 -0
  44. package/src/openpress/core/Press.tsx +34 -0
  45. package/src/openpress/core/index.tsx +34 -15
  46. package/src/openpress/core/primitives.tsx +23 -0
  47. package/src/openpress/core/types.ts +131 -19
  48. package/src/openpress/core/useSource.ts +28 -0
  49. package/src/openpress/manuscript/index.tsx +196 -0
  50. package/src/openpress/mdx/index.ts +88 -0
  51. package/src/openpress/numbering/index.ts +294 -0
  52. package/src/openpress/publicPage.tsx +4 -186
  53. package/src/openpress/reactDocumentMetadata.ts +2 -16
  54. package/src/openpress/types.ts +0 -16
  55. package/src/openpress/workbench.tsx +2 -36
  56. package/src/styles/openpress/responsive.css +0 -14
  57. package/tsconfig.json +4 -1
  58. package/vite.config.ts +10 -3
  59. package/engine/commands/migrate-to-react.mjs +0 -27
  60. package/engine/page-renderer.mjs +0 -217
  61. package/engine/react/migrate-to-react.mjs +0 -355
  62. package/engine/source-workspace.mjs +0 -76
  63. package/src/openpress/core/basePages.tsx +0 -87
  64. package/src/openpress/pagination.ts +0 -845
  65. /package/engine/{chrome-pdf.d.mts → output/chrome-pdf.d.mts} +0 -0
  66. /package/engine/{katex-assets.mjs → output/katex-assets.mjs} +0 -0
  67. /package/engine/{page-block.mjs → output/page-block.mjs} +0 -0
  68. /package/engine/{pdf-media.mjs → output/pdf-media.mjs} +0 -0
  69. /package/engine/{config.d.mts → runtime/config.d.mts} +0 -0
  70. /package/engine/{issue-report.mjs → runtime/issue-report.mjs} +0 -0
@@ -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
- }
@@ -1,87 +0,0 @@
1
- import type {
2
- BaseCalloutProps,
3
- BaseFigureProps,
4
- BasePageProps,
5
- BaseContentPageProps,
6
- BaseShellPageProps,
7
- } from "./types";
8
-
9
- function classNames(...values: Array<string | undefined>) {
10
- const joined = values.filter(Boolean).join(" ");
11
- return joined.length > 0 ? joined : undefined;
12
- }
13
-
14
- export function BasePage({ kind, footer = true, className, children, ...sectionProps }: BasePageProps) {
15
- return (
16
- <section
17
- {...sectionProps}
18
- className={classNames("reader-page", `reader-page--${kind}`, className)}
19
- data-page-footer={footer ? "true" : "false"}
20
- data-page-kind={kind}
21
- >
22
- {children}
23
- </section>
24
- );
25
- }
26
-
27
- export function BaseCoverPage(props: BaseShellPageProps) {
28
- return <BasePage {...props} footer={false} kind="cover" />;
29
- }
30
-
31
- export function BaseTocPage(props: BaseShellPageProps) {
32
- return <BasePage {...props} footer={false} kind="toc" />;
33
- }
34
-
35
- export function BaseContentPage({
36
- pageIndex,
37
- totalPages,
38
- chapterSlug,
39
- chapterTone,
40
- runningHeader,
41
- footerLeft,
42
- footerRight,
43
- children,
44
- ...sectionProps
45
- }: BaseContentPageProps) {
46
- return (
47
- <BasePage
48
- {...sectionProps}
49
- data-chapter-slug={chapterSlug}
50
- data-chapter-tone={chapterTone}
51
- data-page-index={pageIndex}
52
- data-total-pages={totalPages}
53
- footer
54
- kind="content"
55
- >
56
- {runningHeader === undefined ? null : <header data-page-running-header>{runningHeader}</header>}
57
- {children}
58
- {footerLeft === undefined && footerRight === undefined ? null : (
59
- <footer data-page-footer-content>
60
- {footerLeft === undefined ? null : <span data-page-footer-left>{footerLeft}</span>}
61
- {footerRight === undefined ? null : <span data-page-footer-right>{footerRight}</span>}
62
- </footer>
63
- )}
64
- </BasePage>
65
- );
66
- }
67
-
68
- export function BaseBackCoverPage(props: BaseShellPageProps) {
69
- return <BasePage {...props} footer={false} kind="back-cover" />;
70
- }
71
-
72
- export function BaseFigure({ caption, className, children, ...figureProps }: BaseFigureProps) {
73
- return (
74
- <figure {...figureProps} className={classNames("openpress-figure", className)}>
75
- <div data-figure-body>{children}</div>
76
- {caption === undefined ? null : <figcaption>{caption}</figcaption>}
77
- </figure>
78
- );
79
- }
80
-
81
- export function BaseCallout({ kind = "info", className, children, ...calloutProps }: BaseCalloutProps) {
82
- return (
83
- <aside {...calloutProps} className={classNames("openpress-callout", className)} data-callout-kind={kind}>
84
- {children}
85
- </aside>
86
- );
87
- }