@open-press/core 0.6.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.
- package/README.md +9 -5
- package/engine/cli.mjs +2 -5
- package/engine/commands/_shared.mjs +4 -4
- package/engine/commands/deploy.mjs +1 -1
- package/engine/commands/inspect.mjs +3 -3
- package/engine/commands/replace.mjs +1 -1
- package/engine/commands/search.mjs +1 -1
- package/engine/commands/validate.mjs +2 -2
- package/engine/document-export.mjs +1 -1
- package/engine/{chrome-pdf.mjs → output/chrome-pdf.mjs} +1 -2
- package/engine/{deploy-sync.mjs → output/deploy-sync.mjs} +2 -2
- package/engine/{fonts.mjs → output/fonts.mjs} +1 -1
- package/engine/{public-assets.mjs → output/public-assets.mjs} +2 -2
- package/engine/{static-server.mjs → output/static-server.mjs} +2 -2
- package/engine/react/caption-numbering.mjs +73 -0
- package/engine/react/comment-marker.mjs +54 -10
- package/engine/react/document-entry.mjs +124 -64
- package/engine/react/document-export.mjs +252 -311
- package/engine/react/mdx-compile.mjs +123 -3
- package/engine/react/measurement-css.mjs +3 -3
- package/engine/react/pagination/allocator.mjs +122 -0
- package/engine/react/pagination/regions.mjs +81 -0
- package/engine/react/pagination.mjs +9 -121
- package/engine/react/pipeline/allocate.mjs +248 -0
- package/engine/react/pipeline/final-render.mjs +94 -0
- package/engine/react/pipeline/frame-measurement.mjs +271 -0
- package/engine/react/pipeline/press-tree.mjs +135 -0
- package/engine/react/project-asset-endpoint.mjs +2 -2
- package/engine/react/{chapter-css.mjs → section-css.mjs} +12 -9
- package/engine/react/sources/heading-numbering.mjs +132 -0
- package/engine/react/sources/mdx-resolver.mjs +441 -0
- package/engine/react/{workspace-discovery.mjs → style-discovery.mjs} +29 -40
- package/engine/{config.mjs → runtime/config.mjs} +15 -0
- package/engine/{file-utils.mjs → runtime/file-utils.mjs} +1 -1
- package/engine/{inspection.mjs → runtime/inspection.mjs} +3 -4
- package/engine/{source-text-tools.mjs → runtime/source-text-tools.mjs} +24 -7
- package/engine/runtime/source-workspace.mjs +186 -0
- package/engine/{validation.mjs → runtime/validation.mjs} +19 -17
- package/package.json +5 -2
- package/src/openpress/anchorMap.ts +27 -0
- package/src/openpress/core/Frame.tsx +80 -0
- package/src/openpress/core/FrameContext.tsx +19 -0
- package/src/openpress/core/MdxArea.tsx +35 -0
- package/src/openpress/core/Press.tsx +34 -0
- package/src/openpress/core/index.tsx +34 -15
- package/src/openpress/core/primitives.tsx +23 -0
- package/src/openpress/core/types.ts +131 -19
- package/src/openpress/core/useSource.ts +28 -0
- package/src/openpress/manuscript/index.tsx +196 -0
- package/src/openpress/mdx/index.ts +88 -0
- package/src/openpress/numbering/index.ts +294 -0
- package/src/openpress/publicPage.tsx +4 -186
- package/src/openpress/reactDocumentMetadata.ts +2 -16
- package/src/openpress/types.ts +0 -16
- package/src/openpress/workbench.tsx +2 -36
- package/src/styles/openpress/responsive.css +0 -14
- package/tsconfig.json +4 -1
- package/vite.config.ts +10 -3
- package/engine/commands/migrate-to-react.mjs +0 -27
- package/engine/page-renderer.mjs +0 -217
- package/engine/react/migrate-to-react.mjs +0 -355
- package/engine/source-workspace.mjs +0 -76
- package/src/openpress/core/basePages.tsx +0 -87
- package/src/openpress/pagination.ts +0 -845
- /package/engine/{chrome-pdf.d.mts → output/chrome-pdf.d.mts} +0 -0
- /package/engine/{katex-assets.mjs → output/katex-assets.mjs} +0 -0
- /package/engine/{page-block.mjs → output/page-block.mjs} +0 -0
- /package/engine/{pdf-media.mjs → output/pdf-media.mjs} +0 -0
- /package/engine/{config.d.mts → runtime/config.d.mts} +0 -0
- /package/engine/{issue-report.mjs → runtime/issue-report.mjs} +0 -0
|
@@ -1,148 +1,130 @@
|
|
|
1
|
+
// Layer 6 orchestrator.
|
|
2
|
+
//
|
|
3
|
+
// Wires Layer 1 (entry load) -> source resolution -> Layer 2/3/4 iteration
|
|
4
|
+
// -> Layer 5 final render -> document.json + asset sync.
|
|
5
|
+
|
|
1
6
|
import fs from "node:fs/promises";
|
|
2
7
|
import path from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { loadReactDocumentEntry, createReactSsrServer } from "./document-entry.mjs";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { documentRelativePath, pageToBlock } from "../output/page-block.mjs";
|
|
10
|
+
import { syncPublicAssets } from "../output/public-assets.mjs";
|
|
11
|
+
import { createCaptionNumberingState, numberCaptionsInHtml } from "./caption-numbering.mjs";
|
|
12
|
+
import { buildSectionScopedCss } from "./section-css.mjs";
|
|
13
|
+
import { CORE_ENTRY, createReactSsrServer, loadReactDocumentEntry } from "./document-entry.mjs";
|
|
10
14
|
import { buildReactMeasurementCss } from "./measurement-css.mjs";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
15
|
+
import { allocateChains } from "./pipeline/allocate.mjs";
|
|
16
|
+
import { measureFrames } from "./pipeline/frame-measurement.mjs";
|
|
17
|
+
import { renderFinalPress } from "./pipeline/final-render.mjs";
|
|
18
|
+
import { expandPressTree } from "./pipeline/press-tree.mjs";
|
|
19
|
+
import { resolveAllSources } from "./sources/mdx-resolver.mjs";
|
|
20
|
+
import { discoverSectionStyles } from "./style-discovery.mjs";
|
|
21
|
+
|
|
22
|
+
const MAX_ITERATIONS = 20;
|
|
14
23
|
|
|
15
|
-
export async function exportReactDocument(root = ".", { syncAssets = true
|
|
24
|
+
export async function exportReactDocument(root = ".", { syncAssets = true } = {}) {
|
|
16
25
|
const workspaceRoot = path.resolve(root);
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
// Quick existence check without opening an SSR server.
|
|
27
|
+
const fastCheck = await loadReactDocumentEntry(workspaceRoot);
|
|
28
|
+
if (!fastCheck) return null;
|
|
19
29
|
|
|
20
|
-
const workspace = await discoverReactWorkspace(workspaceRoot, entry.config);
|
|
21
|
-
const paginationOptions = normalizePaginationOptions(pagination);
|
|
22
|
-
if (paginationOptions.enabled && paginationOptions.needsMeasurementCss) {
|
|
23
|
-
paginationOptions.css = await buildReactMeasurementCss(workspaceRoot, entry.config, workspace);
|
|
24
|
-
}
|
|
25
30
|
const server = await createReactSsrServer(workspaceRoot);
|
|
26
31
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const chapterMeta = normalizeChapterMeta(chapter, chapterModule.meta);
|
|
36
|
-
const components = await loadComponentScope(server, chapter.componentScope);
|
|
37
|
-
const Page = typeof chapterModule.Page === "function" ? chapterModule.Page : components.Page ?? DefaultContentPage;
|
|
38
|
-
|
|
39
|
-
addShellPage(
|
|
40
|
-
pageJobs,
|
|
41
|
-
chapterModule.opener ?? null,
|
|
42
|
-
chapterSource(entry.config, chapter, {
|
|
43
|
-
chapterIndex,
|
|
44
|
-
kind: "chapter-opener",
|
|
45
|
-
slug: chapterMeta.slug,
|
|
46
|
-
title: chapterMeta.title,
|
|
47
|
-
}),
|
|
32
|
+
// Reload the entry through THIS server so the module identity matches
|
|
33
|
+
// what the rest of the pipeline (PressContext, hooks) sees.
|
|
34
|
+
const entry = await loadReactDocumentEntry(workspaceRoot, { server });
|
|
35
|
+
if (!entry) return null;
|
|
36
|
+
if (!entry.Press) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`OpenPress document entry ${entry.entryPath} must default-export a Press component (function) to export. ` +
|
|
39
|
+
`Legacy named exports (cover/toc/backCover) are not supported in v0.6 — see the Press Tree spec.`,
|
|
48
40
|
);
|
|
41
|
+
}
|
|
42
|
+
// Resolve PressContext + Frame markers from the engine's loaded core module.
|
|
43
|
+
// Use the absolute file path so the user's `import "@open-press/core"`
|
|
44
|
+
// (resolved via vite alias) and our load hit the same module cache entry.
|
|
45
|
+
const coreModule = await server.ssrLoadModule(CORE_ENTRY);
|
|
46
|
+
const PressContext = coreModule.PressContext;
|
|
47
|
+
if (!PressContext) {
|
|
48
|
+
throw new Error("Engine could not resolve PressContext from @open-press/core.");
|
|
49
|
+
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
source,
|
|
54
|
-
filePath: contentFile.absolutePath,
|
|
55
|
-
components,
|
|
56
|
-
chapterSlug: chapterMeta.slug,
|
|
57
|
-
});
|
|
58
|
-
const sourceRecord = chapterSource(entry.config, chapter, {
|
|
59
|
-
chapterIndex,
|
|
60
|
-
contentFile,
|
|
61
|
-
kind: "content",
|
|
62
|
-
slug: chapterMeta.slug,
|
|
63
|
-
title: chapterMeta.title,
|
|
64
|
-
});
|
|
65
|
-
const mdxBlocks = compiled.blocks.map((block) => sanitizeMdxBlock(entry.config, contentFile, block));
|
|
51
|
+
// Discover workspace for component scope and chapter-scoped style files.
|
|
52
|
+
const workspace = await discoverSectionStyles(workspaceRoot, entry.config);
|
|
53
|
+
const globalComponents = await loadComponentModules(server, workspace.globalComponents ?? []);
|
|
66
54
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}));
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
55
|
+
// Resolve sources.
|
|
56
|
+
const documentRoot = entry.config.paths.documentRoot;
|
|
57
|
+
const { resolved: sources, renderData: renderRegistry } = await resolveAllSources({
|
|
58
|
+
sources: entry.sources,
|
|
59
|
+
documentRoot,
|
|
60
|
+
globalComponents,
|
|
61
|
+
});
|
|
77
62
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
pageIndex: 0,
|
|
82
|
-
totalPages: 1,
|
|
83
|
-
chapterSlug: chapterMeta.slug,
|
|
84
|
-
chapterTone: chapterMeta.tone,
|
|
85
|
-
},
|
|
86
|
-
React.createElement(compiled.Content),
|
|
87
|
-
));
|
|
88
|
-
const measured = await paginationOptions.measureBlocks({
|
|
89
|
-
html: measurementHtml,
|
|
90
|
-
blockIds: mdxBlocks.map((block) => block.id),
|
|
91
|
-
pageSafeHeightPx: paginationOptions.pageSafeHeightPx,
|
|
92
|
-
css: paginationOptions.css,
|
|
93
|
-
chapterSlug: chapterMeta.slug,
|
|
94
|
-
contentFile,
|
|
95
|
-
source: sourceRecord,
|
|
96
|
-
});
|
|
97
|
-
const blockLookup = Object.fromEntries(mdxBlocks.map((block) => [block.id, block]));
|
|
98
|
-
for (const warning of measured.warnings ?? []) {
|
|
99
|
-
paginationWarnings.push(enrichPaginationWarning(warning, blockLookup));
|
|
100
|
-
}
|
|
63
|
+
// Build measurement CSS.
|
|
64
|
+
const css = await buildReactMeasurementCss(workspaceRoot, entry.config, workspace);
|
|
101
65
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
66
|
+
// Iterative allocation loop.
|
|
67
|
+
let hints = null;
|
|
68
|
+
let allocation = null;
|
|
69
|
+
let lastFrames = null;
|
|
70
|
+
let warnings = [];
|
|
71
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
72
|
+
const { html, frames } = expandPressTree({
|
|
73
|
+
Press: entry.Press,
|
|
74
|
+
PressContext,
|
|
75
|
+
sources,
|
|
76
|
+
hints,
|
|
77
|
+
});
|
|
78
|
+
lastFrames = frames;
|
|
79
|
+
validateAllChainsKnown(frames, sources);
|
|
80
|
+
const measurement = await measureFrames({
|
|
81
|
+
pressHtml: html,
|
|
82
|
+
sources,
|
|
83
|
+
renderRegistry,
|
|
84
|
+
css,
|
|
85
|
+
baseHref: pathToFileURL(`${documentRoot}${path.sep}`).href,
|
|
86
|
+
mediaDir: path.join(documentRoot, "media"),
|
|
87
|
+
captionNumbering: entry.config.captionNumbering,
|
|
88
|
+
});
|
|
89
|
+
const alloc = allocateChains({
|
|
90
|
+
frames,
|
|
91
|
+
mdxAreas: measurement.mdxAreas,
|
|
92
|
+
blockHeights: measurement.blockHeights,
|
|
93
|
+
sources,
|
|
94
|
+
});
|
|
95
|
+
if (hintsEqual(hints, alloc.hints)) {
|
|
96
|
+
allocation = alloc.allocation;
|
|
97
|
+
warnings = alloc.warnings;
|
|
98
|
+
break;
|
|
126
99
|
}
|
|
100
|
+
hints = alloc.hints;
|
|
101
|
+
}
|
|
102
|
+
if (allocation == null) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Allocation did not converge after ${MAX_ITERATIONS} iterations. ` +
|
|
105
|
+
`This usually means a chain keeps growing without fitting; check MdxArea capacities and block heights.`,
|
|
106
|
+
);
|
|
127
107
|
}
|
|
128
108
|
|
|
129
|
-
|
|
109
|
+
const toc = buildTocContext({ sources, frames: lastFrames ?? [], allocation });
|
|
130
110
|
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return pageToBlock(index, page.html, page.source, entry.config);
|
|
111
|
+
// Final render.
|
|
112
|
+
const final = await renderFinalPress({
|
|
113
|
+
Press: entry.Press,
|
|
114
|
+
PressContext,
|
|
115
|
+
sources,
|
|
116
|
+
hints,
|
|
117
|
+
toc,
|
|
118
|
+
allocation,
|
|
119
|
+
renderRegistry,
|
|
141
120
|
});
|
|
142
|
-
|
|
121
|
+
|
|
122
|
+
// Write chapter-scoped CSS (under section-scoped rules but filename
|
|
123
|
+
// unchanged for now).
|
|
124
|
+
const chapterCss = await buildSectionScopedCss(workspace);
|
|
143
125
|
const styles = [];
|
|
126
|
+
await fs.mkdir(entry.config.paths.publicDir, { recursive: true });
|
|
144
127
|
if (chapterCss.trim()) {
|
|
145
|
-
await fs.mkdir(entry.config.paths.publicDir, { recursive: true });
|
|
146
128
|
await fs.writeFile(path.join(entry.config.paths.publicDir, "chapter-scoped.css"), chapterCss, "utf8");
|
|
147
129
|
styles.push({
|
|
148
130
|
kind: "chapter-scoped-css",
|
|
@@ -151,238 +133,197 @@ export async function exportReactDocument(root = ".", { syncAssets = true, pagin
|
|
|
151
133
|
});
|
|
152
134
|
}
|
|
153
135
|
|
|
136
|
+
// Build document.json. The reader filters blocks by kind === "htmlPage",
|
|
137
|
+
// so wrap each frame through `pageToBlock` to inherit that contract.
|
|
138
|
+
const blockMap = {};
|
|
139
|
+
const captionState = createCaptionNumberingState();
|
|
140
|
+
const blocks = final.frames.map((frame, index) => {
|
|
141
|
+
for (const id of frame.blockIds) {
|
|
142
|
+
blockMap[id] = { id, pageIndex: index, pageNumber: index + 1 };
|
|
143
|
+
}
|
|
144
|
+
const source = {
|
|
145
|
+
file: "index.tsx",
|
|
146
|
+
path: "document/index.tsx",
|
|
147
|
+
kind: frame.role ?? "manuscript.content",
|
|
148
|
+
slug: frame.frameKey,
|
|
149
|
+
sectionIndex: index + 1,
|
|
150
|
+
};
|
|
151
|
+
const html = numberCaptionsInHtml(frame.html, entry.config.captionNumbering, captionState);
|
|
152
|
+
const block = pageToBlock(index, html, source, entry.config, {
|
|
153
|
+
idPrefix: "openpress-page",
|
|
154
|
+
anchorPrefix: "page",
|
|
155
|
+
titleFallback: "Page",
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
...block,
|
|
159
|
+
frameKey: frame.frameKey,
|
|
160
|
+
role: frame.role ?? null,
|
|
161
|
+
chrome: frame.chrome ?? true,
|
|
162
|
+
blockIds: frame.blockIds,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Enrich blockMap with source records from resolved chains so comment
|
|
167
|
+
// tooling can resolve block IDs back to MDX positions.
|
|
168
|
+
const sourceBlockIndex = buildSourceBlockIndex(sources);
|
|
169
|
+
for (const id of Object.keys(blockMap)) {
|
|
170
|
+
const sourceRecord = sourceBlockIndex.get(id);
|
|
171
|
+
if (sourceRecord) {
|
|
172
|
+
blockMap[id] = {
|
|
173
|
+
...blockMap[id],
|
|
174
|
+
kind: sourceRecord.kind,
|
|
175
|
+
name: sourceRecord.name,
|
|
176
|
+
path: sourceRecord.path,
|
|
177
|
+
source: sourceRecord.source,
|
|
178
|
+
chainId: sourceRecord.chainId,
|
|
179
|
+
sectionSlug: sourceRecord.sectionSlug,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
154
184
|
const readerDocument = {
|
|
155
185
|
meta: {
|
|
156
186
|
title: trimmedString(entry.config.title) ?? "Untitled Document",
|
|
157
187
|
subtitle: trimmedString(entry.config.subtitle) ?? "",
|
|
158
188
|
organization: trimmedString(entry.config.organization) ?? "",
|
|
159
189
|
workspaceLabel: trimmedString(entry.config.workspaceLabel) ?? trimmedString(entry.config.title) ?? "Untitled Document",
|
|
160
|
-
version: "openpress-
|
|
190
|
+
version: "openpress-press-tree-v1",
|
|
161
191
|
},
|
|
162
192
|
source: {
|
|
163
|
-
type: "openpress-
|
|
193
|
+
type: "openpress-press-tree-mdx",
|
|
164
194
|
contentDir: documentRelativePath(entry.config, entry.config.sourceDir),
|
|
165
195
|
editable: true,
|
|
166
196
|
editMode: "source-mdx",
|
|
167
197
|
styles,
|
|
168
198
|
blockMap,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
frames: final.frames.map((frame, index) => ({
|
|
200
|
+
frameKey: frame.frameKey,
|
|
201
|
+
role: frame.role ?? null,
|
|
202
|
+
pageIndex: index,
|
|
203
|
+
mdxAreas: frame.mdxAreas.map((area) => ({
|
|
204
|
+
chainId: area.chainId,
|
|
205
|
+
indexInFrame: area.indexInFrame,
|
|
206
|
+
blockIds: area.blockIds,
|
|
207
|
+
})),
|
|
208
|
+
})),
|
|
209
|
+
chains: Object.keys(sources).flatMap((id) => Object.keys(sources[id].chains)),
|
|
210
|
+
warnings,
|
|
176
211
|
},
|
|
177
212
|
blocks,
|
|
178
213
|
};
|
|
179
214
|
|
|
180
215
|
const documentPath = path.join(entry.config.paths.publicDir, "document.json");
|
|
181
|
-
await fs.mkdir(entry.config.paths.publicDir, { recursive: true });
|
|
182
216
|
await fs.writeFile(documentPath, JSON.stringify(readerDocument, null, 2), "utf8");
|
|
217
|
+
|
|
183
218
|
if (syncAssets) {
|
|
184
219
|
await syncPublicAssets(workspaceRoot, entry.config.paths.publicDir, entry.config);
|
|
185
220
|
}
|
|
221
|
+
|
|
186
222
|
return { documentPath, pageCount: blocks.length, document: readerDocument };
|
|
187
223
|
} finally {
|
|
188
224
|
await server.close();
|
|
189
225
|
}
|
|
190
226
|
}
|
|
191
227
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
228
|
+
async function loadComponentModules(server, components) {
|
|
229
|
+
const out = {};
|
|
230
|
+
for (const component of components) {
|
|
231
|
+
const mod = await server.ssrLoadModule(component.absolutePath);
|
|
232
|
+
if (typeof mod.default !== "function") {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`OpenPress component module ${component.documentPath} must default-export a React component.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
out[component.name] = mod.default;
|
|
198
238
|
}
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function renderPageJobs(pageJobs, totalPages) {
|
|
203
|
-
return pageJobs.map((job, index) => ({
|
|
204
|
-
html: renderToStaticMarkup(job.render(index, totalPages)),
|
|
205
|
-
source: job.source,
|
|
206
|
-
mdxBlocks: job.mdxBlocks ?? [],
|
|
207
|
-
}));
|
|
239
|
+
return out;
|
|
208
240
|
}
|
|
209
241
|
|
|
210
|
-
function
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
html: injectedHtml[index],
|
|
215
|
-
}));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const tocIndex = records.findIndex((record) => hasReaderPageKind(record.html, "toc"));
|
|
219
|
-
const extra = injectedHtml.length - records.length;
|
|
220
|
-
if (tocIndex < 0 || extra < 1) {
|
|
221
|
-
throw new Error(`React TOC injection changed page count unexpectedly: ${records.length} -> ${injectedHtml.length}`);
|
|
242
|
+
function validateAllChainsKnown(frames, sources) {
|
|
243
|
+
const known = new Set();
|
|
244
|
+
for (const source of Object.values(sources)) {
|
|
245
|
+
for (const chainId of Object.keys(source.chains)) known.add(chainId);
|
|
222
246
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
},
|
|
233
|
-
mdxBlocks: [],
|
|
234
|
-
};
|
|
247
|
+
for (const frame of frames) {
|
|
248
|
+
for (const area of frame.mdxAreas) {
|
|
249
|
+
if (!known.has(area.chainId)) {
|
|
250
|
+
const list = [...known].sort().slice(0, 10).join(", ");
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Unknown chainId "${area.chainId}" referenced by frame "${frame.frameKey}". ` +
|
|
253
|
+
`Known chains: ${list || "(none)"}${known.size > 10 ? ", ..." : ""}.`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
235
256
|
}
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
...sourceRecord,
|
|
239
|
-
html,
|
|
240
|
-
};
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function hasReaderPageKind(html, kind) {
|
|
245
|
-
const openingTag = String(html).match(/^<section[^>]*>/i)?.[0] ?? "";
|
|
246
|
-
return openingTag.match(/\bdata-page-kind="([^"]*)"/i)?.[1] === kind;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function addShellPage(pageJobs, element, source) {
|
|
250
|
-
if (element == null) return;
|
|
251
|
-
pageJobs.push({
|
|
252
|
-
source,
|
|
253
|
-
render() {
|
|
254
|
-
return element;
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function mdxPageJob({ Page, Content, source, mdxBlocks, chapterMeta, pagination = null }) {
|
|
260
|
-
return {
|
|
261
|
-
source,
|
|
262
|
-
mdxBlocks,
|
|
263
|
-
pagination,
|
|
264
|
-
render(pageIndex, totalPages) {
|
|
265
|
-
return React.createElement(
|
|
266
|
-
Page,
|
|
267
|
-
{
|
|
268
|
-
pageIndex,
|
|
269
|
-
totalPages,
|
|
270
|
-
chapterSlug: chapterMeta.slug,
|
|
271
|
-
chapterTone: chapterMeta.tone,
|
|
272
|
-
},
|
|
273
|
-
React.createElement(Content),
|
|
274
|
-
);
|
|
275
|
-
},
|
|
276
|
-
};
|
|
257
|
+
}
|
|
277
258
|
}
|
|
278
259
|
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
260
|
+
function hintsEqual(a, b) {
|
|
261
|
+
if (a === b) return true;
|
|
262
|
+
if (!a || !b) return false;
|
|
263
|
+
const aMap = a.totalPagesPerChain ?? {};
|
|
264
|
+
const bMap = b.totalPagesPerChain ?? {};
|
|
265
|
+
const keys = new Set([...Object.keys(aMap), ...Object.keys(bMap)]);
|
|
266
|
+
for (const key of keys) {
|
|
267
|
+
if (aMap[key] !== bMap[key]) return false;
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
282
270
|
}
|
|
283
271
|
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
for (const
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
272
|
+
function buildSourceBlockIndex(sources) {
|
|
273
|
+
const index = new Map();
|
|
274
|
+
for (const source of Object.values(sources)) {
|
|
275
|
+
for (const [chainId, blocks] of Object.entries(source.chains)) {
|
|
276
|
+
for (const block of blocks) {
|
|
277
|
+
index.set(block.id, { ...block, chainId });
|
|
278
|
+
}
|
|
290
279
|
}
|
|
291
|
-
components[name] = mod.default;
|
|
292
280
|
}
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function normalizeChapterMeta(chapter, meta) {
|
|
297
|
-
const rawMeta = meta && typeof meta === "object" ? meta : {};
|
|
298
|
-
return {
|
|
299
|
-
slug: trimmedString(rawMeta.slug) ?? chapter.slug,
|
|
300
|
-
title: trimmedString(rawMeta.title) ?? chapter.slug,
|
|
301
|
-
tone: trimmedString(rawMeta.tone) ?? undefined,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function shellSource(config, kind) {
|
|
306
|
-
return {
|
|
307
|
-
file: "index.tsx",
|
|
308
|
-
path: documentRelativePath(config, "index.tsx"),
|
|
309
|
-
kind,
|
|
310
|
-
slug: kind,
|
|
311
|
-
sectionIndex: 1,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function chapterSource(config, chapter, { chapterIndex, contentFile, kind, slug, title }) {
|
|
316
|
-
const file = contentFile?.documentPath ?? chapter.chapterEntry?.documentPath ?? chapter.documentPath;
|
|
317
|
-
return {
|
|
318
|
-
file: path.basename(file),
|
|
319
|
-
path: documentRelativePath(config, file),
|
|
320
|
-
kind,
|
|
321
|
-
chapter: chapterIndex + 1,
|
|
322
|
-
slug,
|
|
323
|
-
title,
|
|
324
|
-
sectionIndex: 1,
|
|
325
|
-
};
|
|
281
|
+
return index;
|
|
326
282
|
}
|
|
327
283
|
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
} : {}),
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function normalizePaginationOptions(pagination) {
|
|
351
|
-
if (!pagination?.enabled) {
|
|
352
|
-
return { enabled: false };
|
|
284
|
+
function buildTocContext({ sources, frames, allocation }) {
|
|
285
|
+
const toc = {};
|
|
286
|
+
for (const source of Object.values(sources)) {
|
|
287
|
+
for (const [tocChainId, tocBlocks] of Object.entries(source.chains).filter(([chainId]) => chainId.startsWith(`toc:${source.id}`))) {
|
|
288
|
+
if (tocBlocks.length === 0) continue;
|
|
289
|
+
toc[tocChainId] = tocBlocks.map((block) => ({
|
|
290
|
+
id: `${source.id}:${block.sectionSlug}`,
|
|
291
|
+
blockId: block.id,
|
|
292
|
+
sourceId: source.id,
|
|
293
|
+
sectionSlug: block.sectionSlug,
|
|
294
|
+
title: block.title,
|
|
295
|
+
href: block.href,
|
|
296
|
+
level: block.level,
|
|
297
|
+
label: block.label,
|
|
298
|
+
pageNumber: firstAllocatedPageNumberForBlock(frames, allocation, block.targetBlockId)
|
|
299
|
+
?? firstAllocatedPageNumber(frames, allocation, `${source.id}:${block.sectionSlug}`),
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
353
302
|
}
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
enabled: true,
|
|
357
|
-
pageSafeHeightPx,
|
|
358
|
-
needsMeasurementCss: typeof pagination.measureBlocks !== "function",
|
|
359
|
-
measureBlocks: pagination.measureBlocks ?? ((input) => measureBlocksInChromium({
|
|
360
|
-
html: input.html,
|
|
361
|
-
css: input.css,
|
|
362
|
-
pageSafeHeightPx,
|
|
363
|
-
})),
|
|
364
|
-
};
|
|
303
|
+
return toc;
|
|
365
304
|
}
|
|
366
305
|
|
|
367
|
-
function
|
|
368
|
-
|
|
369
|
-
|
|
306
|
+
function firstAllocatedPageNumberForBlock(frames, allocation, blockId) {
|
|
307
|
+
if (!blockId) return undefined;
|
|
308
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
309
|
+
const frameAllocation = allocation?.[frames[index].frameKey] ?? {};
|
|
310
|
+
for (const areaArr of Object.values(frameAllocation)) {
|
|
311
|
+
if (areaArr?.some((area) => Array.isArray(area) && area.includes(blockId))) return index + 1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
370
315
|
}
|
|
371
316
|
|
|
372
|
-
function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
"data-chapter-tone": chapterTone,
|
|
383
|
-
},
|
|
384
|
-
children,
|
|
385
|
-
);
|
|
317
|
+
function firstAllocatedPageNumber(frames, allocation, chainId) {
|
|
318
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
319
|
+
const frame = frames[index];
|
|
320
|
+
const allocated = allocation?.[frame.frameKey]?.[chainId];
|
|
321
|
+
if (allocated?.some((area) => Array.isArray(area) && area.length > 0)) return index + 1;
|
|
322
|
+
}
|
|
323
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
324
|
+
if (frames[index].mdxAreas.some((area) => area.chainId === chainId)) return index + 1;
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
386
327
|
}
|
|
387
328
|
|
|
388
329
|
function trimmedString(value) {
|