@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.
- 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
|
@@ -33,18 +33,14 @@ import {
|
|
|
33
33
|
ProjectEntryPanel,
|
|
34
34
|
type ProjectMentionItem,
|
|
35
35
|
} from "./projectWorkspace";
|
|
36
|
-
import {
|
|
36
|
+
import { createAnchorPageMap, resolveAnchorPageIndex } from "./anchorMap";
|
|
37
37
|
import { scheduleBrowserFrame } from "./frameScheduler";
|
|
38
38
|
import {
|
|
39
|
-
createAnchorPageMap,
|
|
40
|
-
numberSourceHeadings,
|
|
41
39
|
PUBLIC_DRAWER_BREAKPOINT,
|
|
42
40
|
PublicPage,
|
|
43
|
-
resolveAnchorPageIndex,
|
|
44
41
|
useViewMode,
|
|
45
42
|
} from "./publicPage";
|
|
46
43
|
import { getProjectIdentity } from "./projectIdentity";
|
|
47
|
-
import { hasBuildTimePagination } from "./reactDocumentMetadata";
|
|
48
44
|
import { buildPublicPreviewHref, isLocalWorkspaceHost } from "./runtimeMode";
|
|
49
45
|
import { useReaderRuntime } from "./readerRuntime";
|
|
50
46
|
import type { DeploymentInfo, ReaderDocument, HtmlPageBlock, SourceBlock } from "./types";
|
|
@@ -123,14 +119,9 @@ export function HtmlWorkbench({
|
|
|
123
119
|
deploymentInfo: DeploymentInfo;
|
|
124
120
|
}) {
|
|
125
121
|
const sourceContainerRef = useRef<HTMLDivElement | null>(null);
|
|
126
|
-
const
|
|
122
|
+
const displayPages = pages;
|
|
127
123
|
const viewModeState = useViewMode();
|
|
128
124
|
const { viewMode } = viewModeState;
|
|
129
|
-
const buildTimePaginated = hasBuildTimePagination(document);
|
|
130
|
-
const [paginatedPages, setPaginatedPages] = useState<PaginatedPage[] | null>(null);
|
|
131
|
-
const displayPages: DisplayPage[] = viewMode === "paged" && !buildTimePaginated
|
|
132
|
-
? (paginatedPages ?? numberedPages)
|
|
133
|
-
: numberedPages;
|
|
134
125
|
const mediaAssets = useMemo(() => collectMediaAssetIndex(displayPages), [displayPages]);
|
|
135
126
|
const anchorPageMap = useMemo(() => createAnchorPageMap(displayPages), [displayPages]);
|
|
136
127
|
const projectComponentUsages = useMemo(() => createProjectComponentUsages(displayPages), [displayPages]);
|
|
@@ -163,7 +154,6 @@ export function HtmlWorkbench({
|
|
|
163
154
|
const pdfButtonText = workbenchPdfButtonText(localDeployEnabled, pdfActionStatus, staticPdfHref);
|
|
164
155
|
const pdfStatusMessage = workbenchPdfStatusMessage(localDeployEnabled, pdfActionStatus);
|
|
165
156
|
const pdfButtonDisabled = localDeployEnabled ? pdfActionStatus === "generating" || pdfActionStatus === "opening" : !staticPdfHref;
|
|
166
|
-
const activePaginatedReady = viewMode === "reading" || buildTimePaginated || Boolean(paginatedPages);
|
|
167
157
|
const inspectorSelectionLabel = formatInspectorSelection(inspector.selectedBlock);
|
|
168
158
|
const activeInlineSavedComment = getInlineSavedCommentForTarget(inlineSavedComment, inspector.selectedTarget);
|
|
169
159
|
const inspectorCommentDisabled = !inspector.selectedBlock || !inspectorCommentText.trim() || inspectorCommentStatus === "submitting";
|
|
@@ -392,10 +382,6 @@ export function HtmlWorkbench({
|
|
|
392
382
|
scheduleBrowserFrame(() => reader.setPage(reader.currentPageIndex, { behavior: "auto" }));
|
|
393
383
|
};
|
|
394
384
|
|
|
395
|
-
useLayoutEffect(() => {
|
|
396
|
-
setPaginatedPages(null);
|
|
397
|
-
}, [numberedPages]);
|
|
398
|
-
|
|
399
385
|
useEffect(() => {
|
|
400
386
|
setInspectorCommentStatus("idle");
|
|
401
387
|
setInspectorCommentError("");
|
|
@@ -407,24 +393,6 @@ export function HtmlWorkbench({
|
|
|
407
393
|
void refreshPendingComments();
|
|
408
394
|
}, [devMode, refreshPendingComments, workspaceView]);
|
|
409
395
|
|
|
410
|
-
useLayoutEffect(() => {
|
|
411
|
-
if (buildTimePaginated) return undefined;
|
|
412
|
-
if (viewMode !== "paged" || paginatedPages) return undefined;
|
|
413
|
-
const sourceContainer = sourceContainerRef.current;
|
|
414
|
-
if (!sourceContainer) return undefined;
|
|
415
|
-
|
|
416
|
-
let cancelled = false;
|
|
417
|
-
const cancelFrame = scheduleBrowserFrame(() => {
|
|
418
|
-
const nextPages = paginateSourcePages(sourceContainer, numberedPages);
|
|
419
|
-
if (!cancelled) setPaginatedPages(nextPages);
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
return () => {
|
|
423
|
-
cancelled = true;
|
|
424
|
-
cancelFrame();
|
|
425
|
-
};
|
|
426
|
-
}, [buildTimePaginated, numberedPages, paginatedPages, viewMode]);
|
|
427
|
-
|
|
428
396
|
const actionSection = (
|
|
429
397
|
<section className="openpress-public-action-section" aria-label="輸出">
|
|
430
398
|
<span className="openpress-public-action-heading">輸出</span>
|
|
@@ -512,7 +480,6 @@ export function HtmlWorkbench({
|
|
|
512
480
|
className={`reader-app openpress-reader-app openpress-public-viewer openpress-dev-public-viewer is-ready${reader.rightPanelOpen ? "" : " is-closed-right"}`}
|
|
513
481
|
data-openpress-react-runtime="true"
|
|
514
482
|
data-openpress-view-mode={viewMode}
|
|
515
|
-
data-openpress-pagination={activePaginatedReady ? "ready" : "pending"}
|
|
516
483
|
data-openpress-inspector-mode={inspector.inspectorMode ? "on" : "off"}
|
|
517
484
|
data-active-workspace={workspaceView}
|
|
518
485
|
>
|
|
@@ -533,7 +500,6 @@ export function HtmlWorkbench({
|
|
|
533
500
|
pages={displayPages}
|
|
534
501
|
currentPageIndex={reader.currentPageIndex}
|
|
535
502
|
devMode={devMode}
|
|
536
|
-
paginatedReady={activePaginatedReady}
|
|
537
503
|
sourceContainerRef={sourceContainerRef}
|
|
538
504
|
registerPage={reader.registerPage}
|
|
539
505
|
exposeSourceData={devMode}
|
|
@@ -269,12 +269,6 @@
|
|
|
269
269
|
box-shadow: none;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
.openpress-public-viewer.openpress-reader-app[data-openpress-pagination="pending"] .openpress-public-navigation {
|
|
273
|
-
transform: translateX(-110%);
|
|
274
|
-
pointer-events: none;
|
|
275
|
-
box-shadow: none;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
272
|
.openpress-public-viewer.openpress-reader-app .openpress-public-scrim {
|
|
279
273
|
position: fixed;
|
|
280
274
|
inset: 0;
|
|
@@ -309,14 +303,6 @@
|
|
|
309
303
|
display: block;
|
|
310
304
|
}
|
|
311
305
|
|
|
312
|
-
.openpress-public-viewer.openpress-reader-app[data-openpress-pagination="pending"] .openpress-public-scrim {
|
|
313
|
-
display: none;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
.openpress-public-viewer.openpress-reader-app[data-openpress-pagination="pending"] .openpress-public-fab {
|
|
317
|
-
display: flex;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
306
|
.openpress-public-viewer[data-openpress-view-mode="paged"] .reader-pages {
|
|
321
307
|
--openpress-public-page-width: min(
|
|
322
308
|
var(--openpress-page-width),
|
package/tsconfig.json
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"noEmit": true,
|
|
18
18
|
"jsx": "react-jsx",
|
|
19
19
|
"paths": {
|
|
20
|
-
"@
|
|
20
|
+
"@open-press/core": ["./src/openpress/core/index.tsx"],
|
|
21
|
+
"@open-press/core/mdx": ["./src/openpress/mdx/index.ts"],
|
|
22
|
+
"@open-press/core/manuscript": ["./src/openpress/manuscript/index.tsx"],
|
|
23
|
+
"@open-press/core/numbering": ["./src/openpress/numbering/index.ts"],
|
|
21
24
|
"@/components": ["./document/components/index.ts", "./document/components/index.tsx"],
|
|
22
25
|
"@/components/*": ["./document/components/*"],
|
|
23
26
|
"@/*": ["./src/*"]
|
package/vite.config.ts
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
6
|
import { defineConfig } from "vite";
|
|
7
7
|
import react from "@vitejs/plugin-react";
|
|
8
|
-
import { loadConfig, publicPdfHref } from "./engine/config.mjs";
|
|
8
|
+
import { loadConfig, publicPdfHref } from "./engine/runtime/config.mjs";
|
|
9
9
|
import { handleCommentRequest } from "./engine/react/comment-endpoint.mjs";
|
|
10
10
|
import { handleProjectAssetRequest } from "./engine/react/project-asset-endpoint.mjs";
|
|
11
11
|
|
|
@@ -15,8 +15,11 @@ const workspaceRoot = process.env.OPENPRESS_WORKSPACE_ROOT
|
|
|
15
15
|
: frameworkRoot;
|
|
16
16
|
const sourceRoot = path.join(frameworkRoot, "src");
|
|
17
17
|
const openpressCliPath = path.join(frameworkRoot, "engine", "cli.mjs");
|
|
18
|
-
const staticServerPath = path.join(frameworkRoot, "engine", "static-server.mjs");
|
|
18
|
+
const staticServerPath = path.join(frameworkRoot, "engine", "output", "static-server.mjs");
|
|
19
19
|
const openpressCoreEntry = path.join(frameworkRoot, "src", "openpress", "core", "index.tsx");
|
|
20
|
+
const openpressMdxEntry = path.join(frameworkRoot, "src", "openpress", "mdx", "index.ts");
|
|
21
|
+
const openpressManuscriptEntry = path.join(frameworkRoot, "src", "openpress", "manuscript", "index.tsx");
|
|
22
|
+
const openpressNumberingEntry = path.join(frameworkRoot, "src", "openpress", "numbering", "index.ts");
|
|
20
23
|
const openpressConfig = await loadConfig(workspaceRoot);
|
|
21
24
|
const outputDir = openpressConfig.paths.outputDir;
|
|
22
25
|
const reactDocumentRoot = openpressConfig.paths.documentRoot;
|
|
@@ -55,7 +58,11 @@ export default defineConfig({
|
|
|
55
58
|
resolve: {
|
|
56
59
|
dedupe: ["react", "react-dom", "@mdx-js/react"],
|
|
57
60
|
alias: {
|
|
58
|
-
|
|
61
|
+
// Subpaths must come before the base path so resolution matches longest first.
|
|
62
|
+
"@open-press/core/mdx": openpressMdxEntry,
|
|
63
|
+
"@open-press/core/manuscript": openpressManuscriptEntry,
|
|
64
|
+
"@open-press/core/numbering": openpressNumberingEntry,
|
|
65
|
+
"@open-press/core": openpressCoreEntry,
|
|
59
66
|
"@/components": reactDocumentComponentsRoot,
|
|
60
67
|
"@": sourceRoot,
|
|
61
68
|
...workspaceAliases,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { migrateLegacyWorkspaceToReact } from "../react/migrate-to-react.mjs";
|
|
2
|
-
import { validateWorkspace } from "../validation.mjs";
|
|
3
|
-
|
|
4
|
-
export async function run({ root, config, options }) {
|
|
5
|
-
const result = await migrateLegacyWorkspaceToReact(root, config, {
|
|
6
|
-
dryRun: options.dryRun,
|
|
7
|
-
force: options.force,
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
if (options.json) {
|
|
11
|
-
console.log(JSON.stringify(result, null, 2));
|
|
12
|
-
return 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const verb = options.dryRun ? "would create" : "created";
|
|
16
|
-
console.log(`OpenPress migrate-to-react ${verb} ${result.files.length} paths from ${result.sourceFiles} legacy files:`);
|
|
17
|
-
for (const file of result.files) {
|
|
18
|
-
console.log(` ${file.action.padEnd(5)} ${file.path}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!options.dryRun) {
|
|
22
|
-
const report = await validateWorkspace(root);
|
|
23
|
-
console.log(report.ok ? `OpenPress validation OK\nChecked: ${report.checked.join(", ")}` : report.format());
|
|
24
|
-
return report.ok ? 0 : 1;
|
|
25
|
-
}
|
|
26
|
-
return 0;
|
|
27
|
-
}
|
package/engine/page-renderer.mjs
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
const TOC_ENTRIES_PER_PAGE = 24;
|
|
2
|
-
|
|
3
|
-
function renderPageShell(sectionClass, bodyHtml, attrs = "", { kind, footer = true } = {}) {
|
|
4
|
-
const className = footer === false ? addClass(sectionClass, "no-footer") : sectionClass;
|
|
5
|
-
const attrsPart = pageAttrs(attrs, { kind, footer });
|
|
6
|
-
const footerHtml = footer === false ? "" : `
|
|
7
|
-
<footer class="page-footer" aria-hidden="true"></footer>`;
|
|
8
|
-
return `<section class="${className}"${attrsPart}>
|
|
9
|
-
<div class="page-frame">
|
|
10
|
-
<header class="page-header" aria-hidden="true"></header>
|
|
11
|
-
<main class="page-body">
|
|
12
|
-
${bodyHtml.trim()}
|
|
13
|
-
</main>${footerHtml}
|
|
14
|
-
</div>
|
|
15
|
-
</section>`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function renderToc({ title, items, className } = {}) {
|
|
19
|
-
const headingText = typeof title === "string" && title.trim() ? title.trim() : "Contents";
|
|
20
|
-
const tocItems = Array.isArray(items) ? items : [];
|
|
21
|
-
const tocChunks = tocItems.length > 0 ? chunkArray(tocItems, TOC_ENTRIES_PER_PAGE) : [[]];
|
|
22
|
-
|
|
23
|
-
return tocChunks.map((chunk, pageIndex) => {
|
|
24
|
-
const isContinuation = pageIndex > 0;
|
|
25
|
-
const pageId = pageIndex === 0 ? "toc" : `toc-${String(pageIndex + 1).padStart(2, "0")}`;
|
|
26
|
-
const headingId = pageIndex === 0 ? "toc-title" : `${pageId}-title`;
|
|
27
|
-
const pageHeadingText = isContinuation ? tocContinuationTitle(headingText) : headingText;
|
|
28
|
-
const headingClass = isContinuation ? ` class="toc-heading toc-heading--continuation"` : ` class="toc-heading"`;
|
|
29
|
-
const tocList = chunk.length > 0
|
|
30
|
-
? `
|
|
31
|
-
<ol class="toc-list">
|
|
32
|
-
${chunk.map((item, index) => {
|
|
33
|
-
const level = item.level === 3 ? 3 : 2;
|
|
34
|
-
const absoluteIndex = pageIndex * TOC_ENTRIES_PER_PAGE + index;
|
|
35
|
-
const label = item.label || (level === 2 ? `#${absoluteIndex + 1}` : "");
|
|
36
|
-
const targetPageIndex = Math.max(0, Number(item.pageNumber || 1) - 1);
|
|
37
|
-
return ` <li class="toc-level-${level}"><a href="#${escapeAttr(item.id)}" data-openpress-anchor="${escapeAttr(item.id)}" data-openpress-target-page-index="${targetPageIndex}"><span class="toc-index" data-toc-index="${escapeAttr(label)}">${escapeHtml(label)}</span><span class="toc-title">${escapeHtml(item.title)}</span><span class="toc-page">${String(item.pageNumber).padStart(2, "0")}</span></a></li>`;
|
|
38
|
-
}).join("\n")}
|
|
39
|
-
</ol>
|
|
40
|
-
`
|
|
41
|
-
: "";
|
|
42
|
-
return renderPageShell(
|
|
43
|
-
tocPageClassName(className, isContinuation),
|
|
44
|
-
`
|
|
45
|
-
<h2 id="${headingId}"${headingClass}>${escapeHtml(pageHeadingText)}</h2>
|
|
46
|
-
${tocList}
|
|
47
|
-
`,
|
|
48
|
-
[
|
|
49
|
-
`id="${pageId}"`,
|
|
50
|
-
`data-page-title="${escapeAttr(headingText)}"`,
|
|
51
|
-
`data-toc-continuation="${isContinuation ? "true" : "false"}"`,
|
|
52
|
-
`aria-labelledby="${headingId}"`,
|
|
53
|
-
].filter(Boolean).join(" "),
|
|
54
|
-
{ kind: "toc", footer: false },
|
|
55
|
-
);
|
|
56
|
-
}).join("\n\n");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function tocContinuationTitle(title) {
|
|
60
|
-
return title === "目錄" ? "目錄續" : `${title} continued`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function pageAttrs(attrs, { kind, footer } = {}) {
|
|
64
|
-
const parts = [];
|
|
65
|
-
if (attrs.trim()) parts.push(attrs.trim());
|
|
66
|
-
if (kind && !hasAttr(attrs, "data-page-kind")) parts.push(`data-page-kind="${escapeAttr(kind)}"`);
|
|
67
|
-
if (footer === false && !hasAttr(attrs, "data-page-footer")) parts.push('data-page-footer="false"');
|
|
68
|
-
return parts.length ? ` ${parts.join(" ")}` : "";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function hasAttr(attrs, name) {
|
|
72
|
-
return new RegExp(`\\b${name}=`).test(attrs);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function addClass(className, extraClass) {
|
|
76
|
-
const classes = className.split(/\s+/).filter(Boolean);
|
|
77
|
-
if (!classes.includes(extraClass)) classes.push(extraClass);
|
|
78
|
-
return classes.join(" ");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function injectStaticToc(pages) {
|
|
82
|
-
const tocItems = collectTocItems(pages);
|
|
83
|
-
if (tocItems.length === 0) return pages;
|
|
84
|
-
const tocIndex = pages.findIndex((page) => hasPageKind(page.match(/^<section[^>]*>/i)?.[0] ?? "", "toc"));
|
|
85
|
-
const tocPageCount = Math.max(1, Math.ceil(tocItems.length / TOC_ENTRIES_PER_PAGE));
|
|
86
|
-
const tocPageNumber = tocIndex + 1;
|
|
87
|
-
const adjustedTocItems = tocPageCount > 1 && tocIndex >= 0
|
|
88
|
-
? tocItems.map((item) => ({
|
|
89
|
-
...item,
|
|
90
|
-
pageNumber: item.pageNumber > tocPageNumber ? item.pageNumber + tocPageCount - 1 : item.pageNumber,
|
|
91
|
-
}))
|
|
92
|
-
: tocItems;
|
|
93
|
-
|
|
94
|
-
return pages.map((page) => {
|
|
95
|
-
const openingTag = page.match(/^<section[^>]*>/i)?.[0] ?? "";
|
|
96
|
-
if (!hasPageKind(openingTag, "toc")) return page;
|
|
97
|
-
const title = extractAttr(openingTag, "data-page-title");
|
|
98
|
-
return renderToc({ title, items: adjustedTocItems, className: extractAttr(openingTag, "class") });
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function tocPageClassName(className, isContinuation) {
|
|
103
|
-
const classes = new Set(String(className || "reader-page reader-page--toc").split(/\s+/).filter(Boolean));
|
|
104
|
-
classes.delete("toc");
|
|
105
|
-
classes.add("reader-page");
|
|
106
|
-
classes.add("reader-page--toc");
|
|
107
|
-
if (isContinuation) classes.add("toc-continuation");
|
|
108
|
-
else classes.delete("toc-continuation");
|
|
109
|
-
return [...classes].join(" ");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function extractAttr(openingTag, name) {
|
|
113
|
-
const re = new RegExp(`${name}="([^"]*)"`);
|
|
114
|
-
return openingTag.match(re)?.[1];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function collectTocItems(pages) {
|
|
118
|
-
const items = [];
|
|
119
|
-
let chapterIndex = 0;
|
|
120
|
-
let sectionIndex = 0;
|
|
121
|
-
let pendingChapterOpener;
|
|
122
|
-
|
|
123
|
-
pages.forEach((page, index) => {
|
|
124
|
-
const openingTag = page.match(/^<section[^>]*>/i)?.[0] ?? "";
|
|
125
|
-
if (hasPageKind(openingTag, "chapter-opener")) {
|
|
126
|
-
pendingChapterOpener = extractChapterOpenerTarget(page, index);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (!hasContentPageKind(openingTag)) return;
|
|
131
|
-
|
|
132
|
-
let pageStartedChapter = false;
|
|
133
|
-
const headings = [...page.matchAll(/<h([23])\b[^>]*\bid="([^"]+)"[^>]*>([\s\S]*?)<\/h\1>/gi)];
|
|
134
|
-
headings.forEach((heading) => {
|
|
135
|
-
const level = Number(heading[1]);
|
|
136
|
-
if (level === 2) {
|
|
137
|
-
const opener = pendingChapterOpener;
|
|
138
|
-
pendingChapterOpener = undefined;
|
|
139
|
-
pageStartedChapter = true;
|
|
140
|
-
chapterIndex += 1;
|
|
141
|
-
sectionIndex = 0;
|
|
142
|
-
items.push({
|
|
143
|
-
id: opener?.id ?? heading[2],
|
|
144
|
-
title: htmlToText(heading[3]),
|
|
145
|
-
pageNumber: opener?.pageNumber ?? index + 1,
|
|
146
|
-
level: 2,
|
|
147
|
-
label: `#${chapterIndex}`,
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (level === 3 && chapterIndex > 0) {
|
|
153
|
-
sectionIndex += 1;
|
|
154
|
-
items.push({
|
|
155
|
-
id: heading[2],
|
|
156
|
-
title: htmlToText(heading[3]),
|
|
157
|
-
pageNumber: index + 1,
|
|
158
|
-
level: 3,
|
|
159
|
-
label: `${chapterIndex}.${sectionIndex}`,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
if (!pageStartedChapter) pendingChapterOpener = undefined;
|
|
164
|
-
});
|
|
165
|
-
return items;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function extractChapterOpenerTarget(page, index) {
|
|
169
|
-
const heading = page.match(/<h2\b[^>]*\bid="([^"]+)"[^>]*>([\s\S]*?)<\/h2>/i);
|
|
170
|
-
if (!heading?.[1]) return undefined;
|
|
171
|
-
return {
|
|
172
|
-
id: heading[1],
|
|
173
|
-
pageNumber: index + 1,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function htmlToText(html) {
|
|
178
|
-
return html
|
|
179
|
-
.replace(/<[^>]+>/g, "")
|
|
180
|
-
.replaceAll(" ", " ")
|
|
181
|
-
.replaceAll("&", "&")
|
|
182
|
-
.replaceAll("<", "<")
|
|
183
|
-
.replaceAll(">", ">")
|
|
184
|
-
.replaceAll(""", '"')
|
|
185
|
-
.trim();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function hasPageKind(openingTag, kind) {
|
|
189
|
-
return extractAttr(openingTag, "data-page-kind") === kind;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function hasContentPageKind(openingTag) {
|
|
193
|
-
return extractAttr(openingTag, "data-page-kind") === "content";
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function escapeAttr(value) {
|
|
197
|
-
return String(value)
|
|
198
|
-
.replaceAll("&", "&")
|
|
199
|
-
.replaceAll('"', """)
|
|
200
|
-
.replaceAll("<", "<")
|
|
201
|
-
.replaceAll(">", ">");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function escapeHtml(value) {
|
|
205
|
-
return String(value)
|
|
206
|
-
.replaceAll("&", "&")
|
|
207
|
-
.replaceAll("<", "<")
|
|
208
|
-
.replaceAll(">", ">");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function chunkArray(items, size) {
|
|
212
|
-
const chunks = [];
|
|
213
|
-
for (let index = 0; index < items.length; index += size) {
|
|
214
|
-
chunks.push(items.slice(index, index + size));
|
|
215
|
-
}
|
|
216
|
-
return chunks;
|
|
217
|
-
}
|