@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,845 +0,0 @@
|
|
|
1
|
-
import type { HtmlPageBlock } from "./types";
|
|
2
|
-
|
|
3
|
-
type SourcePage = HtmlPageBlock & {
|
|
4
|
-
pageNumber: number;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export interface PaginatedPage {
|
|
8
|
-
id: string;
|
|
9
|
-
title: string;
|
|
10
|
-
pageNumber: number;
|
|
11
|
-
html: string;
|
|
12
|
-
anchors: string[];
|
|
13
|
-
source?: SourcePage["source"];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const H3_CONTINUATION_MIN_BODY_RATIO = 0.14;
|
|
17
|
-
const PAGE_BODY_FIT_TOLERANCE = 1;
|
|
18
|
-
const PAGE_BODY_FIT_SAFETY_RATIO = 0.08;
|
|
19
|
-
const PAGE_BODY_FIT_SAFETY_MAX_PX = 96;
|
|
20
|
-
const SOURCE_INDEX_ATTR = "data-openpress-source-index";
|
|
21
|
-
const NO_FOOTER_PAGE_KINDS = new Set(["cover", "toc", "chapter-opener", "back-cover"]);
|
|
22
|
-
const HEADING_SELECTOR =
|
|
23
|
-
'.reader-page[data-page-kind="content"] .page-body > h2, ' +
|
|
24
|
-
'.reader-page[data-page-kind="content"] .page-body h3, ' +
|
|
25
|
-
'.reader-page[data-page-kind="content"] .page-body h4, ' +
|
|
26
|
-
'.reader-page[data-page-kind="content"] > h2, ' +
|
|
27
|
-
'.reader-page[data-page-kind="content"] > h3, ' +
|
|
28
|
-
'.reader-page[data-page-kind="content"] > h4';
|
|
29
|
-
|
|
30
|
-
export function paginateSourcePages(sourceContainer: HTMLElement, sourcePages: SourcePage[]): PaginatedPage[] {
|
|
31
|
-
normalizeSectionHeadings(sourceContainer);
|
|
32
|
-
|
|
33
|
-
const pages = paginateDomPages(sourceContainer);
|
|
34
|
-
expandTocPages(pages, sourceContainer);
|
|
35
|
-
addPageFooters(pages);
|
|
36
|
-
markChapterEnds(pages);
|
|
37
|
-
pages.forEach((page) => {
|
|
38
|
-
if (pageKindOf(page) === "toc") buildToc(page, pages);
|
|
39
|
-
});
|
|
40
|
-
normalizeContentCaptions(pages);
|
|
41
|
-
|
|
42
|
-
return pages.map((page, index) => {
|
|
43
|
-
const anchors = collectElementIds(page);
|
|
44
|
-
const anchor = anchors[0] ?? `page-${String(index + 1).padStart(2, "0")}`;
|
|
45
|
-
const source = sourceForPage(page, sourcePages);
|
|
46
|
-
stripSourceIndexMarkers(page);
|
|
47
|
-
return {
|
|
48
|
-
id: `openpress-rendered-page-${String(index + 1).padStart(2, "0")}`,
|
|
49
|
-
title: pageTitle(page) || `Page ${index + 1}`,
|
|
50
|
-
pageNumber: index + 1,
|
|
51
|
-
html: page.outerHTML,
|
|
52
|
-
anchors: anchors.length > 0 ? anchors : [anchor],
|
|
53
|
-
source,
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function normalizeContentCaptions(pages: HTMLElement[]) {
|
|
59
|
-
let figureCounter = 0;
|
|
60
|
-
let tableCounter = 0;
|
|
61
|
-
|
|
62
|
-
pages.forEach((page) => {
|
|
63
|
-
if (!isContentPage(page)) return;
|
|
64
|
-
|
|
65
|
-
page.querySelectorAll<HTMLElement>("figure > figcaption").forEach((caption) => {
|
|
66
|
-
if (caption.dataset.openpressCaptionNumbered === "false") return;
|
|
67
|
-
figureCounter += 1;
|
|
68
|
-
applyCaptionNumber(caption, "figure", "圖", figureCounter);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
page.querySelectorAll<HTMLElement>("table:not(.figure-grid) > caption").forEach((caption) => {
|
|
72
|
-
if (caption.dataset.openpressCaptionNumbered === "false") return;
|
|
73
|
-
tableCounter += 1;
|
|
74
|
-
applyCaptionNumber(caption, "table", "表", tableCounter);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function applyCaptionNumber(caption: HTMLElement, kind: "figure" | "table", label: string, index: number) {
|
|
80
|
-
const captionText = stripCaptionNumber(caption.textContent ?? "", kind);
|
|
81
|
-
const marker = `${label} ${index}`;
|
|
82
|
-
caption.dataset.openpressCaption = "true";
|
|
83
|
-
caption.dataset.openpressCaptionLabel = marker;
|
|
84
|
-
caption.textContent = captionText ? `${marker}:${captionText}` : marker;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function stripCaptionNumber(value: string, kind: "figure" | "table") {
|
|
88
|
-
const figurePrefix = /^\s*(?:圖|fig\.?|figure)\s*[\dA-Za-z一二三四五六七八九十百千〇零]+(?:[--..][\dA-Za-z一二三四五六七八九十百千〇零]+)?\s*[::、..]\s*/iu;
|
|
89
|
-
const tablePrefix = /^\s*(?:表|table)\s*[\dA-Za-z一二三四五六七八九十百千〇零]+(?:[--..][\dA-Za-z一二三四五六七八九十百千〇零]+)?\s*[::、..]\s*/iu;
|
|
90
|
-
return value.replace(kind === "figure" ? figurePrefix : tablePrefix, "").trim();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function paginateDomPages(sourceContainer: HTMLElement) {
|
|
94
|
-
const sourceSections = Array.from(sourceContainer.querySelectorAll<HTMLElement>(".reader-page"));
|
|
95
|
-
const items: Array<
|
|
96
|
-
| { type: "whole"; node: HTMLElement }
|
|
97
|
-
| { type: "toc"; sourceIndex: number; title: string }
|
|
98
|
-
| { type: "chapter-break" }
|
|
99
|
-
| { type: "block"; node: Element }
|
|
100
|
-
> = [];
|
|
101
|
-
|
|
102
|
-
sourceSections.forEach((section, sourceIndex) => {
|
|
103
|
-
const kind = pageKindOf(section);
|
|
104
|
-
if (section.classList.contains("toc-continuation")) return;
|
|
105
|
-
if (kind === "toc") {
|
|
106
|
-
items.push({ type: "toc", sourceIndex, title: section.dataset.pageTitle?.trim() || "目錄" });
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (isWholePageSurface(section)) {
|
|
110
|
-
const clone = withSourceIndex(section.cloneNode(true) as HTMLElement, sourceIndex);
|
|
111
|
-
if (kind) clone.dataset.pageKind = kind;
|
|
112
|
-
if (!pageShouldHaveFooter(clone)) markNoFooterChrome(clone, kind);
|
|
113
|
-
items.push({ type: "whole", node: clone });
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const sectionBody = getPageBody(section) || section;
|
|
117
|
-
Array.from(sectionBody.children).forEach((child) => {
|
|
118
|
-
if (child.children.length === 0 && !child.textContent?.trim() && child.tagName === "DIV") return;
|
|
119
|
-
if (child.tagName === "H2") items.push({ type: "chapter-break" });
|
|
120
|
-
items.push({ type: "block", node: withSourceIndex(child.cloneNode(true) as Element, sourceIndex) });
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const measurementHost = createMeasurementHost();
|
|
125
|
-
const measurer = createFramedPage("reader-page reader-page--content measurement", { kind: "content" });
|
|
126
|
-
measurementHost.html.appendChild(measurer);
|
|
127
|
-
const measurerBody = getPageBody(measurer);
|
|
128
|
-
if (!measurerBody) return sourceSections;
|
|
129
|
-
sourceContainer.appendChild(measurementHost.host);
|
|
130
|
-
|
|
131
|
-
const lastBlockInChapter = new Set<number>();
|
|
132
|
-
let lastBlockIdx = -1;
|
|
133
|
-
items.forEach((item, index) => {
|
|
134
|
-
if (item.type === "block") {
|
|
135
|
-
lastBlockIdx = index;
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (lastBlockIdx >= 0) lastBlockInChapter.add(lastBlockIdx);
|
|
139
|
-
lastBlockIdx = -1;
|
|
140
|
-
});
|
|
141
|
-
if (lastBlockIdx >= 0) lastBlockInChapter.add(lastBlockIdx);
|
|
142
|
-
|
|
143
|
-
let measureAsChapterEnd = false;
|
|
144
|
-
const pageDefs: Array<{ blocks: Element[] } | { whole: HTMLElement } | { toc: true; sourceIndex: number; title: string }> = [];
|
|
145
|
-
let pending: Element[] = [];
|
|
146
|
-
|
|
147
|
-
const fits = (blocks: Element[]) => {
|
|
148
|
-
measurerBody.innerHTML = "";
|
|
149
|
-
measurer.classList.toggle("is-chapter-end", measureAsChapterEnd);
|
|
150
|
-
blocks.forEach((block) => measurerBody.appendChild(block.cloneNode(true)));
|
|
151
|
-
if (!measurerBody.lastElementChild) return true;
|
|
152
|
-
return contentBottomWithinPageBody(measurerBody, PAGE_BODY_FIT_TOLERANCE);
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const measureBlocksHeight = (blocks: Element[]) => {
|
|
156
|
-
if (!blocks.length) return 0;
|
|
157
|
-
measurerBody.innerHTML = "";
|
|
158
|
-
measurer.classList.remove("is-chapter-end");
|
|
159
|
-
blocks.forEach((block) => measurerBody.appendChild(block.cloneNode(true)));
|
|
160
|
-
const first = measurerBody.firstElementChild;
|
|
161
|
-
const last = measurerBody.lastElementChild;
|
|
162
|
-
if (!first || !last) return 0;
|
|
163
|
-
return last.getBoundingClientRect().bottom - first.getBoundingClientRect().top;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const h3ContinuationMinHeight = () => measurerBody.clientHeight * H3_CONTINUATION_MIN_BODY_RATIO;
|
|
167
|
-
|
|
168
|
-
const commit = () => {
|
|
169
|
-
if (!pending.length) return;
|
|
170
|
-
pageDefs.push({ blocks: pending });
|
|
171
|
-
pending = [];
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const popTrailingHeadingLead = () => {
|
|
175
|
-
let headingIndex = -1;
|
|
176
|
-
for (let index = pending.length - 1; index >= 0; index -= 1) {
|
|
177
|
-
if (isHeading(pending[index])) {
|
|
178
|
-
headingIndex = index;
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (headingIndex <= 0) return [];
|
|
183
|
-
const leadBlocks = pending.slice(headingIndex + 1);
|
|
184
|
-
if (!leadBlocks.every(isLeadBlock)) return [];
|
|
185
|
-
const leadHeight = measureBlocksHeight(leadBlocks);
|
|
186
|
-
if (leadHeight >= h3ContinuationMinHeight()) return [];
|
|
187
|
-
return pending.splice(headingIndex);
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const tryAddSplittable = (block: Element) => {
|
|
191
|
-
let remaining = getParts(block);
|
|
192
|
-
let consumed = 0;
|
|
193
|
-
while (remaining.length > 0) {
|
|
194
|
-
let fitCount = 0;
|
|
195
|
-
for (let index = 1; index <= remaining.length; index += 1) {
|
|
196
|
-
const chunk = buildContainer(block, remaining.slice(0, index), {
|
|
197
|
-
includeCaption: consumed === 0,
|
|
198
|
-
start: consumed + 1,
|
|
199
|
-
});
|
|
200
|
-
if (fits([...pending, chunk])) fitCount = index;
|
|
201
|
-
else break;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (fitCount > 0) {
|
|
205
|
-
pending.push(buildContainer(block, remaining.slice(0, fitCount), {
|
|
206
|
-
includeCaption: consumed === 0,
|
|
207
|
-
start: consumed + 1,
|
|
208
|
-
}));
|
|
209
|
-
remaining = remaining.slice(fitCount);
|
|
210
|
-
consumed += fitCount;
|
|
211
|
-
if (remaining.length > 0) commit();
|
|
212
|
-
} else if (pending.length > 0) {
|
|
213
|
-
const movedLead = popTrailingHeadingLead();
|
|
214
|
-
const movedHeadings: Element[] = [];
|
|
215
|
-
while (!movedLead.length && pending.length && isHeading(pending[pending.length - 1])) {
|
|
216
|
-
const moved = pending.pop();
|
|
217
|
-
if (moved) movedHeadings.unshift(moved);
|
|
218
|
-
}
|
|
219
|
-
const moved = movedLead.length ? movedLead : movedHeadings;
|
|
220
|
-
if (pending.length > 0) {
|
|
221
|
-
commit();
|
|
222
|
-
pending = [...moved];
|
|
223
|
-
} else {
|
|
224
|
-
const chunk = buildContainer(block, [remaining[0]], {
|
|
225
|
-
includeCaption: consumed === 0,
|
|
226
|
-
start: consumed + 1,
|
|
227
|
-
});
|
|
228
|
-
pageDefs.push({ blocks: [...moved, chunk] });
|
|
229
|
-
pending = [];
|
|
230
|
-
remaining = remaining.slice(1);
|
|
231
|
-
consumed += 1;
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
pageDefs.push({
|
|
235
|
-
blocks: [buildContainer(block, [remaining[0]], {
|
|
236
|
-
includeCaption: consumed === 0,
|
|
237
|
-
start: consumed + 1,
|
|
238
|
-
})],
|
|
239
|
-
});
|
|
240
|
-
remaining = remaining.slice(1);
|
|
241
|
-
consumed += 1;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const tryAdd = (block: Element) => {
|
|
247
|
-
const candidate = [...pending, block];
|
|
248
|
-
if (fits(candidate)) {
|
|
249
|
-
pending = candidate;
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
if (canSplit(block)) {
|
|
253
|
-
tryAddSplittable(block);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (pending.length === 0) {
|
|
257
|
-
pageDefs.push({ blocks: [block] });
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const movedLead = popTrailingHeadingLead();
|
|
261
|
-
const moved = movedLead.length ? movedLead : [];
|
|
262
|
-
while (!movedLead.length && pending.length && isHeading(pending[pending.length - 1])) {
|
|
263
|
-
const heading = pending.pop();
|
|
264
|
-
if (heading) moved.unshift(heading);
|
|
265
|
-
}
|
|
266
|
-
if (pending.length === 0) {
|
|
267
|
-
const all = [...moved, block];
|
|
268
|
-
if (fits(all)) pending = all;
|
|
269
|
-
else {
|
|
270
|
-
pageDefs.push({ blocks: all });
|
|
271
|
-
pending = [];
|
|
272
|
-
}
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
commit();
|
|
276
|
-
pending = [...moved, block];
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
items.forEach((item, index) => {
|
|
280
|
-
if (item.type === "whole") {
|
|
281
|
-
measureAsChapterEnd = false;
|
|
282
|
-
commit();
|
|
283
|
-
pageDefs.push({ whole: item.node });
|
|
284
|
-
} else if (item.type === "toc") {
|
|
285
|
-
measureAsChapterEnd = false;
|
|
286
|
-
commit();
|
|
287
|
-
pageDefs.push({ toc: true, sourceIndex: item.sourceIndex, title: item.title });
|
|
288
|
-
} else if (item.type === "chapter-break") {
|
|
289
|
-
measureAsChapterEnd = false;
|
|
290
|
-
commit();
|
|
291
|
-
} else {
|
|
292
|
-
measureAsChapterEnd = lastBlockInChapter.has(index);
|
|
293
|
-
tryAdd(item.node);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
commit();
|
|
297
|
-
measurementHost.host.remove();
|
|
298
|
-
|
|
299
|
-
return pageDefs.map((def) => {
|
|
300
|
-
if ("whole" in def) return def.whole;
|
|
301
|
-
if ("toc" in def) {
|
|
302
|
-
const page = createFramedPage("reader-page reader-page--toc", { kind: "toc", footer: false });
|
|
303
|
-
page.id = "toc";
|
|
304
|
-
page.dataset.pageTitle = def.title;
|
|
305
|
-
page.setAttribute(SOURCE_INDEX_ATTR, String(def.sourceIndex));
|
|
306
|
-
return page;
|
|
307
|
-
}
|
|
308
|
-
const page = createFramedPage("reader-page reader-page--content", { kind: "content" });
|
|
309
|
-
const body = getPageBody(page);
|
|
310
|
-
def.blocks.forEach((block) => body?.appendChild(block));
|
|
311
|
-
return page;
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function createMeasurementHost() {
|
|
316
|
-
const host = document.createElement("div");
|
|
317
|
-
host.className = "openpress-html-page openpress-pagination-measurement";
|
|
318
|
-
host.setAttribute("aria-hidden", "true");
|
|
319
|
-
host.style.position = "absolute";
|
|
320
|
-
host.style.left = "-100000px";
|
|
321
|
-
host.style.top = "0";
|
|
322
|
-
host.style.visibility = "hidden";
|
|
323
|
-
host.style.pointerEvents = "none";
|
|
324
|
-
|
|
325
|
-
const html = document.createElement("div");
|
|
326
|
-
html.className = "openpress-html-page__html";
|
|
327
|
-
host.appendChild(html);
|
|
328
|
-
|
|
329
|
-
return { host, html };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function withSourceIndex<T extends Element>(node: T, sourceIndex: number) {
|
|
333
|
-
node.setAttribute(SOURCE_INDEX_ATTR, String(sourceIndex));
|
|
334
|
-
return node;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function normalizeSectionHeadings(scope: ParentNode) {
|
|
338
|
-
// Engine emits h2/h3 with bare heading text. Pagination assigns counter
|
|
339
|
-
// values to `data-chapter` / `data-chapter-marker` / `data-section`;
|
|
340
|
-
// the theme's ::before rules decide how to display them (01 / #1 / 一 / ...).
|
|
341
|
-
let chapterCounter = 0;
|
|
342
|
-
let sectionCounter = 0;
|
|
343
|
-
let topicCounter = 0;
|
|
344
|
-
scope.querySelectorAll<HTMLElement>(HEADING_SELECTOR).forEach((el) => {
|
|
345
|
-
if (el.tagName === "H2") {
|
|
346
|
-
chapterCounter += 1;
|
|
347
|
-
sectionCounter = 0;
|
|
348
|
-
topicCounter = 0;
|
|
349
|
-
ensureHeadingId(el, `section-${String(chapterCounter).padStart(2, "0")}`);
|
|
350
|
-
el.dataset.chapter = String(chapterCounter).padStart(2, "0");
|
|
351
|
-
el.dataset.chapterMarker = `#${chapterCounter}`;
|
|
352
|
-
} else if (el.tagName === "H3") {
|
|
353
|
-
sectionCounter += 1;
|
|
354
|
-
topicCounter = 0;
|
|
355
|
-
ensureHeadingId(el, `section-${chapterCounter}-${sectionCounter}`);
|
|
356
|
-
el.dataset.section = `${chapterCounter}.${sectionCounter}`;
|
|
357
|
-
} else if (el.tagName === "H4") {
|
|
358
|
-
topicCounter += 1;
|
|
359
|
-
if (chapterCounter > 0 && sectionCounter > 0) {
|
|
360
|
-
ensureHeadingId(el, `section-${chapterCounter}-${sectionCounter}-${topicCounter}`);
|
|
361
|
-
el.dataset.topic = `${chapterCounter}.${sectionCounter}.${topicCounter}`;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function buildToc(tocPage: HTMLElement, allPages: HTMLElement[]) {
|
|
368
|
-
// Preserve the engine-emitted h2 (its text comes from the toc page's
|
|
369
|
-
// frontmatter `title:`). Fall back to "Contents" only if nothing was set.
|
|
370
|
-
const tocBody = getPageBody(tocPage);
|
|
371
|
-
if (!tocBody) return;
|
|
372
|
-
const existingHeading = tocBody.querySelector<HTMLElement>("h2");
|
|
373
|
-
const headingText = existingHeading?.textContent?.trim() || tocPage.dataset.pageTitle?.trim() || "Contents";
|
|
374
|
-
const entries = collectTocEntries(allPages);
|
|
375
|
-
const start = Number(tocPage.dataset.tocStart ?? "0");
|
|
376
|
-
const end = Number(tocPage.dataset.tocEnd ?? String(entries.length));
|
|
377
|
-
buildTocContent(tocPage, entries, start, end, headingText);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function buildTocContent(
|
|
381
|
-
tocPage: HTMLElement,
|
|
382
|
-
entries: Array<{ id: string; title: string; pageIndex: number; level: 2 | 3; label: string }>,
|
|
383
|
-
start: number,
|
|
384
|
-
end: number,
|
|
385
|
-
headingText: string,
|
|
386
|
-
) {
|
|
387
|
-
const tocBody = getPageBody(tocPage);
|
|
388
|
-
if (!tocBody) return;
|
|
389
|
-
const isContinuation = tocPage.dataset.tocContinuation === "true";
|
|
390
|
-
tocBody.innerHTML = "";
|
|
391
|
-
const heading = document.createElement("h2");
|
|
392
|
-
heading.id = tocPage.id === "toc" ? "toc-title" : `${tocPage.id}-title`;
|
|
393
|
-
heading.className = isContinuation ? "toc-heading toc-heading--continuation" : "toc-heading";
|
|
394
|
-
heading.textContent = isContinuation ? tocContinuationTitle(headingText) : headingText;
|
|
395
|
-
tocPage.setAttribute("aria-labelledby", heading.id);
|
|
396
|
-
tocBody.appendChild(heading);
|
|
397
|
-
|
|
398
|
-
const list = document.createElement("ol");
|
|
399
|
-
list.className = "toc-list";
|
|
400
|
-
entries.slice(start, end).forEach((entry) => {
|
|
401
|
-
if (!entry.id) return;
|
|
402
|
-
const li = document.createElement("li");
|
|
403
|
-
li.className = `toc-level-${entry.level}`;
|
|
404
|
-
const a = document.createElement("a");
|
|
405
|
-
a.href = `#${entry.id}`;
|
|
406
|
-
a.dataset.openpressAnchor = entry.id;
|
|
407
|
-
a.dataset.openpressTargetPageIndex = String(entry.pageIndex);
|
|
408
|
-
a.innerHTML = `<span class="toc-index" data-toc-index="${escapeAttr(entry.label)}">${escapeHtml(entry.label)}</span><span class="toc-title">${escapeHtml(entry.title)}</span><span class="toc-page">${String(entry.pageIndex + 1).padStart(2, "0")}</span>`;
|
|
409
|
-
li.appendChild(a);
|
|
410
|
-
list.appendChild(li);
|
|
411
|
-
});
|
|
412
|
-
tocBody.appendChild(list);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function expandTocPages(pages: HTMLElement[], sourceContainer: HTMLElement) {
|
|
416
|
-
const tocIndex = pages.findIndex((page) => pageKindOf(page) === "toc");
|
|
417
|
-
if (tocIndex < 0) return;
|
|
418
|
-
|
|
419
|
-
const tocPage = pages[tocIndex];
|
|
420
|
-
const entryCount = collectTocEntries(pages).length;
|
|
421
|
-
const tocChunks = measureTocChunks(sourceContainer, tocPage, pages);
|
|
422
|
-
tocPage.classList.add("reader-page--toc");
|
|
423
|
-
tocPage.classList.remove("toc");
|
|
424
|
-
if (tocChunks.length <= 1) {
|
|
425
|
-
tocPage.dataset.tocStart = "0";
|
|
426
|
-
tocPage.dataset.tocEnd = String(entryCount);
|
|
427
|
-
tocPage.dataset.tocContinuation = "false";
|
|
428
|
-
tocPage.classList.remove("toc-continuation");
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const sourceIndex = tocPage.getAttribute(SOURCE_INDEX_ATTR);
|
|
433
|
-
const title = tocPage.dataset.pageTitle?.trim() || "目錄";
|
|
434
|
-
const expandedPages = tocChunks.map((chunk, index) => {
|
|
435
|
-
const page = index === 0 ? tocPage : createFramedPage("reader-page reader-page--toc", { kind: "toc", footer: false });
|
|
436
|
-
page.classList.add("reader-page--toc");
|
|
437
|
-
page.classList.remove("toc");
|
|
438
|
-
markNoFooterChrome(page, "toc");
|
|
439
|
-
page.id = index === 0 ? "toc" : `toc-${String(index + 1).padStart(2, "0")}`;
|
|
440
|
-
page.dataset.pageTitle = title;
|
|
441
|
-
page.dataset.tocStart = String(chunk.start);
|
|
442
|
-
page.dataset.tocEnd = String(chunk.end);
|
|
443
|
-
page.dataset.tocContinuation = index > 0 ? "true" : "false";
|
|
444
|
-
page.classList.toggle("toc-continuation", index > 0);
|
|
445
|
-
page.setAttribute("aria-labelledby", index === 0 ? "toc-title" : `${page.id}-title`);
|
|
446
|
-
if (sourceIndex !== null) page.setAttribute(SOURCE_INDEX_ATTR, sourceIndex);
|
|
447
|
-
return page;
|
|
448
|
-
});
|
|
449
|
-
pages.splice(tocIndex, 1, ...expandedPages);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function measureTocChunks(sourceContainer: HTMLElement, tocPage: HTMLElement, allPages: HTMLElement[]) {
|
|
453
|
-
const entries = collectTocEntries(allPages);
|
|
454
|
-
if (entries.length === 0) return [{ start: 0, end: 0 }];
|
|
455
|
-
|
|
456
|
-
const title = tocPage.dataset.pageTitle?.trim() || "目錄";
|
|
457
|
-
const measurementHost = createMeasurementHost();
|
|
458
|
-
const measurer = createFramedPage("reader-page reader-page--toc", { kind: "toc", footer: false });
|
|
459
|
-
measurementHost.html.appendChild(measurer);
|
|
460
|
-
sourceContainer.appendChild(measurementHost.host);
|
|
461
|
-
|
|
462
|
-
const chunks: Array<{ start: number; end: number }> = [];
|
|
463
|
-
let start = 0;
|
|
464
|
-
while (start < entries.length) {
|
|
465
|
-
let lastFit = start;
|
|
466
|
-
for (let end = start + 1; end <= entries.length; end += 1) {
|
|
467
|
-
measurer.dataset.tocContinuation = chunks.length > 0 ? "true" : "false";
|
|
468
|
-
buildTocContent(measurer, entries, start, end, title);
|
|
469
|
-
const body = getPageBody(measurer);
|
|
470
|
-
if (body && contentBottomWithinPageBody(body, PAGE_BODY_FIT_TOLERANCE)) {
|
|
471
|
-
lastFit = end;
|
|
472
|
-
} else {
|
|
473
|
-
break;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const end = lastFit > start ? lastFit : start + 1;
|
|
478
|
-
chunks.push({ start, end });
|
|
479
|
-
start = end;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
measurementHost.host.remove();
|
|
483
|
-
return chunks;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function tocContinuationTitle(title: string) {
|
|
487
|
-
return title === "目錄" ? "目錄續" : `${title} continued`;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function collectTocEntries(allPages: HTMLElement[]) {
|
|
491
|
-
const entries: Array<{ id: string; title: string; pageIndex: number; level: 2 | 3; label: string }> = [];
|
|
492
|
-
let chapterIndex = 0;
|
|
493
|
-
let sectionIndex = 0;
|
|
494
|
-
let pendingChapterOpener: { id: string; pageIndex: number } | undefined;
|
|
495
|
-
|
|
496
|
-
allPages.forEach((page, pageIndex) => {
|
|
497
|
-
if (pageKindOf(page) === "chapter-opener") {
|
|
498
|
-
pendingChapterOpener = readChapterOpenerTarget(page, pageIndex);
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (!isContentPage(page)) return;
|
|
503
|
-
let pageStartedChapter = false;
|
|
504
|
-
page.querySelectorAll<HTMLElement>("h2, h3").forEach((heading) => {
|
|
505
|
-
if (heading.tagName === "H2") {
|
|
506
|
-
const opener = pendingChapterOpener;
|
|
507
|
-
pendingChapterOpener = undefined;
|
|
508
|
-
pageStartedChapter = true;
|
|
509
|
-
chapterIndex += 1;
|
|
510
|
-
sectionIndex = 0;
|
|
511
|
-
entries.push({
|
|
512
|
-
id: opener?.id ?? heading.id,
|
|
513
|
-
title: heading.textContent?.trim() ?? "",
|
|
514
|
-
pageIndex: opener?.pageIndex ?? pageIndex,
|
|
515
|
-
level: 2,
|
|
516
|
-
label: heading.dataset.chapterMarker || `#${chapterIndex}`,
|
|
517
|
-
});
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (heading.tagName === "H3" && chapterIndex > 0) {
|
|
522
|
-
sectionIndex += 1;
|
|
523
|
-
entries.push({
|
|
524
|
-
id: heading.id,
|
|
525
|
-
title: heading.textContent?.trim() ?? "",
|
|
526
|
-
pageIndex,
|
|
527
|
-
level: 3,
|
|
528
|
-
label: heading.dataset.section || `${chapterIndex}.${sectionIndex}`,
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
if (!pageStartedChapter) pendingChapterOpener = undefined;
|
|
533
|
-
});
|
|
534
|
-
return entries;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function readChapterOpenerTarget(page: HTMLElement, pageIndex: number) {
|
|
538
|
-
const heading = page.querySelector<HTMLElement>(".chapter-opener-title, h2");
|
|
539
|
-
if (!heading?.id) return undefined;
|
|
540
|
-
return {
|
|
541
|
-
id: heading.id,
|
|
542
|
-
pageIndex,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function ensureHeadingId(heading: HTMLElement, fallbackId: string) {
|
|
547
|
-
if (heading.id) return;
|
|
548
|
-
heading.id = fallbackId;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function collectElementIds(scope: ParentNode) {
|
|
552
|
-
const ids: string[] = [];
|
|
553
|
-
scope.querySelectorAll<HTMLElement>("[id]").forEach((el) => {
|
|
554
|
-
if (el.id && !ids.includes(el.id)) ids.push(el.id);
|
|
555
|
-
});
|
|
556
|
-
return ids;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function addPageFooters(allPages: HTMLElement[]) {
|
|
560
|
-
let currentChapter = "";
|
|
561
|
-
let chapterCount = 0;
|
|
562
|
-
allPages.forEach((page, index) => {
|
|
563
|
-
if (!pageShouldHaveFooter(page)) {
|
|
564
|
-
markNoFooterChrome(page, pageKindOf(page));
|
|
565
|
-
if (isCustomWholePageSurface(page) && !getPageFrame(page)) return;
|
|
566
|
-
ensurePageShell(page, { footer: false });
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
const pageBody = ensurePageShell(page, { footer: true });
|
|
570
|
-
const footer = getPageFooter(page);
|
|
571
|
-
if (!pageBody || !footer) return;
|
|
572
|
-
footer.innerHTML = "";
|
|
573
|
-
let leftLabel = "";
|
|
574
|
-
if (pageKindOf(page) === "toc") {
|
|
575
|
-
// Use the toc page's own title (set by engine from frontmatter) so
|
|
576
|
-
// non-Chinese documents don't get a Chinese label here.
|
|
577
|
-
leftLabel = page.dataset.pageTitle?.trim()
|
|
578
|
-
|| page.querySelector<HTMLElement>(":scope .page-body > h2")?.textContent?.trim()
|
|
579
|
-
|| "Contents";
|
|
580
|
-
} else if (isContentPage(page)) {
|
|
581
|
-
const h2 = pageBody.querySelector<HTMLElement>(":scope > h2");
|
|
582
|
-
if (h2) {
|
|
583
|
-
currentChapter = h2.textContent?.trim() ?? "";
|
|
584
|
-
chapterCount += 1;
|
|
585
|
-
h2.dataset.chapter = String(chapterCount).padStart(2, "0");
|
|
586
|
-
h2.dataset.chapterMarker = `#${chapterCount}`;
|
|
587
|
-
}
|
|
588
|
-
leftLabel = currentChapter;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const left = document.createElement("span");
|
|
592
|
-
left.className = "footer-left";
|
|
593
|
-
left.textContent = leftLabel;
|
|
594
|
-
const right = document.createElement("span");
|
|
595
|
-
right.className = "footer-right";
|
|
596
|
-
right.textContent = String(index + 1).padStart(2, "0");
|
|
597
|
-
footer.append(left, right);
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function markChapterEnds(allPages: HTMLElement[]) {
|
|
602
|
-
allPages.forEach((page, index) => {
|
|
603
|
-
if (!isContentPage(page)) return;
|
|
604
|
-
const next = allPages[index + 1];
|
|
605
|
-
const nextBody = getPageBody(next) || next;
|
|
606
|
-
const nextStartsChapter = next ? isContentPage(next) && nextBody?.querySelector(":scope > h2:first-child") : false;
|
|
607
|
-
const nextIsBackCover = next ? pageKindOf(next) === "back-cover" : false;
|
|
608
|
-
if (!next || nextStartsChapter || nextIsBackCover) page.classList.add("is-chapter-end");
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function createFramedPage(className: string, options: { kind?: string; footer?: boolean } = {}) {
|
|
613
|
-
const page = document.createElement("section");
|
|
614
|
-
page.className = className;
|
|
615
|
-
if (options.kind) page.dataset.pageKind = options.kind;
|
|
616
|
-
if (options.footer === false) markNoFooterChrome(page, options.kind);
|
|
617
|
-
ensurePageShell(page, options);
|
|
618
|
-
return page;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function ensurePageShell(page: HTMLElement, options: { footer?: boolean } = {}) {
|
|
622
|
-
const shouldHaveFooter = options.footer ?? pageShouldHaveFooter(page);
|
|
623
|
-
const existingFrame = getPageFrame(page);
|
|
624
|
-
if (existingFrame) {
|
|
625
|
-
if (!shouldHaveFooter) markNoFooterChrome(page, pageKindOf(page));
|
|
626
|
-
return getPageBody(page);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const existingHeader = page.querySelector(":scope > .page-header");
|
|
630
|
-
const existingFooter = page.querySelector(":scope > .page-footer");
|
|
631
|
-
const frame = document.createElement("div");
|
|
632
|
-
frame.className = "page-frame";
|
|
633
|
-
|
|
634
|
-
const header = document.createElement("header");
|
|
635
|
-
header.className = "page-header";
|
|
636
|
-
header.setAttribute("aria-hidden", "true");
|
|
637
|
-
if (existingHeader) {
|
|
638
|
-
while (existingHeader.firstChild) header.appendChild(existingHeader.firstChild);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const body = document.createElement("main");
|
|
642
|
-
body.className = "page-body";
|
|
643
|
-
Array.from(page.childNodes).forEach((node) => {
|
|
644
|
-
if (node === existingHeader || node === existingFooter) return;
|
|
645
|
-
body.appendChild(node);
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
if (shouldHaveFooter) {
|
|
649
|
-
const footer = document.createElement("footer");
|
|
650
|
-
footer.className = "page-footer";
|
|
651
|
-
footer.setAttribute("aria-hidden", "true");
|
|
652
|
-
if (existingFooter) {
|
|
653
|
-
while (existingFooter.firstChild) footer.appendChild(existingFooter.firstChild);
|
|
654
|
-
}
|
|
655
|
-
frame.append(header, body, footer);
|
|
656
|
-
} else {
|
|
657
|
-
frame.append(header, body);
|
|
658
|
-
markNoFooterChrome(page, pageKindOf(page));
|
|
659
|
-
}
|
|
660
|
-
page.appendChild(frame);
|
|
661
|
-
existingHeader?.remove();
|
|
662
|
-
existingFooter?.remove();
|
|
663
|
-
return body;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function isWholePageSurface(page: HTMLElement) {
|
|
667
|
-
const kind = pageKindOf(page);
|
|
668
|
-
return (
|
|
669
|
-
kind === "cover" ||
|
|
670
|
-
kind === "chapter-opener" ||
|
|
671
|
-
kind === "back-cover"
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function isCustomWholePageSurface(page: HTMLElement) {
|
|
676
|
-
const kind = pageKindOf(page);
|
|
677
|
-
return kind === "cover" || kind === "back-cover";
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function pageShouldHaveFooter(page: HTMLElement) {
|
|
681
|
-
const kind = pageKindOf(page);
|
|
682
|
-
return (
|
|
683
|
-
page.dataset.pageFooter !== "false" &&
|
|
684
|
-
!page.classList.contains("no-footer") &&
|
|
685
|
-
!NO_FOOTER_PAGE_KINDS.has(kind)
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function markNoFooterChrome(page: HTMLElement, kind?: string) {
|
|
690
|
-
page.classList.add("no-footer");
|
|
691
|
-
page.dataset.pageFooter = "false";
|
|
692
|
-
if (kind && !page.dataset.pageKind) page.dataset.pageKind = kind;
|
|
693
|
-
getPageFooter(page)?.remove();
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function pageKindOf(page: HTMLElement) {
|
|
697
|
-
return page.dataset.pageKind || "";
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function isContentPage(page: HTMLElement) {
|
|
701
|
-
return pageKindOf(page) === "content";
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function getPageFrame(page?: Element | null) {
|
|
705
|
-
return page?.querySelector?.(":scope > .page-frame") ?? null;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function getPageBody(page?: Element | null) {
|
|
709
|
-
return (
|
|
710
|
-
page?.querySelector?.<HTMLElement>(":scope > .page-frame > .page-body") ||
|
|
711
|
-
page?.querySelector?.<HTMLElement>(":scope > .page-body") ||
|
|
712
|
-
null
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function getPageFooter(page?: Element | null) {
|
|
717
|
-
return (
|
|
718
|
-
page?.querySelector?.<HTMLElement>(":scope > .page-frame > .page-footer") ||
|
|
719
|
-
page?.querySelector?.<HTMLElement>(":scope > .page-footer") ||
|
|
720
|
-
null
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function contentBottomWithinPageBody(body: HTMLElement, tolerance = PAGE_BODY_FIT_TOLERANCE) {
|
|
725
|
-
const bodyRect = body.getBoundingClientRect();
|
|
726
|
-
const bodyBottom = bodyRect.bottom - pageBodyFitSafetyInset(bodyRect);
|
|
727
|
-
const contentBottom = Array.from(body.children).reduce((bottom, child) => {
|
|
728
|
-
if (window.getComputedStyle(child).display === "none") return bottom;
|
|
729
|
-
return Math.max(bottom, child.getBoundingClientRect().bottom + getElementMarginBottom(child));
|
|
730
|
-
}, bodyRect.top);
|
|
731
|
-
return contentBottom <= bodyBottom + tolerance;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function pageBodyFitSafetyInset(bodyRect: DOMRect) {
|
|
735
|
-
return Math.min(PAGE_BODY_FIT_SAFETY_MAX_PX, Math.max(0, bodyRect.height * PAGE_BODY_FIT_SAFETY_RATIO));
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function getElementMarginBottom(element: Element) {
|
|
739
|
-
const value = Number.parseFloat(window.getComputedStyle(element).marginBottom);
|
|
740
|
-
return Number.isFinite(value) ? value : 0;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function isHeading(el?: Element) {
|
|
744
|
-
return Boolean(el && /^H[1-6]$/.test(el.tagName));
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function isLeadBlock(el: Element) {
|
|
748
|
-
return el.tagName === "P" || el.tagName === "UL" || el.tagName === "OL";
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function canSplit(block: Element) {
|
|
752
|
-
if (block.classList.contains("figure-grid")) return block.tagName === "DIV" && block.children.length > 1;
|
|
753
|
-
if (block.tagName === "PRE") return getPreLines(block).length > 1;
|
|
754
|
-
if (block.tagName === "TABLE") {
|
|
755
|
-
const tbody = block.querySelector("tbody");
|
|
756
|
-
return Boolean(tbody && tbody.children.length > 1);
|
|
757
|
-
}
|
|
758
|
-
return (block.tagName === "UL" || block.tagName === "OL") && block.children.length > 1;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function getParts(block: Element) {
|
|
762
|
-
if (block.tagName === "PRE") {
|
|
763
|
-
return getPreLines(block).map((line) => {
|
|
764
|
-
const part = document.createElement("span");
|
|
765
|
-
part.textContent = line;
|
|
766
|
-
return part;
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
if (block.tagName === "TABLE") return Array.from(block.querySelector("tbody")?.children ?? []);
|
|
770
|
-
return Array.from(block.children);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function buildContainer(original: Element, parts: Element[], options: { includeCaption?: boolean; start?: number } = {}) {
|
|
774
|
-
if (original.tagName === "PRE") {
|
|
775
|
-
const pre = original.cloneNode(false) as HTMLElement;
|
|
776
|
-
pre.classList.add("openpress-pre-fragment");
|
|
777
|
-
const originalCode = original.querySelector(":scope > code");
|
|
778
|
-
const code = originalCode ? (originalCode.cloneNode(false) as HTMLElement) : document.createElement("code");
|
|
779
|
-
code.textContent = parts.map((part) => part.textContent ?? "").join("\n");
|
|
780
|
-
pre.appendChild(code);
|
|
781
|
-
return pre;
|
|
782
|
-
}
|
|
783
|
-
if (original.tagName === "TABLE") {
|
|
784
|
-
const table = original.cloneNode(false) as HTMLElement;
|
|
785
|
-
const caption = original.querySelector(":scope > caption");
|
|
786
|
-
if (caption && options.includeCaption !== false) table.appendChild(caption.cloneNode(true));
|
|
787
|
-
const thead = original.querySelector("thead");
|
|
788
|
-
if (thead) table.appendChild(thead.cloneNode(true));
|
|
789
|
-
const tbody = document.createElement("tbody");
|
|
790
|
-
parts.forEach((row) => tbody.appendChild(row.cloneNode(true)));
|
|
791
|
-
table.appendChild(tbody);
|
|
792
|
-
return table;
|
|
793
|
-
}
|
|
794
|
-
const container = original.cloneNode(false) as HTMLElement;
|
|
795
|
-
if (container.tagName === "OL" && options.start && options.start > 1) {
|
|
796
|
-
container.setAttribute("start", String(options.start));
|
|
797
|
-
}
|
|
798
|
-
parts.forEach((part) => container.appendChild(part.cloneNode(true)));
|
|
799
|
-
return container;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function getPreLines(block: Element) {
|
|
803
|
-
const code = block.querySelector(":scope > code");
|
|
804
|
-
const text = code?.textContent ?? block.textContent ?? "";
|
|
805
|
-
return splitPreTextForPagination(text);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
export function splitPreTextForPagination(text: string) {
|
|
809
|
-
const withoutTrailingNewline = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
810
|
-
return withoutTrailingNewline.split("\n");
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function pageTitle(page: HTMLElement) {
|
|
814
|
-
return page.dataset.pageTitle || page.querySelector("h1, h2, h3, h4")?.textContent?.trim() || "";
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function sourceForPage(page: HTMLElement, sourcePages: SourcePage[]) {
|
|
818
|
-
const sourceIndex =
|
|
819
|
-
page.getAttribute(SOURCE_INDEX_ATTR) ??
|
|
820
|
-
page.querySelector<HTMLElement>(`[${SOURCE_INDEX_ATTR}]`)?.getAttribute(SOURCE_INDEX_ATTR);
|
|
821
|
-
if (sourceIndex !== null && sourceIndex !== undefined) return sourcePages[Number(sourceIndex)]?.source;
|
|
822
|
-
|
|
823
|
-
const firstAnchor = page.querySelector<HTMLElement>("[id]")?.id;
|
|
824
|
-
if (!firstAnchor) return undefined;
|
|
825
|
-
return sourcePages.find((source) => source.anchors?.includes(firstAnchor))?.source;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function stripSourceIndexMarkers(page: HTMLElement) {
|
|
829
|
-
page.removeAttribute("data-openpress-source-index");
|
|
830
|
-
page.querySelectorAll(`[${SOURCE_INDEX_ATTR}]`).forEach((el) => {
|
|
831
|
-
el.removeAttribute("data-openpress-source-index");
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function escapeHtml(value: string) {
|
|
836
|
-
return value
|
|
837
|
-
.replaceAll("&", "&")
|
|
838
|
-
.replaceAll("<", "<")
|
|
839
|
-
.replaceAll(">", ">")
|
|
840
|
-
.replaceAll('"', """);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
function escapeAttr(value: string) {
|
|
844
|
-
return escapeHtml(value);
|
|
845
|
-
}
|