@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
@@ -33,18 +33,14 @@ import {
33
33
  ProjectEntryPanel,
34
34
  type ProjectMentionItem,
35
35
  } from "./projectWorkspace";
36
- import { paginateSourcePages, type PaginatedPage } from "./pagination";
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 numberedPages = useMemo(() => numberSourceHeadings(pages), [pages]);
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
- "@openpress/core": ["./src/openpress/core/index.tsx"],
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
- "@openpress/core": openpressCoreEntry,
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
- }
@@ -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("&nbsp;", " ")
181
- .replaceAll("&amp;", "&")
182
- .replaceAll("&lt;", "<")
183
- .replaceAll("&gt;", ">")
184
- .replaceAll("&quot;", '"')
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("&", "&amp;")
199
- .replaceAll('"', "&quot;")
200
- .replaceAll("<", "&lt;")
201
- .replaceAll(">", "&gt;");
202
- }
203
-
204
- function escapeHtml(value) {
205
- return String(value)
206
- .replaceAll("&", "&amp;")
207
- .replaceAll("<", "&lt;")
208
- .replaceAll(">", "&gt;");
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
- }