@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.
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
@@ -0,0 +1,294 @@
1
+ // @open-press/core/numbering — label formatters for AI authoring layer.
2
+ //
3
+ // Core stays neutral about how chapter / section / topic numbers should look.
4
+ // Source resolution exposes raw integers via outline items
5
+ // (`chapterNumber`, `sectionIndex`, `topicIndex`). This module is a toolbox
6
+ // of pure functions that turn those integers into human-readable labels —
7
+ // Chinese numerals, Roman numerals, alphabet letters, padded decimals, etc.
8
+ //
9
+ // Workspaces import what they need and compose them inside their own
10
+ // `<Page>` / `<Toc>` components. The core engine never calls these.
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // CJK numeral systems
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const CJK_INFORMAL = ["〇", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
17
+ const CJK_FORMAL = ["零", "壹", "貳", "參", "肆", "伍", "陸", "柒", "捌", "玖"];
18
+ const CJK_HEAVENLY_STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
19
+ const CJK_EARTHLY_BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
20
+
21
+ /**
22
+ * Informal CJK numerals (一二三...). Handles 0 to 9999.
23
+ *
24
+ * toCjk(1) → "一"
25
+ * toCjk(10) → "十"
26
+ * toCjk(15) → "十五"
27
+ * toCjk(23) → "二十三"
28
+ * toCjk(100) → "一百"
29
+ * toCjk(108) → "一百零八"
30
+ * toCjk(2024) → "二千零二十四"
31
+ */
32
+ export function toCjk(n: number): string {
33
+ if (!Number.isFinite(n) || n < 0) return String(n);
34
+ return formatCjkDigits(Math.floor(n), CJK_INFORMAL);
35
+ }
36
+
37
+ /**
38
+ * Formal CJK numerals (壹貳參...). Used in legal / financial contexts.
39
+ *
40
+ * toCjkFormal(1) → "壹"
41
+ * toCjkFormal(100) → "壹佰"
42
+ */
43
+ export function toCjkFormal(n: number): string {
44
+ if (!Number.isFinite(n) || n < 0) return String(n);
45
+ return formatCjkDigits(Math.floor(n), CJK_FORMAL, { formal: true });
46
+ }
47
+
48
+ /**
49
+ * Heavenly Stems cycle (甲乙丙丁戊己庚辛壬癸 — 10 only).
50
+ * Cycles for n > 10; n=11 → 甲 again.
51
+ *
52
+ * toCjkHeavenlyStem(1) → "甲"
53
+ * toCjkHeavenlyStem(10) → "癸"
54
+ * toCjkHeavenlyStem(11) → "甲"
55
+ */
56
+ export function toCjkHeavenlyStem(n: number): string {
57
+ if (!Number.isFinite(n) || n < 1) return String(n);
58
+ return CJK_HEAVENLY_STEMS[(Math.floor(n) - 1) % 10]!;
59
+ }
60
+
61
+ /**
62
+ * Earthly Branches cycle (子丑寅卯辰巳午未申酉戌亥 — 12 only).
63
+ * Cycles for n > 12.
64
+ */
65
+ export function toCjkEarthlyBranch(n: number): string {
66
+ if (!Number.isFinite(n) || n < 1) return String(n);
67
+ return CJK_EARTHLY_BRANCHES[(Math.floor(n) - 1) % 12]!;
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Roman numerals
72
+ // ---------------------------------------------------------------------------
73
+
74
+ const ROMAN_TABLE: Array<[number, string]> = [
75
+ [1000, "M"], [900, "CM"], [500, "D"], [400, "CD"],
76
+ [100, "C"], [90, "XC"], [50, "L"], [40, "XL"],
77
+ [10, "X"], [9, "IX"], [5, "V"], [4, "IV"],
78
+ [1, "I"],
79
+ ];
80
+
81
+ /**
82
+ * Roman numerals (I, II, III, …, MMXX). Uppercase by default.
83
+ * Pass `{ upper: false }` for lowercase (typical front-matter page numbering).
84
+ *
85
+ * toRoman(1) → "I"
86
+ * toRoman(4) → "IV"
87
+ * toRoman(2024) → "MMXXIV"
88
+ * toRoman(3, { upper: false }) → "iii"
89
+ */
90
+ export function toRoman(n: number, opts: { upper?: boolean } = {}): string {
91
+ if (!Number.isFinite(n) || n < 1) return String(n);
92
+ let value = Math.floor(n);
93
+ let out = "";
94
+ for (const [num, sym] of ROMAN_TABLE) {
95
+ while (value >= num) {
96
+ out += sym;
97
+ value -= num;
98
+ }
99
+ }
100
+ return opts.upper === false ? out.toLowerCase() : out;
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Latin alphabet (a, b, c, …, z, aa, ab, …)
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * Spreadsheet-column-style alphabet labels.
109
+ *
110
+ * toAlpha(1) → "a"
111
+ * toAlpha(26) → "z"
112
+ * toAlpha(27) → "aa"
113
+ * toAlpha(53) → "ba"
114
+ * toAlpha(1, { upper: true }) → "A"
115
+ */
116
+ export function toAlpha(n: number, opts: { upper?: boolean } = {}): string {
117
+ if (!Number.isFinite(n) || n < 1) return String(n);
118
+ let value = Math.floor(n);
119
+ let out = "";
120
+ while (value > 0) {
121
+ const rem = (value - 1) % 26;
122
+ out = String.fromCharCode(97 + rem) + out;
123
+ value = Math.floor((value - 1) / 26);
124
+ }
125
+ return opts.upper ? out.toUpperCase() : out;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Zero-padded decimal
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * Zero-padded decimal — `width` is the minimum number of digits.
134
+ *
135
+ * toPadded(1) → "01"
136
+ * toPadded(1, { width: 3 }) → "001"
137
+ * toPadded(123) → "123"
138
+ */
139
+ export function toPadded(n: number, opts: { width?: number } = {}): string {
140
+ if (!Number.isFinite(n)) return String(n);
141
+ const width = Math.max(1, opts.width ?? 2);
142
+ return String(Math.floor(n)).padStart(width, "0");
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Composable label templates
147
+ // ---------------------------------------------------------------------------
148
+
149
+ export type NumberFormat =
150
+ | "decimal"
151
+ | "decimal-padded"
152
+ | "cjk"
153
+ | "cjk-formal"
154
+ | "cjk-heavenly-stem"
155
+ | "cjk-earthly-branch"
156
+ | "roman"
157
+ | "roman-lower"
158
+ | "alpha"
159
+ | "alpha-upper";
160
+
161
+ export interface ChapterLabelOptions {
162
+ format?: NumberFormat;
163
+ prefix?: string;
164
+ suffix?: string;
165
+ width?: number;
166
+ }
167
+
168
+ /**
169
+ * Compose a chapter label like "第一章" / "Chapter 1" / "01" / "Part IV".
170
+ *
171
+ * chapterLabel(1)
172
+ * → "第一章" (default: cjk informal, prefix 第, suffix 章)
173
+ * chapterLabel(1, { format: "decimal-padded", prefix: "Chapter " })
174
+ * → "Chapter 01"
175
+ * chapterLabel(4, { format: "roman", prefix: "Part " })
176
+ * → "Part IV"
177
+ * chapterLabel(2, { prefix: "", suffix: "" })
178
+ * → "二"
179
+ */
180
+ export function chapterLabel(n: number, opts: ChapterLabelOptions = {}): string {
181
+ const { format = "cjk", prefix = "第", suffix = "章", width } = opts;
182
+ return `${prefix}${formatNumber(n, format, width)}${suffix}`;
183
+ }
184
+
185
+ export interface SectionLabelOptions {
186
+ format?: NumberFormat;
187
+ separator?: string;
188
+ prefix?: string;
189
+ suffix?: string;
190
+ }
191
+
192
+ /**
193
+ * Compose a section label like "1.1" / "1.1.2" / "一-1".
194
+ *
195
+ * Accepts variadic counters for nesting:
196
+ * sectionLabel(1, 2)
197
+ * → "1.2"
198
+ * sectionLabel(1, 2, 3)
199
+ * → "1.2.3"
200
+ * sectionLabel(1, 2, { format: "cjk", separator: "之" })
201
+ * ⚠ note: pass options as last argument when using variadic counters;
202
+ * prefer `sectionLabelOf([1, 2], { … })` below for clarity.
203
+ */
204
+ export function sectionLabel(...counters: number[]): string {
205
+ return sectionLabelOf(counters);
206
+ }
207
+
208
+ /**
209
+ * Explicit-array variant of `sectionLabel` so options can be passed cleanly.
210
+ *
211
+ * sectionLabelOf([1, 2], { separator: "-" }) → "1-2"
212
+ * sectionLabelOf([1, 2, 3], { format: "alpha" }) → "a.b.c"
213
+ * sectionLabelOf([1, 1], { format: "cjk", separator: "-" }) → "一-一"
214
+ */
215
+ export function sectionLabelOf(counters: number[], opts: SectionLabelOptions = {}): string {
216
+ const { format = "decimal", separator = ".", prefix = "", suffix = "" } = opts;
217
+ const parts = counters.map((c) => formatNumber(c, format));
218
+ return `${prefix}${parts.join(separator)}${suffix}`;
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Generic dispatch — `format` -> formatter
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Format a single number using one of the named formats. Useful when the
227
+ * caller already knows which format string to use (e.g. from config).
228
+ *
229
+ * formatNumber(7, "cjk") → "七"
230
+ * formatNumber(7, "roman-lower") → "vii"
231
+ * formatNumber(3, "decimal-padded", 3) → "003"
232
+ */
233
+ export function formatNumber(n: number, format: NumberFormat, width?: number): string {
234
+ switch (format) {
235
+ case "decimal": return String(Math.floor(n));
236
+ case "decimal-padded": return toPadded(n, { width });
237
+ case "cjk": return toCjk(n);
238
+ case "cjk-formal": return toCjkFormal(n);
239
+ case "cjk-heavenly-stem": return toCjkHeavenlyStem(n);
240
+ case "cjk-earthly-branch": return toCjkEarthlyBranch(n);
241
+ case "roman": return toRoman(n, { upper: true });
242
+ case "roman-lower": return toRoman(n, { upper: false });
243
+ case "alpha": return toAlpha(n);
244
+ case "alpha-upper": return toAlpha(n, { upper: true });
245
+ default: return String(n);
246
+ }
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // CJK digit formatter (internal)
251
+ // ---------------------------------------------------------------------------
252
+
253
+ const CJK_UNITS = ["", "十", "百", "千"];
254
+ const CJK_UNITS_FORMAL = ["", "拾", "佰", "仟"];
255
+
256
+ // Gap-zero indicator used between non-zero digits when reading aloud
257
+ // (e.g. 一百零八). Distinct from 〇/零 as a literal zero digit.
258
+ const CJK_GAP_ZERO = "零";
259
+
260
+ function formatCjkDigits(n: number, digits: string[], opts: { formal?: boolean } = {}): string {
261
+ if (n === 0) return digits[0]!;
262
+ if (n < 0) return `負${formatCjkDigits(-n, digits, opts)}`;
263
+ if (n >= 10000) {
264
+ const wan = Math.floor(n / 10000);
265
+ const rest = n % 10000;
266
+ if (rest === 0) return `${formatCjkDigits(wan, digits, opts)}萬`;
267
+ const restStr = formatCjkDigits(rest, digits, opts);
268
+ const padded = rest < 1000 ? `${CJK_GAP_ZERO}${restStr}` : restStr;
269
+ return `${formatCjkDigits(wan, digits, opts)}萬${padded}`;
270
+ }
271
+ const units = opts.formal ? CJK_UNITS_FORMAL : CJK_UNITS;
272
+ const str = String(n);
273
+ let out = "";
274
+ let zeroPending = false;
275
+ for (let i = 0; i < str.length; i++) {
276
+ const digit = Number(str[i]);
277
+ const unit = str.length - 1 - i;
278
+ if (digit === 0) {
279
+ zeroPending = true;
280
+ continue;
281
+ }
282
+ if (zeroPending && out.length > 0) {
283
+ out += CJK_GAP_ZERO;
284
+ zeroPending = false;
285
+ }
286
+ // Special case: leading "一十" usually written as just "十" in informal CJK
287
+ if (!opts.formal && digit === 1 && unit === 1 && i === 0) {
288
+ out += units[unit];
289
+ } else {
290
+ out += digits[digit] + units[unit];
291
+ }
292
+ }
293
+ return out;
294
+ }
@@ -9,13 +9,12 @@ import {
9
9
  type RefObject,
10
10
  } from "react";
11
11
  import { BookOpen, ExternalLink, X } from "lucide-react";
12
+ import { createAnchorPageMap, resolveAnchorPageIndex } from "./anchorMap";
12
13
  import { collectBookmarkIndex } from "./indexes";
13
14
  import type { InspectorState } from "./inspector";
14
- import { normalizeContentCaptions, paginateSourcePages, type PaginatedPage } from "./pagination";
15
15
  import { getProjectIdentity } from "./projectIdentity";
16
- import { hasBuildTimePagination } from "./reactDocumentMetadata";
17
16
  import { useReaderRuntime } from "./readerRuntime";
18
- import { scheduleBrowserFrame, waitForBrowserFrame } from "./frameScheduler";
17
+ import { scheduleBrowserFrame } from "./frameScheduler";
19
18
  import type { DeploymentInfo, ReaderDocument, HtmlPageBlock } from "./types";
20
19
  import { Bookmarks, CurrentPagePanel } from "./workbenchPanels";
21
20
  import type { DisplayPage } from "./workbenchTypes";
@@ -38,15 +37,9 @@ export function PublicViewer({
38
37
  deploymentInfo?: DeploymentInfo;
39
38
  }) {
40
39
  const sourceContainerRef = useRef<HTMLDivElement | null>(null);
41
- const numberedPages = useMemo(() => numberSourceHeadings(pages), [pages]);
40
+ const displayPages = pages;
42
41
  const viewModeState = useViewMode();
43
42
  const { viewMode } = viewModeState;
44
- const buildTimePaginated = hasBuildTimePagination(document);
45
- const paginatedPages = usePaginatedPages(numberedPages, sourceContainerRef, viewMode === "paged" && !buildTimePaginated);
46
- const displayPages: DisplayPage[] = viewMode === "paged" && !buildTimePaginated
47
- ? (paginatedPages ?? numberedPages)
48
- : numberedPages;
49
- const paginatedReady = viewMode === "reading" || buildTimePaginated || Boolean(paginatedPages);
50
43
  const bookmarks = collectBookmarkIndex(displayPages);
51
44
  const anchorPageMap = useMemo(() => createAnchorPageMap(displayPages), [displayPages]);
52
45
  const reader = useReaderRuntime({
@@ -87,7 +80,6 @@ export function PublicViewer({
87
80
  className={appClassName}
88
81
  data-openpress-react-runtime="true"
89
82
  data-openpress-view-mode={viewMode}
90
- data-openpress-pagination={paginatedReady ? "ready" : "pending"}
91
83
  >
92
84
  {drawerOpen && (
93
85
  <div className="openpress-public-scrim" aria-hidden="true" onClick={reader.toggleRightPanel} />
@@ -102,7 +94,6 @@ export function PublicViewer({
102
94
  pages={displayPages}
103
95
  currentPageIndex={reader.currentPageIndex}
104
96
  devMode={false}
105
- paginatedReady={paginatedReady}
106
97
  sourceContainerRef={sourceContainerRef}
107
98
  registerPage={reader.registerPage}
108
99
  onInternalAnchorNavigate={selectPublicAnchor}
@@ -196,12 +187,8 @@ export function PrintDocument({
196
187
  pages: Array<HtmlPageBlock>;
197
188
  style: CSSProperties;
198
189
  }) {
199
- const numberedPages = useMemo(() => numberSourceHeadings(pages), [pages]);
200
190
  const sourceContainerRef = useRef<HTMLDivElement | null>(null);
201
- const buildTimePaginated = hasBuildTimePagination(document);
202
- const paginatedPages = usePaginatedPages(numberedPages, sourceContainerRef, !buildTimePaginated);
203
- const displayPages: DisplayPage[] = buildTimePaginated ? numberedPages : (paginatedPages ?? numberedPages);
204
- const paginatedReady = buildTimePaginated || Boolean(paginatedPages);
191
+ const displayPages = pages;
205
192
  const registerPage = () => () => undefined;
206
193
 
207
194
  return (
@@ -209,14 +196,12 @@ export function PrintDocument({
209
196
  className="openpress-print-document"
210
197
  style={style}
211
198
  data-openpress-print-document="true"
212
- data-openpress-pagination={paginatedReady ? "ready" : "pending"}
213
199
  aria-label={`${document.meta.title} PDF 輸出`}
214
200
  >
215
201
  <PublicPage
216
202
  pages={displayPages}
217
203
  currentPageIndex={0}
218
204
  devMode={false}
219
- paginatedReady={paginatedReady}
220
205
  sourceContainerRef={sourceContainerRef}
221
206
  registerPage={registerPage}
222
207
  exposeSourceData
@@ -225,137 +210,15 @@ export function PrintDocument({
225
210
  );
226
211
  }
227
212
 
228
- function usePaginatedPages(
229
- pages: Array<HtmlPageBlock>,
230
- sourceContainerRef: RefObject<HTMLDivElement | null>,
231
- enabled = true,
232
- ) {
233
- const [paginatedPages, setPaginatedPages] = useState<PaginatedPage[] | null>(null);
234
-
235
- useLayoutEffect(() => {
236
- setPaginatedPages(null);
237
- }, [pages]);
238
-
239
- useLayoutEffect(() => {
240
- if (!enabled) return undefined;
241
- if (paginatedPages) return undefined;
242
- const sourceContainer = sourceContainerRef.current;
243
- if (!sourceContainer) return undefined;
244
-
245
- let cancelled = false;
246
-
247
- void (async () => {
248
- await waitForPaginationAssets(sourceContainer);
249
- await waitForBrowserFrame();
250
- if (cancelled) return;
251
-
252
- const nextPages = paginateSourcePages(sourceContainer, pages);
253
- if (!cancelled) setPaginatedPages(nextPages);
254
- })();
255
-
256
- return () => {
257
- cancelled = true;
258
- };
259
- }, [enabled, pages, paginatedPages, sourceContainerRef]);
260
-
261
- return paginatedPages;
262
- }
263
-
264
213
  function viewportAllowsPagedMode() {
265
214
  if (typeof window === "undefined") return true;
266
215
  return window.innerWidth >= PAGED_VIEW_MIN_WIDTH;
267
216
  }
268
217
 
269
- export function numberSourceHeadings(pages: Array<HtmlPageBlock>): Array<HtmlPageBlock> {
270
- if (typeof document === "undefined") return pages;
271
-
272
- let chapterCounter = 0;
273
- let sectionCounter = 0;
274
- let topicCounter = 0;
275
-
276
- const parsedPages = pages.map((page) => {
277
- const template = document.createElement("template");
278
- template.innerHTML = page.html;
279
- const readerPage = template.content.querySelector<HTMLElement>(".reader-page");
280
- if (!readerPage || !isContentPage(readerPage)) return { page, template, readerPage };
281
-
282
- readerPage.querySelectorAll<HTMLElement>("h2, h3, h4").forEach((heading) => {
283
- if (heading.tagName === "H2") {
284
- chapterCounter += 1;
285
- sectionCounter = 0;
286
- topicCounter = 0;
287
- ensureHeadingId(heading, `section-${String(chapterCounter).padStart(2, "0")}`);
288
- heading.dataset.chapter = String(chapterCounter).padStart(2, "0");
289
- heading.dataset.chapterMarker = `#${chapterCounter}`;
290
- return;
291
- }
292
-
293
- if (heading.tagName === "H3") {
294
- sectionCounter += 1;
295
- topicCounter = 0;
296
- ensureHeadingId(heading, `section-${chapterCounter}-${sectionCounter}`);
297
- heading.dataset.section = `${chapterCounter}.${sectionCounter}`;
298
- return;
299
- }
300
-
301
- if (heading.tagName === "H4") {
302
- topicCounter += 1;
303
- if (chapterCounter > 0 && sectionCounter > 0) {
304
- ensureHeadingId(heading, `section-${chapterCounter}-${sectionCounter}-${topicCounter}`);
305
- heading.dataset.topic = `${chapterCounter}.${sectionCounter}.${topicCounter}`;
306
- }
307
- }
308
- });
309
-
310
- return { page, template, readerPage };
311
- });
312
-
313
- normalizeContentCaptions(
314
- parsedPages
315
- .map((entry) => entry.readerPage)
316
- .filter((page): page is HTMLElement => Boolean(page)),
317
- );
318
-
319
- return parsedPages.map(({ page, template }) => {
320
- const html = template.innerHTML;
321
- const anchors = collectElementIds(template.content);
322
- return html === page.html && anchorsAreEqual(page.anchors, anchors) ? page : { ...page, html, anchors };
323
- });
324
- }
325
-
326
- function isContentPage(page: HTMLElement) {
327
- return page.dataset.pageKind === "content";
328
- }
329
-
330
- async function waitForPaginationAssets(scope: HTMLElement) {
331
- await document.fonts?.ready;
332
- const images = Array.from(scope.querySelectorAll<HTMLImageElement>("img"));
333
- await Promise.all(images.map(waitForImage));
334
- await waitForBrowserFrame();
335
- }
336
-
337
- async function waitForImage(img: HTMLImageElement) {
338
- if (!img.complete) {
339
- await new Promise<void>((resolve) => {
340
- const settle = () => {
341
- img.removeEventListener("load", settle);
342
- img.removeEventListener("error", settle);
343
- resolve();
344
- };
345
-
346
- img.addEventListener("load", settle, { once: true });
347
- img.addEventListener("error", settle, { once: true });
348
- });
349
- }
350
-
351
- await img.decode?.().catch(() => undefined);
352
- }
353
-
354
218
  export function PublicPage({
355
219
  pages,
356
220
  currentPageIndex,
357
221
  devMode,
358
- paginatedReady,
359
222
  sourceContainerRef,
360
223
  registerPage,
361
224
  exposeSourceData = false,
@@ -365,7 +228,6 @@ export function PublicPage({
365
228
  pages: DisplayPage[];
366
229
  currentPageIndex: number;
367
230
  devMode: boolean;
368
- paginatedReady: boolean;
369
231
  sourceContainerRef: RefObject<HTMLDivElement | null>;
370
232
  registerPage: (pageIndex: number) => RefCallback<HTMLElement>;
371
233
  exposeSourceData?: boolean;
@@ -408,11 +270,6 @@ export function PublicPage({
408
270
  data-source-path={exposeSourceData ? page.source?.path : undefined}
409
271
  data-source-file={exposeSourceData ? page.source?.file : undefined}
410
272
  >
411
- {devMode && !paginatedReady && page.source?.path ? (
412
- <div className="openpress-html-page__toolbar">
413
- <code>{page.source.path}</code>
414
- </div>
415
- ) : null}
416
273
  <div className="openpress-html-page__html" dangerouslySetInnerHTML={{ __html: page.html }} />
417
274
  </div>
418
275
  ))}
@@ -420,45 +277,6 @@ export function PublicPage({
420
277
  );
421
278
  }
422
279
 
423
- export function createAnchorPageMap(pages: DisplayPage[]) {
424
- const map = new Map<string, number>();
425
- pages.forEach((page, index) => {
426
- page.anchors?.forEach((anchor) => {
427
- if (anchor && !map.has(anchor)) map.set(anchor, index);
428
- });
429
- });
430
- return map;
431
- }
432
-
433
- export function resolveAnchorPageIndex(
434
- anchorPageMap: Map<string, number>,
435
- pageCount: number,
436
- anchorId: string,
437
- pageIndex?: number,
438
- ): number | null {
439
- if (typeof pageIndex === "number" && Number.isInteger(pageIndex) && pageIndex >= 0 && pageIndex < pageCount) return pageIndex;
440
- const mapped = anchorPageMap.get(anchorId);
441
- return mapped === undefined ? null : mapped;
442
- }
443
-
444
- function ensureHeadingId(heading: HTMLElement, fallbackId: string) {
445
- if (heading.id) return;
446
- heading.id = fallbackId;
447
- }
448
-
449
- function collectElementIds(scope: ParentNode) {
450
- const ids: string[] = [];
451
- scope.querySelectorAll<HTMLElement>("[id]").forEach((el) => {
452
- if (el.id && !ids.includes(el.id)) ids.push(el.id);
453
- });
454
- return ids;
455
- }
456
-
457
- function anchorsAreEqual(left: string[] | undefined, right: string[]) {
458
- if (!left || left.length !== right.length) return false;
459
- return left.every((item, index) => item === right[index]);
460
- }
461
-
462
280
  function safeDecodeAnchor(value: string) {
463
281
  if (!value) return "";
464
282
  try {
@@ -1,26 +1,12 @@
1
1
  import type {
2
2
  ReaderDocument,
3
- BuildPagination,
4
3
  SourceBlock,
5
4
  } from "./types";
6
5
 
7
- export const REACT_MDX_SOURCE_TYPE = "openpress-react-mdx";
8
- export const BUILD_TIME_PAGINATION_MODE = "build-time-block-measurement";
6
+ export const PRESS_TREE_MDX_SOURCE_TYPE = "openpress-press-tree-mdx";
9
7
 
10
8
  export function isReactMdxDocument(document: Pick<ReaderDocument, "source"> | null | undefined) {
11
- return document?.source?.type === REACT_MDX_SOURCE_TYPE;
12
- }
13
-
14
- export function hasBuildTimePagination(document: Pick<ReaderDocument, "source"> | null | undefined) {
15
- return getBuildPagination(document)?.mode === BUILD_TIME_PAGINATION_MODE;
16
- }
17
-
18
- export function getBuildPagination(
19
- document: Pick<ReaderDocument, "source"> | null | undefined,
20
- ): BuildPagination | null {
21
- if (!isReactMdxDocument(document)) return null;
22
- const pagination = document?.source?.pagination;
23
- return pagination && typeof pagination.mode === "string" ? pagination : null;
9
+ return document?.source?.type === PRESS_TREE_MDX_SOURCE_TYPE;
24
10
  }
25
11
 
26
12
  export function getSourceBlockMap(
@@ -25,7 +25,6 @@ export interface DocumentSource {
25
25
  editMode?: string;
26
26
  styles?: DocumentStyle[];
27
27
  blockMap?: Record<string, SourceBlock>;
28
- pagination?: BuildPagination;
29
28
  }
30
29
 
31
30
  export interface DocumentStyle {
@@ -52,21 +51,6 @@ export interface SourceBlock {
52
51
  source?: SourceLocation;
53
52
  }
54
53
 
55
- export interface BuildPagination {
56
- mode: string;
57
- pageSafeHeightPx?: number;
58
- warnings?: PaginationWarning[];
59
- }
60
-
61
- export interface PaginationWarning {
62
- code: string;
63
- blockId?: string;
64
- height?: number;
65
- pageSafeHeightPx?: number;
66
- path?: string;
67
- source?: SourceLocation;
68
- }
69
-
70
54
  export interface DocumentMeta {
71
55
  title: string;
72
56
  subtitle?: string;