@open-press/core 0.7.1 → 1.0.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 +6 -3
- package/engine/cli.mjs +8 -8
- package/engine/commands/_shared.mjs +37 -15
- package/engine/commands/dev.mjs +2 -2
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +63 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +110 -3
- package/engine/output/static-server.mjs +87 -9
- package/engine/react/comment-endpoint.mjs +13 -39
- package/engine/react/comment-marker.mjs +43 -19
- package/engine/react/document-entry.mjs +46 -28
- package/engine/react/document-export.mjs +328 -164
- package/engine/react/http-json.mjs +24 -0
- package/engine/react/mdx-compile.mjs +126 -3
- package/engine/react/measurement-css.mjs +114 -1
- package/engine/react/object-entities.mjs +204 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +41 -72
- package/engine/react/pipeline/frame-measurement.mjs +6 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- package/engine/react/project-asset-endpoint.mjs +6 -24
- package/engine/react/source-edit-endpoint.d.mts +10 -0
- package/engine/react/source-edit-endpoint.mjs +75 -0
- package/engine/react/sources/mdx-resolver.mjs +13 -15
- package/engine/react/style-discovery.mjs +23 -8
- package/engine/runtime/config.d.mts +8 -0
- package/engine/runtime/config.mjs +57 -60
- package/engine/runtime/file-utils.mjs +9 -1
- package/engine/runtime/file-walk.mjs +22 -0
- package/engine/runtime/inspection.mjs +1 -20
- package/engine/runtime/page-geometry.mjs +131 -0
- package/engine/runtime/path-utils.mjs +20 -0
- package/engine/runtime/source-text-tools.d.mts +102 -0
- package/engine/runtime/source-text-tools.mjs +551 -16
- package/engine/runtime/source-workspace.mjs +16 -34
- package/engine/runtime/validation.mjs +19 -10
- package/package.json +3 -5
- package/src/openpress/app/OpenPressApp.tsx +296 -0
- package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/app/index.ts +2 -0
- package/src/openpress/core/Frame.tsx +26 -15
- package/src/openpress/core/FrameContext.tsx +10 -3
- package/src/openpress/core/MdxArea.tsx +11 -12
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/cn.ts +4 -0
- package/src/openpress/core/index.tsx +11 -3
- package/src/openpress/core/primitives.tsx +74 -6
- package/src/openpress/core/types.ts +94 -41
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
- package/src/openpress/document-model/index.ts +7 -0
- package/src/openpress/document-model/objectEntityModel.ts +55 -0
- package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/manuscript/index.tsx +49 -7
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/src/openpress/reader/index.ts +11 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/src/openpress/reader/readerTypes.ts +4 -0
- package/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/src/openpress/reader/usePanelState.ts +56 -0
- package/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/src/openpress/shared/Panel.tsx +77 -0
- package/src/openpress/shared/index.ts +4 -0
- package/src/openpress/shared/numberUtils.ts +3 -0
- package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/src/openpress/workbench/Workbench.tsx +506 -0
- package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/src/openpress/workbench/actions/index.ts +6 -0
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/src/openpress/workbench/dialog/index.ts +1 -0
- package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/src/openpress/workbench/document/index.ts +10 -0
- package/src/openpress/workbench/index.ts +2 -0
- package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/src/openpress/workbench/inspector/index.ts +5 -0
- package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
- package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/src/openpress/workbench/mentions/index.ts +2 -0
- package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
- package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/src/openpress/workbench/panels/index.ts +3 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
- package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/src/openpress/workbench/project/index.ts +2 -0
- package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/src/openpress/workbench/shell/index.ts +1 -0
- package/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/src/styles/openpress/print-route.css +0 -2
- package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/src/styles/openpress/public-viewer.css +25 -320
- package/src/styles/openpress/reader-runtime.css +252 -55
- package/src/styles/openpress/responsive.css +145 -270
- package/src/styles/openpress/workbench-panels.css +327 -178
- package/src/styles/openpress/workbench.css +986 -451
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +2 -1
- package/tsconfig.json +1 -1
- package/vite.config.ts +50 -0
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
- package/src/openpress/App.tsx +0 -127
- package/src/openpress/inspector.ts +0 -282
- package/src/openpress/projectWorkspace.tsx +0 -919
- package/src/openpress/readerRuntime.ts +0 -230
- package/src/openpress/workbench.tsx +0 -1265
- package/src/openpress/workbenchTypes.ts +0 -4
- /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
|
@@ -112,6 +112,7 @@ export function rehypeBlockIds(options = {}) {
|
|
|
112
112
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
113
113
|
|
|
114
114
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
115
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
115
116
|
const extraAttributes = blockAttributes.get(id);
|
|
116
117
|
if (extraAttributes) {
|
|
117
118
|
for (const [name, value] of Object.entries(extraAttributes)) {
|
|
@@ -142,9 +143,19 @@ function applyTableRowBlocks({
|
|
|
142
143
|
includeBlockIds,
|
|
143
144
|
}) {
|
|
144
145
|
const rows = tableBodyRows(node);
|
|
146
|
+
const header = tableHeaderRow(node);
|
|
147
|
+
const caption = tableCaption(node);
|
|
148
|
+
const captionRecord = caption ? { id: `${id}-caption`, node: caption } : null;
|
|
149
|
+
const headerRecord = header ? { id: `${id}-h0`, node: header } : null;
|
|
150
|
+
const selectedCaption = captionRecord && (!includeBlockIds || includeBlockIds.has(captionRecord.id));
|
|
151
|
+
const selectedHeader = headerRecord && (!includeBlockIds || includeBlockIds.has(headerRecord.id));
|
|
152
|
+
const firstSelectedRowIndex = selectedFirstTableRowIndex(rows, includeBlockIds, id);
|
|
153
|
+
const renderCaption = selectedCaption || (captionRecord && includeBlockIds && firstSelectedRowIndex === 0);
|
|
154
|
+
const renderHeader = Boolean(headerRecord && (!includeBlockIds || firstSelectedRowIndex === 0 || selectedHeader));
|
|
145
155
|
if (rows.length === 0) {
|
|
146
156
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
147
157
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
158
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
148
159
|
blocks.push({
|
|
149
160
|
id,
|
|
150
161
|
kind: "element",
|
|
@@ -165,15 +176,64 @@ function applyTableRowBlocks({
|
|
|
165
176
|
const selected = includeBlockIds
|
|
166
177
|
? rowRecords.filter((row) => includeBlockIds.has(row.id))
|
|
167
178
|
: rowRecords;
|
|
168
|
-
if (selected.length === 0) return false;
|
|
179
|
+
if (selected.length === 0 && !selectedCaption && !selectedHeader) return false;
|
|
169
180
|
|
|
170
181
|
setDataAttribute(node, "data-openpress-table-id", id);
|
|
182
|
+
if (headerRecord && renderHeader) {
|
|
183
|
+
setDataAttribute(headerRecord.node, "data-openpress-block-id", headerRecord.id);
|
|
184
|
+
setDataAttribute(headerRecord.node, "data-openpress-object-id", createBlockObjectEntityId(headerRecord.id));
|
|
185
|
+
setDataAttribute(headerRecord.node, "data-openpress-block-layout", "attached");
|
|
186
|
+
annotateTableCells(headerRecord.node, headerRecord.id);
|
|
187
|
+
}
|
|
188
|
+
if (captionRecord) {
|
|
189
|
+
if (renderCaption) {
|
|
190
|
+
setDataAttribute(captionRecord.node, "data-openpress-block-id", captionRecord.id);
|
|
191
|
+
setDataAttribute(captionRecord.node, "data-openpress-object-id", createBlockObjectEntityId(captionRecord.id));
|
|
192
|
+
if (selectedCaption) {
|
|
193
|
+
blocks.push({
|
|
194
|
+
id: captionRecord.id,
|
|
195
|
+
kind: "element",
|
|
196
|
+
name: "caption",
|
|
197
|
+
text: textContent(captionRecord.node).trim() || undefined,
|
|
198
|
+
filePath,
|
|
199
|
+
chapterSlug,
|
|
200
|
+
tableId: id,
|
|
201
|
+
layout: "attached",
|
|
202
|
+
source: sourcePosition(captionRecord.node.position ?? node.position),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
removeTableCaption(node);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (headerRecord && selectedHeader) {
|
|
210
|
+
blocks.push({
|
|
211
|
+
id: headerRecord.id,
|
|
212
|
+
kind: "table-row",
|
|
213
|
+
name: "table-header-row",
|
|
214
|
+
text: textContent(headerRecord.node).trim() || undefined,
|
|
215
|
+
filePath,
|
|
216
|
+
chapterSlug,
|
|
217
|
+
tableId: id,
|
|
218
|
+
rowIndex: -1,
|
|
219
|
+
layout: "attached",
|
|
220
|
+
source: sourcePosition(headerRecord.node.position ?? node.position),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
171
223
|
const selectedNodes = new Set(selected.map((row) => row.node));
|
|
172
224
|
pruneUnselectedTableRows(node, new Set(rowRecords.map((row) => row.node)), selectedNodes);
|
|
173
|
-
if (
|
|
225
|
+
if (!renderHeader) stripTableHeader(node);
|
|
174
226
|
|
|
175
227
|
for (const row of selected) {
|
|
176
228
|
setDataAttribute(row.node, "data-openpress-block-id", row.id);
|
|
229
|
+
setDataAttribute(row.node, "data-openpress-object-id", createBlockObjectEntityId(row.id));
|
|
230
|
+
// Bake cell-level object ids into every <td>/<th>. The inspector resolves
|
|
231
|
+
// a clicked target via `closest("[data-openpress-object-id]")` — without
|
|
232
|
+
// this, a click inside a cell would walk up to the row and a comment
|
|
233
|
+
// would target the entire row. With the cell-precision id present in the
|
|
234
|
+
// static HTML the inspector targets the individual cell, matching the
|
|
235
|
+
// engine's per-cell source-edit pipeline (`cellIndex`).
|
|
236
|
+
annotateTableCells(row.node, row.id);
|
|
177
237
|
blocks.push({
|
|
178
238
|
id: row.id,
|
|
179
239
|
kind: "table-row",
|
|
@@ -189,6 +249,28 @@ function applyTableRowBlocks({
|
|
|
189
249
|
return "skip";
|
|
190
250
|
}
|
|
191
251
|
|
|
252
|
+
function annotateTableCells(rowNode, rowBlockId) {
|
|
253
|
+
const children = Array.isArray(rowNode?.children) ? rowNode.children : [];
|
|
254
|
+
let cellIndex = 0;
|
|
255
|
+
for (const child of children) {
|
|
256
|
+
if (child?.type !== "element") continue;
|
|
257
|
+
if (child.tagName !== "td" && child.tagName !== "th") continue;
|
|
258
|
+
// Inherit the row's block id so `findObjectSelection` can resolve the
|
|
259
|
+
// cell's underlying SourceBlock (which lives on the row). The
|
|
260
|
+
// cell-precision `data-openpress-object-id` + cellIndex still let the
|
|
261
|
+
// inspector / source-edit pipeline target a single cell within that row.
|
|
262
|
+
// `data-openpress-inherited-block-id="true"` keeps the same convention
|
|
263
|
+
// the inline editor uses for caption / cell descendants, so block
|
|
264
|
+
// measurement (which queries `[data-openpress-block-id]`) can skip
|
|
265
|
+
// these and not double-count the row's height across N cells.
|
|
266
|
+
setDataAttribute(child, "data-openpress-block-id", rowBlockId);
|
|
267
|
+
setDataAttribute(child, "data-openpress-inherited-block-id", "true");
|
|
268
|
+
setDataAttribute(child, "data-openpress-object-id", `${createBlockObjectEntityId(rowBlockId)}:cell:${cellIndex}`);
|
|
269
|
+
setDataAttribute(child, "data-openpress-table-cell-index", String(cellIndex));
|
|
270
|
+
cellIndex += 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
192
274
|
export function remarkBlockOnlyMdx(options = {}) {
|
|
193
275
|
const filePath = String(options.filePath ?? "document.mdx");
|
|
194
276
|
|
|
@@ -229,6 +311,7 @@ function normalizeTableCaptions(node) {
|
|
|
229
311
|
type: "element",
|
|
230
312
|
tagName: "caption",
|
|
231
313
|
properties: {},
|
|
314
|
+
position: child.position,
|
|
232
315
|
children: [{ type: "text", value: captionText }],
|
|
233
316
|
});
|
|
234
317
|
}
|
|
@@ -272,8 +355,10 @@ function wrapMdxComponents(components) {
|
|
|
272
355
|
if (typeof Component !== "function") continue;
|
|
273
356
|
wrapped[name] = function ComponentBlock(props = {}) {
|
|
274
357
|
const blockId = props["data-openpress-block-id"];
|
|
358
|
+
const objectId = props["data-openpress-object-id"] || (blockId ? createBlockObjectEntityId(blockId) : undefined);
|
|
275
359
|
const rest = { ...props };
|
|
276
360
|
delete rest["data-openpress-block-id"];
|
|
361
|
+
delete rest["data-openpress-object-id"];
|
|
277
362
|
|
|
278
363
|
if (!blockId) return React.createElement(Component, rest);
|
|
279
364
|
|
|
@@ -281,6 +366,7 @@ function wrapMdxComponents(components) {
|
|
|
281
366
|
"div",
|
|
282
367
|
{
|
|
283
368
|
"data-openpress-block-id": blockId,
|
|
369
|
+
"data-openpress-object-id": objectId,
|
|
284
370
|
"data-openpress-component-block": name,
|
|
285
371
|
},
|
|
286
372
|
React.createElement(Component, rest),
|
|
@@ -336,6 +422,7 @@ function applyListItemBlocks({
|
|
|
336
422
|
if (items.length === 0) {
|
|
337
423
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
338
424
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
425
|
+
setDataAttribute(node, "data-openpress-object-id", createBlockObjectEntityId(id));
|
|
339
426
|
blocks.push({
|
|
340
427
|
id,
|
|
341
428
|
kind: "element",
|
|
@@ -375,6 +462,7 @@ function applyListItemBlocks({
|
|
|
375
462
|
|
|
376
463
|
for (const item of selected) {
|
|
377
464
|
setDataAttribute(item.node, "data-openpress-block-id", item.id);
|
|
465
|
+
setDataAttribute(item.node, "data-openpress-object-id", createBlockObjectEntityId(item.id));
|
|
378
466
|
blocks.push({
|
|
379
467
|
id: item.id,
|
|
380
468
|
kind: "list-item",
|
|
@@ -419,6 +507,28 @@ function tableBodyRows(table) {
|
|
|
419
507
|
return (table.children ?? []).filter((child) => child?.type === "element" && child.tagName === "tr");
|
|
420
508
|
}
|
|
421
509
|
|
|
510
|
+
function tableHeaderRow(table) {
|
|
511
|
+
if (table?.type !== "element" || table.tagName !== "table") return null;
|
|
512
|
+
for (const child of table.children ?? []) {
|
|
513
|
+
if (child?.type !== "element" || child.tagName !== "thead") continue;
|
|
514
|
+
return (child.children ?? []).find((row) => row?.type === "element" && row.tagName === "tr") ?? null;
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function selectedFirstTableRowIndex(rows, includeBlockIds, tableId) {
|
|
520
|
+
if (!includeBlockIds) return 0;
|
|
521
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
522
|
+
if (includeBlockIds.has(`${tableId}-r${index}`)) return index;
|
|
523
|
+
}
|
|
524
|
+
return -1;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function tableCaption(table) {
|
|
528
|
+
if (table?.type !== "element" || table.tagName !== "table") return null;
|
|
529
|
+
return (table.children ?? []).find((child) => child?.type === "element" && child.tagName === "caption") ?? null;
|
|
530
|
+
}
|
|
531
|
+
|
|
422
532
|
function pruneUnselectedTableRows(node, rowNodes, selectedNodes) {
|
|
423
533
|
if (!Array.isArray(node?.children)) return;
|
|
424
534
|
node.children = node.children.filter((child) => {
|
|
@@ -432,10 +542,15 @@ function stripTableHeader(table) {
|
|
|
432
542
|
if (!Array.isArray(table?.children)) return;
|
|
433
543
|
table.children = table.children.filter((child) => {
|
|
434
544
|
if (child?.type !== "element") return true;
|
|
435
|
-
return child.tagName !== "
|
|
545
|
+
return child.tagName !== "thead";
|
|
436
546
|
});
|
|
437
547
|
}
|
|
438
548
|
|
|
549
|
+
function removeTableCaption(table) {
|
|
550
|
+
if (!Array.isArray(table?.children)) return;
|
|
551
|
+
table.children = table.children.filter((child) => child?.type !== "element" || child.tagName !== "caption");
|
|
552
|
+
}
|
|
553
|
+
|
|
439
554
|
function headingText(node) {
|
|
440
555
|
if (!/^h[1-6]$/.test(String(node?.tagName ?? ""))) return undefined;
|
|
441
556
|
return textContent(node).trim() || undefined;
|
|
@@ -470,6 +585,14 @@ function setDataAttribute(node, name, value) {
|
|
|
470
585
|
node.properties[name] = value;
|
|
471
586
|
}
|
|
472
587
|
|
|
588
|
+
function createObjectEntityId(kind, ...parts) {
|
|
589
|
+
return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function createBlockObjectEntityId(blockId) {
|
|
593
|
+
return createObjectEntityId("mdx-block", blockId);
|
|
594
|
+
}
|
|
595
|
+
|
|
473
596
|
function visit(node, visitor) {
|
|
474
597
|
visitor(node);
|
|
475
598
|
if (!Array.isArray(node?.children)) return;
|
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { buildComponentsCss, buildContentCss } from "../runtime/file-utils.mjs";
|
|
6
|
+
import { pageGeometryToTheme } from "../runtime/page-geometry.mjs";
|
|
6
7
|
import { buildSectionScopedCss } from "./section-css.mjs";
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
@@ -11,6 +12,7 @@ export async function buildReactMeasurementCss(root, config, workspace) {
|
|
|
11
12
|
const parts = [];
|
|
12
13
|
await appendOptionalFile(parts, path.join(config.paths.themeDir, "fonts.css"), "theme/fonts.css");
|
|
13
14
|
await appendOptionalFile(parts, path.join(config.paths.themeDir, "tokens.css"), "theme/tokens.css");
|
|
15
|
+
appendPageGeometryCss(parts, config.page);
|
|
14
16
|
parts.push("/* === public/openpress/content.css === */\n");
|
|
15
17
|
parts.push(await buildContentCss(root, config));
|
|
16
18
|
parts.push("\n/* === public/openpress/components.css === */\n");
|
|
@@ -20,7 +22,26 @@ export async function buildReactMeasurementCss(root, config, workspace) {
|
|
|
20
22
|
parts.push("\n/* === public/openpress/chapter-scoped.css === */\n");
|
|
21
23
|
parts.push(chapterCss);
|
|
22
24
|
}
|
|
23
|
-
return rewriteAssetUrls(parts.join("\n"), config);
|
|
25
|
+
return rewriteAssetUrls(stripViewportMediaQueries(parts.join("\n")), config);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function appendPageGeometryCss(parts, page) {
|
|
29
|
+
const theme = pageGeometryToTheme(page);
|
|
30
|
+
if (!theme) return;
|
|
31
|
+
|
|
32
|
+
const declarations = [
|
|
33
|
+
["--openpress-page-width", theme.pageWidth],
|
|
34
|
+
["--openpress-page-height", theme.pageHeight],
|
|
35
|
+
["--openpress-page-aspect-ratio", theme.pageAspectRatio],
|
|
36
|
+
["--openpress-page-height-ratio", theme.pageHeightRatio],
|
|
37
|
+
].filter(([, value]) => value);
|
|
38
|
+
|
|
39
|
+
parts.push("/* === openpress page geometry === */\n");
|
|
40
|
+
parts.push(":root {\n");
|
|
41
|
+
for (const [name, value] of declarations) {
|
|
42
|
+
parts.push(` ${name}: ${value};\n`);
|
|
43
|
+
}
|
|
44
|
+
parts.push("}\n\n");
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
async function appendOptionalFile(parts, filePath, label) {
|
|
@@ -42,3 +63,95 @@ function rewriteAssetUrls(css, config) {
|
|
|
42
63
|
.replace(/url\((["'])?\/openpress\/fonts\//g, `url($1${themeFontsDir}`)
|
|
43
64
|
.replace(/url\((["'])?\/openpress\/katex-fonts\//g, `url($1${katexFontsDir}`);
|
|
44
65
|
}
|
|
66
|
+
|
|
67
|
+
function stripViewportMediaQueries(css) {
|
|
68
|
+
let output = "";
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
|
|
71
|
+
while (cursor < css.length) {
|
|
72
|
+
const mediaIndex = css.indexOf("@media", cursor);
|
|
73
|
+
if (mediaIndex < 0) {
|
|
74
|
+
output += css.slice(cursor);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
output += css.slice(cursor, mediaIndex);
|
|
79
|
+
const blockStart = css.indexOf("{", mediaIndex);
|
|
80
|
+
if (blockStart < 0) {
|
|
81
|
+
output += css.slice(mediaIndex);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const prelude = css.slice(mediaIndex + "@media".length, blockStart);
|
|
86
|
+
const blockEnd = findCssBlockEnd(css, blockStart);
|
|
87
|
+
if (blockEnd < 0) {
|
|
88
|
+
output += css.slice(mediaIndex);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!isViewportMediaPrelude(prelude)) {
|
|
93
|
+
output += css.slice(mediaIndex, blockEnd + 1);
|
|
94
|
+
}
|
|
95
|
+
cursor = blockEnd + 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return output;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isViewportMediaPrelude(prelude) {
|
|
102
|
+
if (/\bprint\b/i.test(prelude)) return false;
|
|
103
|
+
return /\(\s*(?:min-|max-)?(?:device-)?(?:width|height)\s*:/i.test(prelude)
|
|
104
|
+
|| /\(\s*orientation\s*:/i.test(prelude)
|
|
105
|
+
|| /\(\s*(?:min-|max-)?aspect-ratio\s*:/i.test(prelude);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findCssBlockEnd(css, blockStart) {
|
|
109
|
+
let depth = 0;
|
|
110
|
+
let quote = "";
|
|
111
|
+
let inComment = false;
|
|
112
|
+
|
|
113
|
+
for (let index = blockStart; index < css.length; index += 1) {
|
|
114
|
+
const current = css[index];
|
|
115
|
+
const next = css[index + 1];
|
|
116
|
+
|
|
117
|
+
if (inComment) {
|
|
118
|
+
if (current === "*" && next === "/") {
|
|
119
|
+
inComment = false;
|
|
120
|
+
index += 1;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (quote) {
|
|
126
|
+
if (current === "\\") {
|
|
127
|
+
index += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (current === quote) quote = "";
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (current === "/" && next === "*") {
|
|
135
|
+
inComment = true;
|
|
136
|
+
index += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (current === "\"" || current === "'") {
|
|
141
|
+
quote = current;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (current === "{") {
|
|
146
|
+
depth += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (current === "}") {
|
|
151
|
+
depth -= 1;
|
|
152
|
+
if (depth === 0) return index;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return -1;
|
|
157
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
export function createObjectEntityId(kind, ...parts) {
|
|
2
|
+
return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function createBlockObjectEntityId(blockId) {
|
|
6
|
+
return createObjectEntityId("mdx-block", blockId);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createPageObjectEntityId(frameKey) {
|
|
10
|
+
return createObjectEntityId("page", frameKey);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createFrameObjectEntityId(frameKey) {
|
|
14
|
+
return createObjectEntityId("frame", frameKey);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createScopedObjectEntityId(kind, parentId, objectId) {
|
|
18
|
+
return parentId ? createObjectEntityId(kind, parentId, objectId) : createObjectEntityId(kind, objectId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createMdxAreaObjectEntityId(frameKey, chainId, indexInFrame) {
|
|
22
|
+
return createObjectEntityId("mdx-area", frameKey, chainId, indexInFrame);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildObjectEntities({ frames, blocks, blockMap }) {
|
|
26
|
+
const entities = {};
|
|
27
|
+
const blockParentIdByBlockId = new Map();
|
|
28
|
+
|
|
29
|
+
for (const block of blocks) {
|
|
30
|
+
const frameKey = block.frameKey ?? block.id;
|
|
31
|
+
const sourcePath = block.source?.path;
|
|
32
|
+
const pageId = createPageObjectEntityId(frameKey);
|
|
33
|
+
const base = {
|
|
34
|
+
frameKey,
|
|
35
|
+
pageId,
|
|
36
|
+
source: sourcePath
|
|
37
|
+
? {
|
|
38
|
+
path: sourcePath,
|
|
39
|
+
file: block.source?.file,
|
|
40
|
+
line: 1,
|
|
41
|
+
column: 1,
|
|
42
|
+
}
|
|
43
|
+
: undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
entities[pageId] = {
|
|
47
|
+
id: pageId,
|
|
48
|
+
kind: "page",
|
|
49
|
+
label: block.title || `Page ${block.pageNumber}`,
|
|
50
|
+
...base,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const frameId = createFrameObjectEntityId(frameKey);
|
|
54
|
+
entities[frameId] = {
|
|
55
|
+
id: frameId,
|
|
56
|
+
kind: "frame",
|
|
57
|
+
label: block.role || block.title || frameKey,
|
|
58
|
+
...base,
|
|
59
|
+
parentId: pageId,
|
|
60
|
+
metadata: { role: block.role ?? null, chrome: block.chrome ?? null },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const frame of frames) {
|
|
65
|
+
const pageId = createPageObjectEntityId(frame.frameKey);
|
|
66
|
+
const frameId = createFrameObjectEntityId(frame.frameKey);
|
|
67
|
+
for (const area of frame.mdxAreas ?? []) {
|
|
68
|
+
const id = createMdxAreaObjectEntityId(frame.frameKey, area.chainId, area.indexInFrame);
|
|
69
|
+
const firstEditableBlock = (area.blockIds ?? [])
|
|
70
|
+
.map((blockId) => blockMap[blockId])
|
|
71
|
+
.find((block) => block?.path);
|
|
72
|
+
for (const blockId of area.blockIds ?? []) blockParentIdByBlockId.set(blockId, id);
|
|
73
|
+
entities[id] = {
|
|
74
|
+
id,
|
|
75
|
+
kind: "mdx-area",
|
|
76
|
+
label: `${area.chainId} area ${area.indexInFrame + 1}`,
|
|
77
|
+
parentId: frameId,
|
|
78
|
+
pageId,
|
|
79
|
+
frameKey: frame.frameKey,
|
|
80
|
+
chainId: area.chainId,
|
|
81
|
+
source: firstEditableBlock
|
|
82
|
+
? {
|
|
83
|
+
path: firstEditableBlock.path,
|
|
84
|
+
source: firstEditableBlock.source,
|
|
85
|
+
line: firstEditableBlock.source?.line,
|
|
86
|
+
column: firstEditableBlock.source?.column,
|
|
87
|
+
}
|
|
88
|
+
: undefined,
|
|
89
|
+
metadata: { blockCount: area.blockIds?.length ?? 0 },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const block of Object.values(blockMap)) {
|
|
95
|
+
if (!block?.id) continue;
|
|
96
|
+
const id = createBlockObjectEntityId(block.id);
|
|
97
|
+
const pageId = block.frameKey ? createPageObjectEntityId(block.frameKey) : undefined;
|
|
98
|
+
entities[id] = {
|
|
99
|
+
id,
|
|
100
|
+
kind: "mdx-block",
|
|
101
|
+
label: block.name ? `${block.name} ${block.id}` : block.id,
|
|
102
|
+
parentId: blockParentIdByBlockId.get(block.id),
|
|
103
|
+
pageId,
|
|
104
|
+
blockId: block.id,
|
|
105
|
+
frameKey: block.frameKey,
|
|
106
|
+
chainId: block.chainId,
|
|
107
|
+
source: block.path
|
|
108
|
+
? {
|
|
109
|
+
path: block.path,
|
|
110
|
+
source: block.source,
|
|
111
|
+
line: block.source?.line,
|
|
112
|
+
column: block.source?.column,
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
115
|
+
metadata: {
|
|
116
|
+
blockKind: block.kind ?? null,
|
|
117
|
+
componentName: block.kind === "component" ? block.name ?? null : null,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const entity of collectRenderedObjectEntities(frames)) {
|
|
123
|
+
if (!entity.id || entities[entity.id]) continue;
|
|
124
|
+
const pageId = entity.pageId || createPageObjectEntityId(entity.frameKey);
|
|
125
|
+
const frameId = createFrameObjectEntityId(entity.frameKey);
|
|
126
|
+
entities[entity.id] = {
|
|
127
|
+
id: entity.id,
|
|
128
|
+
kind: entity.kind,
|
|
129
|
+
label: entity.label || entity.id,
|
|
130
|
+
parentId: entity.parentId || (entity.id === frameId ? pageId : frameId),
|
|
131
|
+
pageId,
|
|
132
|
+
blockId: entity.blockId,
|
|
133
|
+
frameKey: entity.frameKey,
|
|
134
|
+
chainId: entity.chainId,
|
|
135
|
+
source: entity.source,
|
|
136
|
+
metadata: entity.metadata,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return entities;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const OBJECT_OPEN_RE = /<([a-z][a-z0-9-]*)\b([^>]*)\bdata-openpress-object-id="([^"]+)"([^>]*)>/gi;
|
|
144
|
+
const ATTR_RE = (name) => new RegExp(`\\b${name}="([^"]*)"`);
|
|
145
|
+
|
|
146
|
+
function collectRenderedObjectEntities(frames) {
|
|
147
|
+
const entities = [];
|
|
148
|
+
for (const frame of frames) {
|
|
149
|
+
const pageId = createPageObjectEntityId(frame.frameKey);
|
|
150
|
+
const frameId = createFrameObjectEntityId(frame.frameKey);
|
|
151
|
+
const html = String(frame.html ?? "");
|
|
152
|
+
let match;
|
|
153
|
+
OBJECT_OPEN_RE.lastIndex = 0;
|
|
154
|
+
while ((match = OBJECT_OPEN_RE.exec(html)) !== null) {
|
|
155
|
+
const attrs = `${match[2] ?? ""} data-openpress-object-id="${match[3] ?? ""}" ${match[4] ?? ""}`;
|
|
156
|
+
const id = htmlDecode(match[3] ?? "");
|
|
157
|
+
const kind = htmlDecode(pickAttr(attrs, "data-openpress-object-kind")) || objectKindFromId(id);
|
|
158
|
+
if (!id || !kind) continue;
|
|
159
|
+
entities.push({
|
|
160
|
+
id,
|
|
161
|
+
kind,
|
|
162
|
+
label: htmlDecode(pickAttr(attrs, "data-openpress-object-label")) || id,
|
|
163
|
+
parentId: htmlDecode(pickAttr(attrs, "data-openpress-object-parent-id")) || (id === frameId ? pageId : frameId),
|
|
164
|
+
pageId: htmlDecode(pickAttr(attrs, "data-openpress-object-page-id")) || pageId,
|
|
165
|
+
blockId: htmlDecode(pickAttr(attrs, "data-openpress-block-id")) || undefined,
|
|
166
|
+
frameKey: htmlDecode(pickAttr(attrs, "data-openpress-object-frame-key")) || frame.frameKey,
|
|
167
|
+
chainId: htmlDecode(pickAttr(attrs, "data-openpress-object-chain-id")) || undefined,
|
|
168
|
+
source: parseJsonAttribute(pickAttr(attrs, "data-openpress-object-source")),
|
|
169
|
+
metadata: parseJsonAttribute(pickAttr(attrs, "data-openpress-object-metadata")),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return entities;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function pickAttr(attrs, name) {
|
|
177
|
+
const match = ATTR_RE(name).exec(attrs);
|
|
178
|
+
return match ? match[1] : "";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function objectKindFromId(id) {
|
|
182
|
+
const separator = id.indexOf(":");
|
|
183
|
+
return separator === -1 ? "" : id.slice(0, separator);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseJsonAttribute(value) {
|
|
187
|
+
const decoded = htmlDecode(value);
|
|
188
|
+
if (!decoded) return undefined;
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(decoded);
|
|
191
|
+
} catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function htmlDecode(value) {
|
|
197
|
+
return String(value ?? "")
|
|
198
|
+
.replaceAll(""", '"')
|
|
199
|
+
.replaceAll("'", "'")
|
|
200
|
+
.replaceAll("'", "'")
|
|
201
|
+
.replaceAll("<", "<")
|
|
202
|
+
.replaceAll(">", ">")
|
|
203
|
+
.replaceAll("&", "&");
|
|
204
|
+
}
|
|
@@ -7,7 +7,8 @@ import { singleColumnRegionStream } from "./regions.mjs";
|
|
|
7
7
|
// region until adding the next block would exceed capacity, then advance to
|
|
8
8
|
// the next region. Pages are a derived view (grouping by pageIndex), so the
|
|
9
9
|
// same code paginates single-column, multi-column, and heterogeneous layouts.
|
|
10
|
-
export function allocateBlocksToRegions(measuredBlocks, regionStream) {
|
|
10
|
+
export function allocateBlocksToRegions(measuredBlocks, regionStream, options = {}) {
|
|
11
|
+
const keepWithNext = typeof options.keepWithNext === "function" ? options.keepWithNext : null;
|
|
11
12
|
const filled = [];
|
|
12
13
|
const warnings = [];
|
|
13
14
|
let current = regionStream.next();
|
|
@@ -16,6 +17,7 @@ export function allocateBlocksToRegions(measuredBlocks, regionStream) {
|
|
|
16
17
|
}
|
|
17
18
|
let currentBlockIds = [];
|
|
18
19
|
let currentHeight = 0;
|
|
20
|
+
let consumedCount = 0;
|
|
19
21
|
|
|
20
22
|
const flush = () => {
|
|
21
23
|
if (currentBlockIds.length === 0) return;
|
|
@@ -29,7 +31,9 @@ export function allocateBlocksToRegions(measuredBlocks, regionStream) {
|
|
|
29
31
|
currentHeight = 0;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
const blocks = measuredBlocks ?? [];
|
|
35
|
+
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
|
|
36
|
+
const block = blocks[blockIndex];
|
|
33
37
|
const id = String(block?.id ?? "");
|
|
34
38
|
if (!id) continue;
|
|
35
39
|
const height = Math.max(0, Number(block.height) || 0);
|
|
@@ -45,6 +49,24 @@ export function allocateBlocksToRegions(measuredBlocks, regionStream) {
|
|
|
45
49
|
});
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
const nextBlock = blocks[blockIndex + 1];
|
|
53
|
+
const nextHeight = Math.max(0, Number(nextBlock?.height) || 0);
|
|
54
|
+
const keepWithNextHeight = keepWithNext?.(block, nextBlock) ? height + nextHeight : 0;
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
currentBlockIds.length > 0 &&
|
|
58
|
+
keepWithNextHeight > 0 &&
|
|
59
|
+
currentHeight + keepWithNextHeight > current.capacity
|
|
60
|
+
) {
|
|
61
|
+
flush();
|
|
62
|
+
const next = regionStream.next();
|
|
63
|
+
if (!next) {
|
|
64
|
+
warnings.push({ code: "out-of-regions", blockId: id });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
current = next;
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
if (currentBlockIds.length > 0 && currentHeight + height > current.capacity) {
|
|
49
71
|
flush();
|
|
50
72
|
const next = regionStream.next();
|
|
@@ -57,10 +79,17 @@ export function allocateBlocksToRegions(measuredBlocks, regionStream) {
|
|
|
57
79
|
|
|
58
80
|
currentBlockIds.push(id);
|
|
59
81
|
currentHeight += height;
|
|
82
|
+
consumedCount += 1;
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
flush();
|
|
63
|
-
return { regions: filled, warnings };
|
|
86
|
+
return { regions: filled, warnings, consumedCount };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function estimateRegionsNeeded(measuredBlocks, regionCapacity, options = {}) {
|
|
90
|
+
const capacity = positiveNumber(regionCapacity, DEFAULT_PAGE_SAFE_HEIGHT_PX);
|
|
91
|
+
const result = allocateBlocksToRegions(measuredBlocks, infiniteFixedCapacityRegionStream(capacity), options);
|
|
92
|
+
return result.regions.length;
|
|
64
93
|
}
|
|
65
94
|
|
|
66
95
|
// Derive a flat pages[] view from filled regions. Blocks within a page are
|
|
@@ -101,6 +130,22 @@ export function paginateMeasuredBlocks(measuredBlocks, options = {}) {
|
|
|
101
130
|
};
|
|
102
131
|
}
|
|
103
132
|
|
|
133
|
+
function infiniteFixedCapacityRegionStream(capacity) {
|
|
134
|
+
let index = 0;
|
|
135
|
+
return {
|
|
136
|
+
next() {
|
|
137
|
+
const region = {
|
|
138
|
+
id: `estimate-region-${index}`,
|
|
139
|
+
capacity,
|
|
140
|
+
pageIndex: index,
|
|
141
|
+
columnIndex: 0,
|
|
142
|
+
};
|
|
143
|
+
index += 1;
|
|
144
|
+
return region;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
104
149
|
// Translate the new region-shaped warnings back to the legacy
|
|
105
150
|
// `block-overflows-page` schema that document-export.mjs and downstream
|
|
106
151
|
// consumers expect. Once consumers migrate, this can drop.
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
// these helpers. The region kernel is also usable on its own for custom
|
|
6
6
|
// pipelines or unit tests.
|
|
7
7
|
|
|
8
|
-
export { paginateMeasuredBlocks, allocateBlocksToRegions, pagesFromRegions } from "./pagination/allocator.mjs";
|
|
8
|
+
export { paginateMeasuredBlocks, allocateBlocksToRegions, estimateRegionsNeeded, pagesFromRegions } from "./pagination/allocator.mjs";
|
|
9
9
|
export { singleColumnRegionStream, multiColumnRegionStream, fixedRegionStream } from "./pagination/regions.mjs";
|