@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
@@ -31,6 +31,7 @@ export async function compileMdx({
31
31
  components = {},
32
32
  chapterSlug = "document",
33
33
  includeBlockIds = null,
34
+ blockAttributes = null,
34
35
  } = {}) {
35
36
  if (typeof source !== "string") throw new Error("compileMdx requires a string `source`.");
36
37
  if (typeof filePath !== "string" || !filePath.trim()) throw new Error("compileMdx requires `filePath`.");
@@ -39,7 +40,7 @@ export async function compileMdx({
39
40
 
40
41
  const blocks = [];
41
42
  const remarkPlugins = [[remarkMath, { singleDollarTextMath: true }], remarkGfm, [remarkBlockOnlyMdx, { filePath }]];
42
- const rehypePlugins = [rehypeKatex, rehypeTableCaptions, [rehypeBlockIds, { blocks, filePath, chapterSlug, includeBlockIds }]];
43
+ const rehypePlugins = [rehypeKatex, rehypeTableCaptions, [rehypeBlockIds, { blocks, filePath, chapterSlug, includeBlockIds, blockAttributes }]];
43
44
  const mod = await evaluate(mdxSource, {
44
45
  ...jsxRuntime,
45
46
  baseUrl: pathToFileURL(filePath).href,
@@ -78,6 +79,7 @@ export function rehypeBlockIds(options = {}) {
78
79
  const chapterSlug = slugPart(options.chapterSlug ?? "document");
79
80
  const sourceSlug = slugPart(path.basename(filePath, path.extname(filePath)));
80
81
  const includeBlockIds = Array.isArray(options.includeBlockIds) ? new Set(options.includeBlockIds) : null;
82
+ const blockAttributes = normalizeBlockAttributes(options.blockAttributes);
81
83
  let counter = 0;
82
84
 
83
85
  return (tree) => {
@@ -87,22 +89,96 @@ export function rehypeBlockIds(options = {}) {
87
89
 
88
90
  const id = `b-${chapterSlug}-${sourceSlug}-${counter}`;
89
91
  counter += 1;
92
+ if (block.name === "table") {
93
+ return applyTableRowBlocks({
94
+ node,
95
+ id,
96
+ blocks,
97
+ filePath,
98
+ chapterSlug,
99
+ includeBlockIds,
100
+ });
101
+ }
90
102
  if (includeBlockIds && !includeBlockIds.has(id)) return false;
91
103
 
92
104
  setDataAttribute(node, "data-openpress-block-id", id);
105
+ const extraAttributes = blockAttributes.get(id);
106
+ if (extraAttributes) {
107
+ for (const [name, value] of Object.entries(extraAttributes)) {
108
+ if (value == null || value === "") continue;
109
+ setDataAttribute(node, name, String(value));
110
+ }
111
+ }
93
112
  blocks.push({
94
113
  id,
95
114
  kind: block.kind,
96
115
  name: block.name,
116
+ text: block.text,
97
117
  filePath,
98
118
  chapterSlug,
99
119
  source: sourcePosition(node.position),
100
120
  });
101
- return true;
121
+ return "skip";
102
122
  });
103
123
  };
104
124
  }
105
125
 
126
+ function applyTableRowBlocks({
127
+ node,
128
+ id,
129
+ blocks,
130
+ filePath,
131
+ chapterSlug,
132
+ includeBlockIds,
133
+ }) {
134
+ const rows = tableBodyRows(node);
135
+ if (rows.length === 0) {
136
+ if (includeBlockIds && !includeBlockIds.has(id)) return false;
137
+ setDataAttribute(node, "data-openpress-block-id", id);
138
+ blocks.push({
139
+ id,
140
+ kind: "element",
141
+ name: "table",
142
+ text: textContent(node).trim() || undefined,
143
+ filePath,
144
+ chapterSlug,
145
+ source: sourcePosition(node.position),
146
+ });
147
+ return "skip";
148
+ }
149
+
150
+ const rowRecords = rows.map((row, index) => ({
151
+ id: `${id}-r${index}`,
152
+ node: row,
153
+ index,
154
+ }));
155
+ const selected = includeBlockIds
156
+ ? rowRecords.filter((row) => includeBlockIds.has(row.id))
157
+ : rowRecords;
158
+ if (selected.length === 0) return false;
159
+
160
+ setDataAttribute(node, "data-openpress-table-id", id);
161
+ const selectedNodes = new Set(selected.map((row) => row.node));
162
+ pruneUnselectedTableRows(node, new Set(rowRecords.map((row) => row.node)), selectedNodes);
163
+ if (selected[0]?.index > 0) stripTableHeader(node);
164
+
165
+ for (const row of selected) {
166
+ setDataAttribute(row.node, "data-openpress-block-id", row.id);
167
+ blocks.push({
168
+ id: row.id,
169
+ kind: "table-row",
170
+ name: "table-row",
171
+ text: textContent(row.node).trim() || undefined,
172
+ filePath,
173
+ chapterSlug,
174
+ tableId: id,
175
+ rowIndex: row.index,
176
+ source: sourcePosition(row.node.position ?? node.position),
177
+ });
178
+ }
179
+ return "skip";
180
+ }
181
+
106
182
  export function remarkBlockOnlyMdx(options = {}) {
107
183
  const filePath = String(options.filePath ?? "document.mdx");
108
184
 
@@ -227,7 +303,7 @@ function normalizeSingleLineDisplayMath(source) {
227
303
 
228
304
  function blockInfo(node) {
229
305
  if (node?.type === "element" && PAGINABLE_TAGS.has(node.tagName)) {
230
- return { kind: "element", name: node.tagName };
306
+ return { kind: "element", name: node.tagName, text: headingText(node) };
231
307
  }
232
308
  if (node?.type === "element" && node.tagName === "span" && hasClassName(node, "katex-display")) {
233
309
  return { kind: "element", name: "math" };
@@ -238,6 +314,49 @@ function blockInfo(node) {
238
314
  return null;
239
315
  }
240
316
 
317
+ function tableBodyRows(table) {
318
+ if (table?.type !== "element" || table.tagName !== "table") return [];
319
+ const rows = [];
320
+ for (const child of table.children ?? []) {
321
+ if (child?.type === "element" && child.tagName === "tbody") {
322
+ for (const row of child.children ?? []) {
323
+ if (row?.type === "element" && row.tagName === "tr") rows.push(row);
324
+ }
325
+ }
326
+ }
327
+ if (rows.length > 0) return rows;
328
+ return (table.children ?? []).filter((child) => child?.type === "element" && child.tagName === "tr");
329
+ }
330
+
331
+ function pruneUnselectedTableRows(node, rowNodes, selectedNodes) {
332
+ if (!Array.isArray(node?.children)) return;
333
+ node.children = node.children.filter((child) => {
334
+ if (!rowNodes.has(child)) return true;
335
+ return selectedNodes.has(child);
336
+ });
337
+ for (const child of node.children) pruneUnselectedTableRows(child, rowNodes, selectedNodes);
338
+ }
339
+
340
+ function stripTableHeader(table) {
341
+ if (!Array.isArray(table?.children)) return;
342
+ table.children = table.children.filter((child) => {
343
+ if (child?.type !== "element") return true;
344
+ return child.tagName !== "caption" && child.tagName !== "thead";
345
+ });
346
+ }
347
+
348
+ function headingText(node) {
349
+ if (!/^h[1-6]$/.test(String(node?.tagName ?? ""))) return undefined;
350
+ return textContent(node).trim() || undefined;
351
+ }
352
+
353
+ function normalizeBlockAttributes(value) {
354
+ if (!value) return new Map();
355
+ if (value instanceof Map) return value;
356
+ if (typeof value === "object") return new Map(Object.entries(value));
357
+ return new Map();
358
+ }
359
+
241
360
  function hasClassName(node, className) {
242
361
  const raw = node?.properties?.className;
243
362
  if (Array.isArray(raw)) return raw.includes(className);
@@ -269,6 +388,7 @@ function visit(node, visitor) {
269
388
  function filterTree(node, visitor) {
270
389
  const keep = visitor(node);
271
390
  if (!keep) return false;
391
+ if (keep === "skip") return true;
272
392
  if (!Array.isArray(node?.children)) return true;
273
393
  node.children = node.children.filter((child) => filterTree(child, visitor));
274
394
  return true;
@@ -2,8 +2,8 @@ import fs from "node:fs/promises";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { buildComponentsCss, buildContentCss } from "../file-utils.mjs";
6
- import { buildChapterScopedCss } from "./chapter-css.mjs";
5
+ import { buildComponentsCss, buildContentCss } from "../runtime/file-utils.mjs";
6
+ import { buildSectionScopedCss } from "./section-css.mjs";
7
7
 
8
8
  const require = createRequire(import.meta.url);
9
9
 
@@ -15,7 +15,7 @@ export async function buildReactMeasurementCss(root, config, workspace) {
15
15
  parts.push(await buildContentCss(root, config));
16
16
  parts.push("\n/* === public/openpress/components.css === */\n");
17
17
  parts.push(await buildComponentsCss(root, config));
18
- const chapterCss = await buildChapterScopedCss(workspace);
18
+ const chapterCss = await buildSectionScopedCss(workspace);
19
19
  if (chapterCss.trim()) {
20
20
  parts.push("\n/* === public/openpress/chapter-scoped.css === */\n");
21
21
  parts.push(chapterCss);
@@ -0,0 +1,122 @@
1
+ import { DEFAULT_PAGE_SAFE_HEIGHT_PX } from "../pagination-constants.mjs";
2
+ import { singleColumnRegionStream } from "./regions.mjs";
3
+
4
+ // Pure region-based block allocator.
5
+ //
6
+ // Greedy bin-packing: walk measured blocks in order, append to the current
7
+ // region until adding the next block would exceed capacity, then advance to
8
+ // the next region. Pages are a derived view (grouping by pageIndex), so the
9
+ // same code paginates single-column, multi-column, and heterogeneous layouts.
10
+ export function allocateBlocksToRegions(measuredBlocks, regionStream) {
11
+ const filled = [];
12
+ const warnings = [];
13
+ let current = regionStream.next();
14
+ if (!current) {
15
+ return { regions: filled, warnings: [{ code: "out-of-regions" }] };
16
+ }
17
+ let currentBlockIds = [];
18
+ let currentHeight = 0;
19
+
20
+ const flush = () => {
21
+ if (currentBlockIds.length === 0) return;
22
+ filled.push({
23
+ regionId: current.id,
24
+ pageIndex: current.pageIndex,
25
+ columnIndex: current.columnIndex,
26
+ blockIds: currentBlockIds,
27
+ });
28
+ currentBlockIds = [];
29
+ currentHeight = 0;
30
+ };
31
+
32
+ for (const block of measuredBlocks ?? []) {
33
+ const id = String(block?.id ?? "");
34
+ if (!id) continue;
35
+ const height = Math.max(0, Number(block.height) || 0);
36
+
37
+ if (height > current.capacity) {
38
+ warnings.push({
39
+ code: "block-overflows-region",
40
+ blockId: id,
41
+ height,
42
+ regionCapacity: current.capacity,
43
+ regionId: current.id,
44
+ pageIndex: current.pageIndex,
45
+ });
46
+ }
47
+
48
+ if (currentBlockIds.length > 0 && currentHeight + height > current.capacity) {
49
+ flush();
50
+ const next = regionStream.next();
51
+ if (!next) {
52
+ warnings.push({ code: "out-of-regions", blockId: id });
53
+ break;
54
+ }
55
+ current = next;
56
+ }
57
+
58
+ currentBlockIds.push(id);
59
+ currentHeight += height;
60
+ }
61
+
62
+ flush();
63
+ return { regions: filled, warnings };
64
+ }
65
+
66
+ // Derive a flat pages[] view from filled regions. Blocks within a page are
67
+ // emitted in column order (col 0, col 1, ...) — matching how readers consume
68
+ // a multi-column page (left-to-right, top-to-bottom).
69
+ export function pagesFromRegions(filledRegions) {
70
+ const byPage = new Map();
71
+ for (const region of filledRegions) {
72
+ if (!byPage.has(region.pageIndex)) byPage.set(region.pageIndex, []);
73
+ byPage.get(region.pageIndex).push(region);
74
+ }
75
+ const pages = [];
76
+ for (const [pageIndex, regionsOnPage] of [...byPage.entries()].sort((a, b) => a[0] - b[0])) {
77
+ const sorted = regionsOnPage.slice().sort((a, b) => a.columnIndex - b.columnIndex);
78
+ const blockIds = sorted.flatMap((r) => r.blockIds);
79
+ pages.push({
80
+ pageIndex,
81
+ blockIds,
82
+ breakAfter: blockIds.at(-1),
83
+ });
84
+ }
85
+ return pages;
86
+ }
87
+
88
+ // Public wrapper preserving the legacy (blocks, { pageSafeHeightPx }) signature.
89
+ // New code can pass a `regions` stream directly to opt into multi-column or
90
+ // heterogeneous layouts.
91
+ export function paginateMeasuredBlocks(measuredBlocks, options = {}) {
92
+ const { pageSafeHeightPx = DEFAULT_PAGE_SAFE_HEIGHT_PX, regions } = options;
93
+ const safeHeight = positiveNumber(pageSafeHeightPx, DEFAULT_PAGE_SAFE_HEIGHT_PX);
94
+ const stream = regions ?? singleColumnRegionStream({ pageSafeHeightPx: safeHeight });
95
+ const { regions: filledRegions, warnings } = allocateBlocksToRegions(measuredBlocks, stream);
96
+ const pages = pagesFromRegions(filledRegions);
97
+ return {
98
+ pages,
99
+ regions: filledRegions,
100
+ warnings: warnings.map((w) => mapWarning(w, safeHeight)),
101
+ };
102
+ }
103
+
104
+ // Translate the new region-shaped warnings back to the legacy
105
+ // `block-overflows-page` schema that document-export.mjs and downstream
106
+ // consumers expect. Once consumers migrate, this can drop.
107
+ function mapWarning(warning, pageSafeHeightPx) {
108
+ if (warning.code === "block-overflows-region") {
109
+ return {
110
+ code: "block-overflows-page",
111
+ blockId: warning.blockId,
112
+ height: warning.height,
113
+ pageSafeHeightPx,
114
+ };
115
+ }
116
+ return warning;
117
+ }
118
+
119
+ function positiveNumber(value, fallback) {
120
+ const number = Number(value);
121
+ return Number.isFinite(number) && number > 0 ? number : fallback;
122
+ }
@@ -0,0 +1,81 @@
1
+ // A Region is a fillable area on a page — the engine treats pagination as a
2
+ // stream of regions consumed in order. One single-column page is "one region";
3
+ // a two-column page is two regions on the same pageIndex; a newspaper page
4
+ // could be three (main-left, main-right, sidebar).
5
+ //
6
+ // A RegionStream is a lazy iterator that yields regions on demand. The
7
+ // allocator pulls the next region when the current one is full. This makes
8
+ // "multi-column" and "newspaper-style mixed layout" the same code path as
9
+ // single-column — only the stream differs.
10
+ //
11
+ // Shape:
12
+ // Region = { id: string, capacity: number, pageIndex: number, columnIndex: number }
13
+ // RegionStream = { next(): Region } | Iterable<Region>
14
+
15
+ /**
16
+ * Default region stream: an infinite sequence of single-column pages.
17
+ * Equivalent to today's "one safe-height page after another" behavior.
18
+ */
19
+ export function singleColumnRegionStream({ pageSafeHeightPx }) {
20
+ return iteratorFromGenerator(function* () {
21
+ let pageIndex = 0;
22
+ while (true) {
23
+ yield {
24
+ id: `page-${pageIndex}-col-0`,
25
+ capacity: pageSafeHeightPx,
26
+ pageIndex,
27
+ columnIndex: 0,
28
+ };
29
+ pageIndex++;
30
+ }
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Multi-column region stream: each page yields `columnCount` regions in order,
36
+ * all sharing the same pageIndex but with increasing columnIndex.
37
+ *
38
+ * Blocks fill column 0 first, then column 1, then advance to the next page's
39
+ * column 0 — same greedy semantics as single-column, just more regions per page.
40
+ */
41
+ export function multiColumnRegionStream({ pageSafeHeightPx, columnCount }) {
42
+ const cols = Math.max(1, Math.floor(columnCount) || 1);
43
+ return iteratorFromGenerator(function* () {
44
+ let pageIndex = 0;
45
+ while (true) {
46
+ for (let col = 0; col < cols; col++) {
47
+ yield {
48
+ id: `page-${pageIndex}-col-${col}`,
49
+ capacity: pageSafeHeightPx,
50
+ pageIndex,
51
+ columnIndex: col,
52
+ };
53
+ }
54
+ pageIndex++;
55
+ }
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Build a region stream from an explicit list of regions. Useful for
61
+ * heterogeneous layouts (e.g. a research-article first page with a wide
62
+ * abstract region on top + two narrow columns below). Stream ends when
63
+ * the list is exhausted — the caller is responsible for providing enough
64
+ * regions; the allocator emits an `out-of-regions` warning otherwise.
65
+ */
66
+ export function fixedRegionStream(regions) {
67
+ const list = Array.isArray(regions) ? regions : [];
68
+ return iteratorFromGenerator(function* () {
69
+ for (const region of list) yield region;
70
+ });
71
+ }
72
+
73
+ function iteratorFromGenerator(genFn) {
74
+ const iter = genFn();
75
+ return {
76
+ next() {
77
+ const { value, done } = iter.next();
78
+ return done ? null : value;
79
+ },
80
+ };
81
+ }
@@ -1,121 +1,9 @@
1
- import { chromium } from "playwright";
2
- import {
3
- DEFAULT_PAGE_SAFE_HEIGHT_PX,
4
- PAGE_BODY_FIT_SAFETY_MAX_PX,
5
- PAGE_BODY_FIT_SAFETY_RATIO,
6
- } from "./pagination-constants.mjs";
7
-
8
- const DEFAULT_VIEWPORT = { width: 794, height: 1123 };
9
-
10
- export function paginateMeasuredBlocks(measuredBlocks, { pageSafeHeightPx = DEFAULT_PAGE_SAFE_HEIGHT_PX } = {}) {
11
- const safeHeight = positiveNumber(pageSafeHeightPx, DEFAULT_PAGE_SAFE_HEIGHT_PX);
12
- const pages = [];
13
- const warnings = [];
14
- let currentBlockIds = [];
15
- let currentHeight = 0;
16
-
17
- for (const block of measuredBlocks ?? []) {
18
- const id = String(block?.id ?? "");
19
- if (!id) continue;
20
- const height = Math.max(0, Number(block.height) || 0);
21
-
22
- if (height > safeHeight) {
23
- warnings.push({
24
- code: "block-overflows-page",
25
- blockId: id,
26
- height,
27
- pageSafeHeightPx: safeHeight,
28
- });
29
- }
30
-
31
- if (currentBlockIds.length > 0 && currentHeight + height > safeHeight) {
32
- pages.push(pageRecord(pages.length, currentBlockIds));
33
- currentBlockIds = [];
34
- currentHeight = 0;
35
- }
36
-
37
- currentBlockIds.push(id);
38
- currentHeight += height;
39
- }
40
-
41
- if (currentBlockIds.length > 0) {
42
- pages.push(pageRecord(pages.length, currentBlockIds));
43
- }
44
-
45
- return { pages, warnings };
46
- }
47
-
48
- export async function measureBlocksInChromium({
49
- html,
50
- css = "",
51
- pageSafeHeightPx,
52
- viewport = DEFAULT_VIEWPORT,
53
- } = {}) {
54
- const browser = await chromium.launch();
55
- try {
56
- const page = await browser.newPage({ viewport });
57
- await page.setContent(measurementDocument(html, css), { waitUntil: "load" });
58
- await page.evaluate(async () => {
59
- if (document.fonts?.ready) await document.fonts.ready;
60
- });
61
- const measurements = await page.evaluate(() => (
62
- Array.from(document.querySelectorAll("[data-openpress-block-id]")).map((element) => ({
63
- id: element.getAttribute("data-openpress-block-id"),
64
- height: element.getBoundingClientRect().height,
65
- }))
66
- ));
67
- const safeHeight = positiveNumber(pageSafeHeightPx, null)
68
- ?? await measurePageSafeHeight(page)
69
- ?? DEFAULT_PAGE_SAFE_HEIGHT_PX;
70
- return {
71
- measurements,
72
- pageSafeHeightPx: safeHeight,
73
- ...paginateMeasuredBlocks(measurements, { pageSafeHeightPx: safeHeight }),
74
- };
75
- } finally {
76
- await browser.close();
77
- }
78
- }
79
-
80
- async function measurePageSafeHeight(page) {
81
- return page.evaluate(({ ratio, maxPx }) => {
82
- const body = document.querySelector(".reader-page[data-page-kind='content'] .page-body")
83
- ?? document.querySelector(".reader-page--content .page-body")
84
- ?? document.querySelector(".page-body");
85
- if (!body) return null;
86
- const height = body.getBoundingClientRect().height;
87
- if (!Number.isFinite(height) || height <= 0) return null;
88
- const safetyInset = Math.min(maxPx, Math.max(0, height * ratio));
89
- return Math.max(1, height - safetyInset);
90
- }, {
91
- ratio: PAGE_BODY_FIT_SAFETY_RATIO,
92
- maxPx: PAGE_BODY_FIT_SAFETY_MAX_PX,
93
- });
94
- }
95
-
96
- function pageRecord(pageIndex, blockIds) {
97
- return {
98
- pageIndex,
99
- blockIds,
100
- breakAfter: blockIds.at(-1),
101
- };
102
- }
103
-
104
- function positiveNumber(value, fallback) {
105
- const number = Number(value);
106
- return Number.isFinite(number) && number > 0 ? number : fallback;
107
- }
108
-
109
- function measurementDocument(html = "", css = "") {
110
- return `<!doctype html>
111
- <html>
112
- <head>
113
- <meta charset="utf-8">
114
- <style>
115
- body { margin: 0; }
116
- ${css}
117
- </style>
118
- </head>
119
- <body>${html}</body>
120
- </html>`;
121
- }
1
+ // Public surface for the build-time region allocator.
2
+ //
3
+ // The Press Tree pipeline measures MdxArea capacities and block heights in
4
+ // `engine/react/pipeline/frame-measurement.mjs` and runs allocation through
5
+ // these helpers. The region kernel is also usable on its own for custom
6
+ // pipelines or unit tests.
7
+
8
+ export { paginateMeasuredBlocks, allocateBlocksToRegions, pagesFromRegions } from "./pagination/allocator.mjs";
9
+ export { singleColumnRegionStream, multiColumnRegionStream, fixedRegionStream } from "./pagination/regions.mjs";