@drawnagency/primitives 0.1.118 → 0.1.119
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/dist/{chunk-O37AV27P.js → chunk-5JYHTHVR.js} +28 -0
- package/dist/{chunk-W72BPGR3.js → chunk-BZKHHUIN.js} +44 -8
- package/dist/{chunk-CEFFP3CH.js → chunk-G6O3TYPJ.js} +297 -83
- package/dist/{chunk-4NSOKEA7.js → chunk-HFHM4MNK.js} +12 -7
- package/dist/{chunk-RIITGTV3.js → chunk-LQHBC7C6.js} +37 -2
- package/dist/{chunk-VY3IFGE3.js → chunk-QAMAIDXZ.js} +1 -1
- package/dist/{chunk-HRX3XJBU.js → chunk-RIN2QCYJ.js} +9 -11
- package/dist/{chunk-ZOTRHHUB.js → chunk-UNFPATDE.js} +8 -10
- package/dist/{chunk-ERLIL4RU.js → chunk-YKKGVSJH.js} +35 -37
- package/dist/{chunk-67P34WNW.js → chunk-ZKVQMIMJ.js} +3 -20
- package/dist/collab/rebase.js +4 -5
- package/dist/components/chat/ChatComposer.d.ts +0 -4
- package/dist/components/chat/ChatComposer.d.ts.map +1 -1
- package/dist/components/chat/ChatPanel.d.ts +2 -3
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/markdown.d.ts +2 -3
- package/dist/components/chat/markdown.d.ts.map +1 -1
- package/dist/components/editor/PagesModal.d.ts +0 -2
- package/dist/components/editor/PagesModal.d.ts.map +1 -1
- package/dist/components/editor/index.js +15 -19
- package/dist/components/primitives/index.js +11 -15
- package/dist/components/sections/brand-guide.js +10 -13
- package/dist/components/sections/register-schemas.js +16 -21
- package/dist/components/sections/register.js +16 -21
- package/dist/components/shell/AudienceChips.d.ts +1 -3
- package/dist/components/shell/AudienceChips.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +0 -34
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/hooks/useSortableNavRow.d.ts +1 -1
- package/dist/hooks/useSortableRow.d.ts +1 -1
- package/dist/index.js +83 -90
- package/dist/lib/index-ops.js +5 -6
- package/dist/lib/index.js +27 -32
- package/dist/lib/pages.d.ts +0 -2
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/lib/sidecars.js +2 -2
- package/dist/lib/site-validation.js +13 -14
- package/dist/lib/storage-tier.js +4 -4
- package/dist/schemas/index.js +42 -45
- package/dist/schemas/site-config.d.ts +1 -277
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +2 -42
- package/src/components/chat/ChatComposer.tsx +0 -4
- package/src/components/chat/ChatPanel.tsx +2 -3
- package/src/components/chat/markdown.ts +2 -3
- package/src/components/editor/PagesModal.tsx +1 -15
- package/src/components/editor/StatusIndicator.tsx +4 -4
- package/src/components/shell/AudienceChips.tsx +1 -3
- package/src/components/shell/EditorShell.tsx +5 -113
- package/src/components/shell/SiteSettingsModal.tsx +0 -11
- package/src/lib/pages.ts +0 -22
- package/src/schemas/index.ts +0 -4
- package/src/schemas/site-config.ts +4 -32
- package/dist/chunk-4TZJZU2T.js +0 -92
- package/dist/chunk-DIMDRBFK.js +0 -11
- package/dist/chunk-FV7JQRCN.js +0 -94
- package/dist/chunk-IIDKW7AV.js +0 -41
- package/dist/chunk-JCDZAFX2.js +0 -224
- package/dist/chunk-MM7KZS5C.js +0 -30
- package/dist/chunk-QQ3S3P5P.js +0 -38
- package/dist/chunk-TEOAKGZA.js +0 -784
- package/dist/chunk-VAYZJKVR.js +0 -209
- package/dist/components/shell/DocInspector.d.ts +0 -18
- package/dist/components/shell/DocInspector.d.ts.map +0 -1
- package/dist/components/shell/DocumentEditorOverlay.d.ts +0 -29
- package/dist/components/shell/DocumentEditorOverlay.d.ts.map +0 -1
- package/dist/components/shell/DocumentPresentOverlay.d.ts +0 -30
- package/dist/components/shell/DocumentPresentOverlay.d.ts.map +0 -1
- package/dist/components/shell/SiteSettingsDecks.d.ts +0 -13
- package/dist/components/shell/SiteSettingsDecks.d.ts.map +0 -1
- package/dist/deck/DeckComponentView.d.ts +0 -13
- package/dist/deck/DeckComponentView.d.ts.map +0 -1
- package/dist/deck/DeckPlayer.d.ts +0 -26
- package/dist/deck/DeckPlayer.d.ts.map +0 -1
- package/dist/deck/DeckSlideView.d.ts +0 -17
- package/dist/deck/DeckSlideView.d.ts.map +0 -1
- package/dist/deck/DeckStage.d.ts +0 -16
- package/dist/deck/DeckStage.d.ts.map +0 -1
- package/dist/deck/components/DeckImage.d.ts +0 -14
- package/dist/deck/components/DeckImage.d.ts.map +0 -1
- package/dist/deck/components/DeckImageGrid.d.ts +0 -27
- package/dist/deck/components/DeckImageGrid.d.ts.map +0 -1
- package/dist/deck/components/DeckRichText.d.ts +0 -22
- package/dist/deck/components/DeckRichText.d.ts.map +0 -1
- package/dist/deck/components/DeckVideo.d.ts +0 -15
- package/dist/deck/components/DeckVideo.d.ts.map +0 -1
- package/dist/deck/editing.d.ts +0 -50
- package/dist/deck/editing.d.ts.map +0 -1
- package/dist/deck/flip.d.ts +0 -11
- package/dist/deck/flip.d.ts.map +0 -1
- package/dist/deck/index.d.ts +0 -24
- package/dist/deck/index.d.ts.map +0 -1
- package/dist/deck/index.js +0 -51
- package/dist/deck/layouts/CenteredTextLayout.d.ts +0 -12
- package/dist/deck/layouts/CenteredTextLayout.d.ts.map +0 -1
- package/dist/deck/layouts/ColumnSplitLayout.d.ts +0 -14
- package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +0 -1
- package/dist/deck/layouts/FullBleedImageLayout.d.ts +0 -14
- package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +0 -1
- package/dist/deck/media.d.ts +0 -33
- package/dist/deck/media.d.ts.map +0 -1
- package/dist/deck/tokens.d.ts +0 -22
- package/dist/deck/tokens.d.ts.map +0 -1
- package/dist/deck/useStageScale.d.ts +0 -10
- package/dist/deck/useStageScale.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderApp.d.ts +0 -30
- package/dist/deck-builder/DeckBuilderApp.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderTopBar.d.ts +0 -24
- package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +0 -1
- package/dist/deck-builder/DeckChatPanel.d.ts +0 -45
- package/dist/deck-builder/DeckChatPanel.d.ts.map +0 -1
- package/dist/deck-builder/DeckEditor.d.ts +0 -37
- package/dist/deck-builder/DeckEditor.d.ts.map +0 -1
- package/dist/deck-builder/DeckLibrary.d.ts +0 -22
- package/dist/deck-builder/DeckLibrary.d.ts.map +0 -1
- package/dist/deck-builder/DeckSlideSurface.d.ts +0 -31
- package/dist/deck-builder/DeckSlideSurface.d.ts.map +0 -1
- package/dist/deck-builder/deck-api.d.ts +0 -76
- package/dist/deck-builder/deck-api.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-client.d.ts +0 -44
- package/dist/deck-builder/deck-chat-client.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-history.d.ts +0 -10
- package/dist/deck-builder/deck-chat-history.d.ts.map +0 -1
- package/dist/deck-builder/index.d.ts +0 -13
- package/dist/deck-builder/index.d.ts.map +0 -1
- package/dist/deck-builder/index.js +0 -1498
- package/dist/deck-builder/starter-deck.d.ts +0 -23
- package/dist/deck-builder/starter-deck.d.ts.map +0 -1
- package/dist/deck-builder/types.d.ts +0 -29
- package/dist/deck-builder/types.d.ts.map +0 -1
- package/dist/deck-builder/useSortableSlide.d.ts +0 -29
- package/dist/deck-builder/useSortableSlide.d.ts.map +0 -1
- package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/dist/document/components/DocCoverPage.d.ts +0 -11
- package/dist/document/components/DocCoverPage.d.ts.map +0 -1
- package/dist/document/components/DocPage.d.ts +0 -10
- package/dist/document/components/DocPage.d.ts.map +0 -1
- package/dist/document/components/DocRail.d.ts +0 -28
- package/dist/document/components/DocRail.d.ts.map +0 -1
- package/dist/document/components/DocSectionPage.d.ts +0 -12
- package/dist/document/components/DocSectionPage.d.ts.map +0 -1
- package/dist/document/components/DocViewer.d.ts +0 -21
- package/dist/document/components/DocViewer.d.ts.map +0 -1
- package/dist/document/components/Wordmark.d.ts +0 -4
- package/dist/document/components/Wordmark.d.ts.map +0 -1
- package/dist/document/components/measure.d.ts +0 -7
- package/dist/document/components/measure.d.ts.map +0 -1
- package/dist/document/components/useDocPages.d.ts +0 -12
- package/dist/document/components/useDocPages.d.ts.map +0 -1
- package/dist/document/engine/blocks.d.ts +0 -2
- package/dist/document/engine/blocks.d.ts.map +0 -1
- package/dist/document/engine/harvest.d.ts +0 -3
- package/dist/document/engine/harvest.d.ts.map +0 -1
- package/dist/document/engine/masonry.d.ts +0 -26
- package/dist/document/engine/masonry.d.ts.map +0 -1
- package/dist/document/engine/paginate.d.ts +0 -43
- package/dist/document/engine/paginate.d.ts.map +0 -1
- package/dist/document/engine/settings.d.ts +0 -91
- package/dist/document/engine/settings.d.ts.map +0 -1
- package/dist/document/engine/types.d.ts +0 -44
- package/dist/document/engine/types.d.ts.map +0 -1
- package/dist/document/index.d.ts +0 -13
- package/dist/document/index.d.ts.map +0 -1
- package/dist/document/index.js +0 -1030
- package/dist/document/styles/document.css +0 -217
- package/dist/lib/deck-access.d.ts +0 -86
- package/dist/lib/deck-access.d.ts.map +0 -1
- package/dist/lib/deck-access.js +0 -58
- package/dist/lib/deck-dexie.d.ts +0 -88
- package/dist/lib/deck-dexie.d.ts.map +0 -1
- package/dist/lib/deck-dexie.js +0 -22
- package/dist/lib/deck-edit.d.ts +0 -67
- package/dist/lib/deck-edit.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.d.ts +0 -27
- package/dist/lib/deck-media-assets.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.js +0 -25
- package/dist/lib/deck-media-refs.d.ts +0 -35
- package/dist/lib/deck-media-refs.d.ts.map +0 -1
- package/dist/lib/deck-media-refs.js +0 -36
- package/dist/lib/deck-tool.d.ts +0 -25
- package/dist/lib/deck-tool.d.ts.map +0 -1
- package/dist/lib/deck-tool.js +0 -58
- package/dist/lib/document-input.d.ts +0 -25
- package/dist/lib/document-input.d.ts.map +0 -1
- package/dist/schemas/deck-meta.d.ts +0 -85
- package/dist/schemas/deck-meta.d.ts.map +0 -1
- package/dist/schemas/deck-meta.js +0 -64
- package/dist/schemas/deck.d.ts +0 -954
- package/dist/schemas/deck.d.ts.map +0 -1
- package/dist/schemas/deck.js +0 -65
- package/src/components/shell/DocInspector.tsx +0 -314
- package/src/components/shell/DocumentEditorOverlay.tsx +0 -179
- package/src/components/shell/DocumentPresentOverlay.tsx +0 -87
- package/src/components/shell/SiteSettingsDecks.tsx +0 -41
- package/src/deck/DeckComponentView.tsx +0 -28
- package/src/deck/DeckPlayer.tsx +0 -166
- package/src/deck/DeckSlideView.tsx +0 -38
- package/src/deck/DeckStage.tsx +0 -39
- package/src/deck/components/DeckImage.tsx +0 -38
- package/src/deck/components/DeckImageGrid.tsx +0 -254
- package/src/deck/components/DeckRichText.tsx +0 -111
- package/src/deck/components/DeckVideo.tsx +0 -81
- package/src/deck/editing.tsx +0 -201
- package/src/deck/flip.ts +0 -70
- package/src/deck/index.ts +0 -30
- package/src/deck/layouts/CenteredTextLayout.tsx +0 -29
- package/src/deck/layouts/ColumnSplitLayout.tsx +0 -69
- package/src/deck/layouts/FullBleedImageLayout.tsx +0 -58
- package/src/deck/media.tsx +0 -46
- package/src/deck/tokens.ts +0 -57
- package/src/deck/useStageScale.ts +0 -44
- package/src/deck-builder/DeckBuilderApp.tsx +0 -572
- package/src/deck-builder/DeckBuilderTopBar.tsx +0 -62
- package/src/deck-builder/DeckChatPanel.tsx +0 -334
- package/src/deck-builder/DeckEditor.tsx +0 -258
- package/src/deck-builder/DeckLibrary.tsx +0 -195
- package/src/deck-builder/DeckSlideSurface.tsx +0 -55
- package/src/deck-builder/deck-api.ts +0 -177
- package/src/deck-builder/deck-chat-client.ts +0 -129
- package/src/deck-builder/deck-chat-history.ts +0 -62
- package/src/deck-builder/index.ts +0 -12
- package/src/deck-builder/starter-deck.ts +0 -86
- package/src/deck-builder/types.ts +0 -24
- package/src/deck-builder/useSortableSlide.ts +0 -102
- package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/src/document/components/DocCoverPage.tsx +0 -45
- package/src/document/components/DocPage.tsx +0 -40
- package/src/document/components/DocRail.tsx +0 -189
- package/src/document/components/DocSectionPage.tsx +0 -48
- package/src/document/components/DocViewer.tsx +0 -313
- package/src/document/components/Wordmark.tsx +0 -18
- package/src/document/components/measure.tsx +0 -133
- package/src/document/components/useDocPages.ts +0 -95
- package/src/document/engine/blocks.ts +0 -116
- package/src/document/engine/harvest.ts +0 -95
- package/src/document/engine/masonry.ts +0 -158
- package/src/document/engine/paginate.ts +0 -232
- package/src/document/engine/settings.ts +0 -125
- package/src/document/engine/types.ts +0 -64
- package/src/document/index.ts +0 -18
- package/src/document/styles/document.css +0 -217
- package/src/lib/deck-access.ts +0 -132
- package/src/lib/deck-dexie.ts +0 -190
- package/src/lib/deck-edit.ts +0 -196
- package/src/lib/deck-media-assets.ts +0 -57
- package/src/lib/deck-media-refs.ts +0 -67
- package/src/lib/deck-tool.ts +0 -95
- package/src/lib/document-input.ts +0 -57
- package/src/schemas/deck-meta.ts +0 -88
- package/src/schemas/deck.ts +0 -478
- package/dist/{chunk-DJABADOO.js → chunk-AJ2QOAZI.js} +3 -3
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// Elements that are never allowed to survive into rendered output, wherever
|
|
2
|
-
// they occur (top-level or nested).
|
|
3
|
-
const DISALLOWED_TAGS = new Set([
|
|
4
|
-
"script",
|
|
5
|
-
"style",
|
|
6
|
-
"iframe",
|
|
7
|
-
"object",
|
|
8
|
-
"embed",
|
|
9
|
-
"link",
|
|
10
|
-
"meta",
|
|
11
|
-
"base",
|
|
12
|
-
"form",
|
|
13
|
-
// Raw-text / escapable-raw-text and declarative elements whose contents are
|
|
14
|
-
// not parsed as normal markup. Left in place they enable serialize/reparse
|
|
15
|
-
// (mXSS) bypasses — e.g. an <img onerror> smuggled inside <noscript> text.
|
|
16
|
-
// Removing the element drops its contents too (including <template> content).
|
|
17
|
-
"noscript",
|
|
18
|
-
"xmp",
|
|
19
|
-
"noembed",
|
|
20
|
-
"noframes",
|
|
21
|
-
"plaintext",
|
|
22
|
-
"template",
|
|
23
|
-
]);
|
|
24
|
-
const DISALLOWED_SELECTOR = Array.from(DISALLOWED_TAGS).join(",");
|
|
25
|
-
|
|
26
|
-
// Attributes that can carry a URL and therefore need scheme scrutiny. `poster`
|
|
27
|
-
// and `srcset` matter because <video>/<img>/<source> survive the scrub; the
|
|
28
|
-
// rest belong to elements DISALLOWED_TAGS already removes, and are listed so a
|
|
29
|
-
// future relaxation of that set doesn't quietly open a hole.
|
|
30
|
-
const URL_ATTRS = new Set([
|
|
31
|
-
"href",
|
|
32
|
-
"src",
|
|
33
|
-
"srcset",
|
|
34
|
-
"poster",
|
|
35
|
-
"xlink:href",
|
|
36
|
-
"formaction",
|
|
37
|
-
"action",
|
|
38
|
-
"ping",
|
|
39
|
-
"data",
|
|
40
|
-
"background",
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
function isDangerousUrl(attrName: string, rawValue: string): boolean {
|
|
44
|
-
// Browsers strip ASCII tab/CR/LF anywhere in a URL (and ignore leading
|
|
45
|
-
// controls) before resolving its scheme, so "jav\tascript:" runs like
|
|
46
|
-
// "javascript:". Over-stripping here only makes the check more conservative.
|
|
47
|
-
const value = rawValue.toLowerCase().replace(/[\u0000-\u0020]/g, "");
|
|
48
|
-
if (value.startsWith("javascript:") || value.startsWith("vbscript:")) return true;
|
|
49
|
-
if (attrName === "src" && value.startsWith("data:text/html")) return true;
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// srcset is a comma-separated candidate list, so a startsWith check over the
|
|
54
|
-
// whole value misses "a.png 1x, javascript:alert(1) 2x". Scrutinize each
|
|
55
|
-
// candidate's URL part instead.
|
|
56
|
-
function hasDangerousUrl(attrName: string, rawValue: string): boolean {
|
|
57
|
-
if (attrName !== "srcset") return isDangerousUrl(attrName, rawValue);
|
|
58
|
-
return rawValue
|
|
59
|
-
.split(",")
|
|
60
|
-
.some((candidate) => isDangerousUrl("src", candidate.trim().split(/\s+/)[0] ?? ""));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Defense-in-depth scrub applied at the dangerouslySetInnerHTML/innerHTML
|
|
64
|
-
// boundary. Upstream portal content is sanitized at authoring time, but
|
|
65
|
-
// /print runs in an authed headless browser, so this engine (kept
|
|
66
|
-
// dependency-free — no DOMPurify) removes known-dangerous elements and
|
|
67
|
-
// attributes from the DOM it already parsed, in place.
|
|
68
|
-
function scrubElement(root: Element): void {
|
|
69
|
-
root.querySelectorAll(DISALLOWED_SELECTOR).forEach((el) => el.remove());
|
|
70
|
-
|
|
71
|
-
const elements: Element[] = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
72
|
-
for (const el of elements) {
|
|
73
|
-
for (const attr of Array.from(el.attributes)) {
|
|
74
|
-
const name = attr.name.toLowerCase();
|
|
75
|
-
if (name.startsWith("on") || name === "srcdoc") {
|
|
76
|
-
el.removeAttribute(attr.name);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (URL_ATTRS.has(name) && hasDangerousUrl(name, attr.value)) {
|
|
80
|
-
el.removeAttribute(attr.name);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Elements that carry meaning with no text of their own. The keep test used to
|
|
87
|
-
// name <img> alone, so a rule, a video, an inline SVG diagram or a <picture>
|
|
88
|
-
// was silently dropped from the printed document.
|
|
89
|
-
const VISIBLE_EMPTY_SELECTOR = "img, hr, video, svg, picture, canvas";
|
|
90
|
-
|
|
91
|
-
// Split sanitized rich-text HTML into atomic top-level blocks. Pagination
|
|
92
|
-
// never splits inside a block — a block that doesn't fit moves wholesale to
|
|
93
|
-
// the next page (spec: "pagination by block measurement").
|
|
94
|
-
export function splitProseBlocks(html: string): string[] {
|
|
95
|
-
if (!html.trim()) return [];
|
|
96
|
-
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
97
|
-
const blocks: string[] = [];
|
|
98
|
-
for (const node of Array.from(doc.body.childNodes)) {
|
|
99
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
100
|
-
const el = node as Element;
|
|
101
|
-
if (DISALLOWED_TAGS.has(el.tagName.toLowerCase())) continue;
|
|
102
|
-
scrubElement(el);
|
|
103
|
-
if ((el.textContent ?? "").trim() || el.matches(VISIBLE_EMPTY_SELECTOR) || el.querySelector(VISIBLE_EMPTY_SELECTOR)) {
|
|
104
|
-
blocks.push(el.outerHTML);
|
|
105
|
-
}
|
|
106
|
-
} else if (node.nodeType === Node.TEXT_NODE) {
|
|
107
|
-
const text = (node.textContent ?? "").trim();
|
|
108
|
-
if (text) {
|
|
109
|
-
const p = doc.createElement("p");
|
|
110
|
-
p.textContent = text;
|
|
111
|
-
blocks.push(p.outerHTML);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return blocks;
|
|
116
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { splitProseBlocks } from "./blocks";
|
|
2
|
-
import { PREAMBLE_ID, type Chunk, type DocImage, type DocSectionInput, type SplitAt } from "./types";
|
|
3
|
-
|
|
4
|
-
// Shape of the fields harvest reads, checked structurally at runtime — the
|
|
5
|
-
// engine must tolerate any future portal section without crashing (allowlist).
|
|
6
|
-
type Loose = Record<string, unknown>;
|
|
7
|
-
|
|
8
|
-
function str(v: unknown): string {
|
|
9
|
-
return typeof v === "string" ? v : "";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Normalizes any runtime value (including null/undefined from malformed
|
|
13
|
-
// portal data) into a safe-to-index object so field access never throws.
|
|
14
|
-
function asRecord(v: unknown): Loose {
|
|
15
|
-
return v && typeof v === "object" ? (v as Loose) : {};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// One section's contribution to the current chunk (containers recurse).
|
|
19
|
-
function harvestInto(
|
|
20
|
-
target: Chunk,
|
|
21
|
-
type: string,
|
|
22
|
-
content: unknown,
|
|
23
|
-
imagesById: Record<string, DocImage>,
|
|
24
|
-
): void {
|
|
25
|
-
const c = asRecord(content);
|
|
26
|
-
switch (type) {
|
|
27
|
-
case "sub_heading":
|
|
28
|
-
case "link_heading": // nested (inside a container) — not a boundary here
|
|
29
|
-
target.textBlocks.push({ kind: "heading", level: 2, text: str(c.heading) });
|
|
30
|
-
break;
|
|
31
|
-
case "sub_sub_heading":
|
|
32
|
-
target.textBlocks.push({ kind: "heading", level: 3, text: str(c.heading) });
|
|
33
|
-
break;
|
|
34
|
-
case "prose":
|
|
35
|
-
for (const html of splitProseBlocks(str(c.body))) {
|
|
36
|
-
target.textBlocks.push({ kind: "prose", html });
|
|
37
|
-
}
|
|
38
|
-
break;
|
|
39
|
-
case "media": {
|
|
40
|
-
const ref = asRecord(c.ref);
|
|
41
|
-
const image = imagesById[str(ref.imageId)];
|
|
42
|
-
if (image) target.images.push(image);
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
case "container": {
|
|
46
|
-
const children = Array.isArray(c.children) ? c.children : [];
|
|
47
|
-
for (const child of children) {
|
|
48
|
-
const cc = asRecord(child);
|
|
49
|
-
if (typeof cc.type === "string") {
|
|
50
|
-
harvestInto(target, cc.type, cc.content, imagesById);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
default:
|
|
56
|
-
break; // allowlist: video, button, spacer, colors, do/dont, icon list, unknown future types
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function harvest(
|
|
61
|
-
sections: DocSectionInput[],
|
|
62
|
-
pageTitle: string,
|
|
63
|
-
imagesById: Record<string, DocImage>,
|
|
64
|
-
splitAtFor: (chunkId: string) => SplitAt = () => "none",
|
|
65
|
-
): Chunk[] {
|
|
66
|
-
const chunks: Chunk[] = [];
|
|
67
|
-
const preamble: Chunk = { id: PREAMBLE_ID, title: pageTitle, textBlocks: [], images: [] };
|
|
68
|
-
let parent = preamble; // current top-level chunk — owns the splitAt setting
|
|
69
|
-
let current = preamble; // chunk receiving harvested content
|
|
70
|
-
let splitAt = splitAtFor(PREAMBLE_ID);
|
|
71
|
-
|
|
72
|
-
for (const section of sections) {
|
|
73
|
-
const heading = str(asRecord(section.content).heading);
|
|
74
|
-
if (section.type === "link_heading") {
|
|
75
|
-
parent = { id: section.id, title: heading, textBlocks: [], images: [] };
|
|
76
|
-
current = parent;
|
|
77
|
-
splitAt = splitAtFor(section.id);
|
|
78
|
-
chunks.push(parent);
|
|
79
|
-
} else if (section.type === "sub_heading" && splitAt === "sub_heading") {
|
|
80
|
-
// Promotion: the boundary heading becomes the chunk title, not a TextBlock.
|
|
81
|
-
current = { id: section.id, title: heading, textBlocks: [], images: [], parentId: parent.id, kicker: parent.title, depth: 1 };
|
|
82
|
-
chunks.push(current);
|
|
83
|
-
} else {
|
|
84
|
-
harvestInto(current, section.type, section.content, imagesById);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Kept when it has own content OR spawned sub-chunks — an emptied-but-split
|
|
89
|
-
// preamble must stay selectable (its ghost is the way back to splitAt).
|
|
90
|
-
const keepPreamble =
|
|
91
|
-
preamble.textBlocks.length > 0 ||
|
|
92
|
-
preamble.images.length > 0 ||
|
|
93
|
-
chunks.some((c) => c.parentId === PREAMBLE_ID);
|
|
94
|
-
return keepPreamble ? [preamble, ...chunks] : chunks;
|
|
95
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
const EPS = 1e-6;
|
|
2
|
-
|
|
3
|
-
export type MasonryMode = "rows" | "columns";
|
|
4
|
-
|
|
5
|
-
export type Sized = { width: number; height: number };
|
|
6
|
-
|
|
7
|
-
export interface MasonryOpts {
|
|
8
|
-
mode: MasonryMode;
|
|
9
|
-
columns: 2 | 3 | 4 | 5 | 6;
|
|
10
|
-
bodyWIn: number;
|
|
11
|
-
bodyHIn: number;
|
|
12
|
-
gapIn: number;
|
|
13
|
-
targetRowHeightIn: number;
|
|
14
|
-
rows?: "auto" | 1 | 2 | 3 | 4;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface MasonryBox<T extends Sized = Sized> {
|
|
18
|
-
asset: T;
|
|
19
|
-
xIn: number;
|
|
20
|
-
yIn: number;
|
|
21
|
-
wIn: number;
|
|
22
|
-
hIn: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// A "cover" photo is laid out in a SQUARE cell (effective aspect 1) and the
|
|
26
|
-
// image crops to fill it; "contain" (default) uses the photo's natural aspect.
|
|
27
|
-
const effectiveAspect = (im: Sized, fit: "cover" | "contain" | undefined) => {
|
|
28
|
-
if (fit === "cover") return 1;
|
|
29
|
-
const aspect = im.width / im.height;
|
|
30
|
-
// A 0-height item (a legacy manifest row from before the decode guard) made
|
|
31
|
-
// this Infinity or NaN, and every downstream width went with it — the whole
|
|
32
|
-
// page laid out blank. Square is the same fallback the cover path uses.
|
|
33
|
-
return Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function layoutRows<T extends Sized>(images: T[], o: MasonryOpts, fits: ("cover" | "contain")[]): { boxes: MasonryBox<T>[]; used: number; endY: number } {
|
|
37
|
-
const asp = images.map((im, i) => effectiveAspect(im, fits[i]));
|
|
38
|
-
const boxes: MasonryBox<T>[] = [];
|
|
39
|
-
let y = 0, i = 0;
|
|
40
|
-
let used: number | null = null;
|
|
41
|
-
while (i < images.length) {
|
|
42
|
-
const row: number[] = []; // indices in this row
|
|
43
|
-
let natW = 0;
|
|
44
|
-
while (i + row.length < images.length) {
|
|
45
|
-
row.push(i + row.length);
|
|
46
|
-
const gaps = o.gapIn * (row.length - 1);
|
|
47
|
-
natW = row.reduce((s, idx) => s + asp[idx] * o.targetRowHeightIn, 0) + gaps;
|
|
48
|
-
if (natW >= o.bodyWIn) break;
|
|
49
|
-
}
|
|
50
|
-
const gaps = o.gapIn * (row.length - 1);
|
|
51
|
-
const sumAspect = row.reduce((s, idx) => s + asp[idx], 0);
|
|
52
|
-
const isFull = natW >= o.bodyWIn;
|
|
53
|
-
const rowH = isFull ? (o.bodyWIn - gaps) / sumAspect : o.targetRowHeightIn;
|
|
54
|
-
if (used === null && y + rowH > o.bodyHIn + EPS && boxes.length > 0) used = boxes.length;
|
|
55
|
-
let x = 0;
|
|
56
|
-
for (const idx of row) {
|
|
57
|
-
const w = asp[idx] * rowH;
|
|
58
|
-
boxes.push({ asset: images[idx], xIn: x, yIn: y, wIn: w, hIn: rowH });
|
|
59
|
-
x += w + o.gapIn;
|
|
60
|
-
}
|
|
61
|
-
y += rowH + o.gapIn;
|
|
62
|
-
i += row.length;
|
|
63
|
-
}
|
|
64
|
-
return { boxes, used: used ?? images.length, endY: y };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Forced row count: partition the images into exactly N rows (fewer only if
|
|
68
|
-
// there are fewer images), aspect-balanced, each row scaled to full width —
|
|
69
|
-
// unless the stack would exceed the region height, in which case the whole
|
|
70
|
-
// block scales down uniformly to fit (never up), left-aligned. Unlike
|
|
71
|
-
// layoutRows there is no target height — the row's share of the total aspect
|
|
72
|
-
// determines its height.
|
|
73
|
-
function layoutForcedRows<T extends Sized>(
|
|
74
|
-
images: T[],
|
|
75
|
-
o: MasonryOpts,
|
|
76
|
-
fits: ("cover" | "contain")[],
|
|
77
|
-
n: number,
|
|
78
|
-
): { boxes: MasonryBox<T>[]; used: number; endY: number } {
|
|
79
|
-
const asp = images.map((im, i) => effectiveAspect(im, fits[i]));
|
|
80
|
-
const count = Math.min(n, images.length);
|
|
81
|
-
// Greedy balance: each row takes images until it reaches the average
|
|
82
|
-
// aspect-sum of what's left, always leaving >=1 image per remaining row.
|
|
83
|
-
const rows: number[][] = [];
|
|
84
|
-
let i = 0;
|
|
85
|
-
for (let r = 0; r < count; r++) {
|
|
86
|
-
const remainingRows = count - r;
|
|
87
|
-
const target = asp.slice(i).reduce((s, a) => s + a, 0) / remainingRows;
|
|
88
|
-
const row: number[] = [];
|
|
89
|
-
let sum = 0;
|
|
90
|
-
while (i < asp.length) {
|
|
91
|
-
if (row.length > 0) {
|
|
92
|
-
if (asp.length - (i + 1) < remainingRows - 1) break; // taking i must leave >=1 image per remaining row
|
|
93
|
-
if (sum >= target - EPS) break;
|
|
94
|
-
}
|
|
95
|
-
row.push(i);
|
|
96
|
-
sum += asp[i];
|
|
97
|
-
i++;
|
|
98
|
-
}
|
|
99
|
-
rows.push(row);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Pass 1: full-width row heights from the aspect math.
|
|
103
|
-
const rowMeta = rows.map((row) => {
|
|
104
|
-
const gaps = o.gapIn * (row.length - 1);
|
|
105
|
-
const sumAspect = row.reduce((s, a) => s + asp[a], 0);
|
|
106
|
-
return { row, gaps, sumAspect, hIn: (o.bodyWIn - gaps) / sumAspect };
|
|
107
|
-
});
|
|
108
|
-
// Pass 2: if the stack exceeds the region, scale everything down uniformly —
|
|
109
|
-
// natural aspects kept, gaps fixed, block left-aligned. Never upscale.
|
|
110
|
-
const rowGaps = o.gapIn * (rowMeta.length - 1);
|
|
111
|
-
const totalH = rowMeta.reduce((s, m) => s + m.hIn, 0);
|
|
112
|
-
const scale = totalH > 0 ? Math.min(1, Math.max(0, o.bodyHIn - rowGaps) / totalH) : 1;
|
|
113
|
-
|
|
114
|
-
const boxes: MasonryBox<T>[] = [];
|
|
115
|
-
let y = 0;
|
|
116
|
-
for (const m of rowMeta) {
|
|
117
|
-
const rowH = m.hIn * scale;
|
|
118
|
-
let x = 0;
|
|
119
|
-
for (const idx of m.row) {
|
|
120
|
-
const w = asp[idx] * rowH;
|
|
121
|
-
boxes.push({ asset: images[idx], xIn: x, yIn: y, wIn: w, hIn: rowH });
|
|
122
|
-
x += w + o.gapIn;
|
|
123
|
-
}
|
|
124
|
-
y += rowH + o.gapIn;
|
|
125
|
-
}
|
|
126
|
-
return { boxes, used: images.length, endY: y };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function layoutColumns<T extends Sized>(images: T[], o: MasonryOpts, fits: ("cover" | "contain")[]): { boxes: MasonryBox<T>[]; used: number; colHeights: number[]; colW: number } {
|
|
130
|
-
const n = o.columns;
|
|
131
|
-
const colW = (o.bodyWIn - o.gapIn * (n - 1)) / n;
|
|
132
|
-
const colH = new Array(n).fill(0);
|
|
133
|
-
const boxes: MasonryBox<T>[] = [];
|
|
134
|
-
let used: number | null = null;
|
|
135
|
-
images.forEach((im, idx) => {
|
|
136
|
-
let c = 0;
|
|
137
|
-
for (let k = 1; k < n; k++) if (colH[k] < colH[c]) c = k;
|
|
138
|
-
const y = colH[c];
|
|
139
|
-
const h = colW / effectiveAspect(im, fits[idx]);
|
|
140
|
-
if (used === null && y + h > o.bodyHIn + EPS && idx > 0) used = idx;
|
|
141
|
-
boxes.push({ asset: im, xIn: c * (colW + o.gapIn), yIn: y, wIn: colW, hIn: h });
|
|
142
|
-
colH[c] = y + h + o.gapIn;
|
|
143
|
-
});
|
|
144
|
-
return { boxes, used: used ?? images.length, colHeights: colH, colW };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function layoutMasonryPage<T extends Sized>(
|
|
148
|
-
images: T[],
|
|
149
|
-
o: MasonryOpts,
|
|
150
|
-
fits: ("cover" | "contain")[] = [],
|
|
151
|
-
): { boxes: MasonryBox<T>[]; used: number } {
|
|
152
|
-
if (o.mode === "columns") {
|
|
153
|
-
const r = layoutColumns(images, o, fits);
|
|
154
|
-
return { boxes: r.boxes, used: r.used };
|
|
155
|
-
}
|
|
156
|
-
const r = typeof o.rows === "number" ? layoutForcedRows(images, o, fits, o.rows) : layoutRows(images, o, fits);
|
|
157
|
-
return { boxes: r.boxes, used: r.used };
|
|
158
|
-
}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { layoutMasonryPage } from "./masonry";
|
|
2
|
-
import type { SectionSettings } from "./settings";
|
|
3
|
-
import type { Chunk, DocImage, TextBlock } from "./types";
|
|
4
|
-
|
|
5
|
-
export interface DocGeom {
|
|
6
|
-
bodyWIn: number;
|
|
7
|
-
bodyHIn: number;
|
|
8
|
-
titleBandIn: number;
|
|
9
|
-
gapIn: number;
|
|
10
|
-
leftColWIn: number;
|
|
11
|
-
colGutterIn: number;
|
|
12
|
-
minTextColIn: number;
|
|
13
|
-
minImageRegionIn: number;
|
|
14
|
-
masonryTargetRowIn: number;
|
|
15
|
-
topTextMaxFraction: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Mirrors .deck-body in deck.css (17×11 page, top 1.35in, sides/bottom 0.6in).
|
|
19
|
-
export const DOC_GEOM: DocGeom = {
|
|
20
|
-
bodyWIn: 15.8,
|
|
21
|
-
bodyHIn: 9.05,
|
|
22
|
-
titleBandIn: 0.9,
|
|
23
|
-
gapIn: 0.15,
|
|
24
|
-
leftColWIn: 4.6,
|
|
25
|
-
colGutterIn: 0.4,
|
|
26
|
-
minTextColIn: 1,
|
|
27
|
-
minImageRegionIn: 1.2,
|
|
28
|
-
masonryTargetRowIn: 2.6,
|
|
29
|
-
// Top text fills at most this fraction of the body height below the title
|
|
30
|
-
// band, so a text-heavy top section always leaves ≥ half the page for images.
|
|
31
|
-
topTextMaxFraction: 0.5,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const EPS = 1e-6;
|
|
35
|
-
|
|
36
|
-
export interface Region { xIn: number; yIn: number; wIn: number; hIn: number; }
|
|
37
|
-
export interface ImageBox {
|
|
38
|
-
image: DocImage;
|
|
39
|
-
xIn: number; yIn: number; wIn: number; hIn: number;
|
|
40
|
-
fit: "cover" | "contain";
|
|
41
|
-
}
|
|
42
|
-
export interface DerivedPage {
|
|
43
|
-
id: string;
|
|
44
|
-
chunkId: string;
|
|
45
|
-
title: string;
|
|
46
|
-
cont: boolean;
|
|
47
|
-
// Sub-chunk context (absent on top-level pages) — see Chunk.
|
|
48
|
-
kicker?: string;
|
|
49
|
-
depth?: 1;
|
|
50
|
-
textBlocks: TextBlock[];
|
|
51
|
-
textRegion: Region | null;
|
|
52
|
-
imageBoxes: ImageBox[];
|
|
53
|
-
warnings: string[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function fitImages(
|
|
57
|
-
images: DocImage[],
|
|
58
|
-
layout: SectionSettings["imageLayout"],
|
|
59
|
-
region: Region,
|
|
60
|
-
geom: DocGeom,
|
|
61
|
-
): { boxes: ImageBox[]; used: number } {
|
|
62
|
-
if (images.length === 0 || region.hIn < geom.minImageRegionIn) return { boxes: [], used: 0 };
|
|
63
|
-
|
|
64
|
-
if (layout.mode === "grid") {
|
|
65
|
-
const used = Math.min(layout.rows * layout.columns, images.length);
|
|
66
|
-
const cellW = (region.wIn - (layout.columns - 1) * geom.gapIn) / layout.columns;
|
|
67
|
-
const cellH = (region.hIn - (layout.rows - 1) * geom.gapIn) / layout.rows;
|
|
68
|
-
const boxes = images.slice(0, used).map((image, i) => {
|
|
69
|
-
const r = Math.floor(i / layout.columns);
|
|
70
|
-
const c = i % layout.columns;
|
|
71
|
-
return {
|
|
72
|
-
image,
|
|
73
|
-
xIn: region.xIn + c * (cellW + geom.gapIn),
|
|
74
|
-
yIn: region.yIn + r * (cellH + geom.gapIn),
|
|
75
|
-
wIn: cellW,
|
|
76
|
-
hIn: cellH,
|
|
77
|
-
fit: layout.fit,
|
|
78
|
-
};
|
|
79
|
-
});
|
|
80
|
-
return { boxes, used };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const r = layoutMasonryPage(images, {
|
|
84
|
-
mode: layout.flow,
|
|
85
|
-
columns: layout.columns,
|
|
86
|
-
rows: layout.rows,
|
|
87
|
-
bodyWIn: region.wIn,
|
|
88
|
-
bodyHIn: region.hIn,
|
|
89
|
-
gapIn: geom.gapIn,
|
|
90
|
-
targetRowHeightIn: geom.masonryTargetRowIn,
|
|
91
|
-
});
|
|
92
|
-
const boxes = r.boxes.slice(0, r.used).map((b) => ({
|
|
93
|
-
image: b.asset,
|
|
94
|
-
xIn: region.xIn + b.xIn,
|
|
95
|
-
yIn: region.yIn + b.yIn,
|
|
96
|
-
wIn: b.wIn,
|
|
97
|
-
hIn: b.hIn,
|
|
98
|
-
fit: "contain" as const,
|
|
99
|
-
}));
|
|
100
|
-
return { boxes, used: r.used };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function paginateChunk(
|
|
104
|
-
chunk: Chunk,
|
|
105
|
-
settings: SectionSettings,
|
|
106
|
-
textHeightsIn: number[],
|
|
107
|
-
geom: DocGeom = DOC_GEOM,
|
|
108
|
-
naturalTextWIn?: number,
|
|
109
|
-
): DerivedPage[] {
|
|
110
|
-
if (settings.excluded) return [];
|
|
111
|
-
if (chunk.textBlocks.length !== textHeightsIn.length) {
|
|
112
|
-
throw new Error("paginateChunk: textHeightsIn must match chunk.textBlocks");
|
|
113
|
-
}
|
|
114
|
-
if (chunk.textBlocks.length === 0 && chunk.images.length === 0) return [];
|
|
115
|
-
|
|
116
|
-
// leftColumn shrinks to the widest natural text line (+ slack so sub-pixel
|
|
117
|
-
// rounding can't re-wrap), floored/capped; absent measurement = legacy fixed
|
|
118
|
-
// column. The shrunk width is >= every block's natural width by
|
|
119
|
-
// construction, so measured heights remain valid.
|
|
120
|
-
const colWIn =
|
|
121
|
-
naturalTextWIn === undefined
|
|
122
|
-
? geom.leftColWIn
|
|
123
|
-
: Math.min(geom.leftColWIn, Math.max(geom.minTextColIn, naturalTextWIn + 0.05));
|
|
124
|
-
|
|
125
|
-
const pages: DerivedPage[] = [];
|
|
126
|
-
let ti = 0;
|
|
127
|
-
let ii = 0;
|
|
128
|
-
const top = geom.titleBandIn;
|
|
129
|
-
|
|
130
|
-
while (ti < chunk.textBlocks.length || ii < chunk.images.length) {
|
|
131
|
-
const cont = pages.length > 0;
|
|
132
|
-
const warnings: string[] = [];
|
|
133
|
-
const textW = settings.textPlacement === "top" ? geom.bodyWIn : colWIn;
|
|
134
|
-
|
|
135
|
-
// 1. Greedy text fill. Top text is capped at half the body so a text-heavy
|
|
136
|
-
// section still leaves room for images below; leftColumn keeps full height.
|
|
137
|
-
// The cap only earns its keep while images are still waiting for room. Held
|
|
138
|
-
// on every page, it paginated a text-only section (or the text-only tail of
|
|
139
|
-
// one) at half utilisation and roughly doubled its page count.
|
|
140
|
-
const capY =
|
|
141
|
-
settings.textPlacement === "top" && ii < chunk.images.length
|
|
142
|
-
? geom.titleBandIn + (geom.bodyHIn - geom.titleBandIn) * geom.topTextMaxFraction
|
|
143
|
-
: geom.bodyHIn;
|
|
144
|
-
const placed: TextBlock[] = [];
|
|
145
|
-
let textEnd = top;
|
|
146
|
-
while (ti < chunk.textBlocks.length) {
|
|
147
|
-
const h = textHeightsIn[ti];
|
|
148
|
-
const yAfter = placed.length === 0 ? top + h : textEnd + geom.gapIn + h;
|
|
149
|
-
if (yAfter <= capY + EPS) {
|
|
150
|
-
placed.push(chunk.textBlocks[ti]);
|
|
151
|
-
textEnd = yAfter;
|
|
152
|
-
ti++;
|
|
153
|
-
} else if (placed.length === 0) {
|
|
154
|
-
// Oversize block: it gets the page, clipped at the cap (masonry-notice style).
|
|
155
|
-
placed.push(chunk.textBlocks[ti]);
|
|
156
|
-
textEnd = capY;
|
|
157
|
-
ti++;
|
|
158
|
-
warnings.push("A text block is taller than the page and was clipped.");
|
|
159
|
-
break;
|
|
160
|
-
} else {
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// dropTextOverflow: on the FIRST page only, discard any text that didn't fit
|
|
166
|
-
// rather than flowing it onto Cont. pages. Images still paginate as usual.
|
|
167
|
-
if (!cont && settings.dropTextOverflow && ti < chunk.textBlocks.length) {
|
|
168
|
-
const dropped = chunk.textBlocks.length - ti;
|
|
169
|
-
ti = chunk.textBlocks.length;
|
|
170
|
-
warnings.push(`${dropped} text block(s) hidden (overflow dropped)`);
|
|
171
|
-
}
|
|
172
|
-
const textRegion: Region | null =
|
|
173
|
-
placed.length > 0 ? { xIn: 0, yIn: top, wIn: textW, hIn: textEnd - top } : null;
|
|
174
|
-
|
|
175
|
-
// 2. Image region. leftColumn+right runs beside the text; every other
|
|
176
|
-
// case — and any page whose text is exhausted — is full width below it.
|
|
177
|
-
let region: Region;
|
|
178
|
-
if (settings.textPlacement === "leftColumn" && settings.imagePlacement === "right" && placed.length > 0) {
|
|
179
|
-
region = {
|
|
180
|
-
xIn: colWIn + geom.colGutterIn,
|
|
181
|
-
yIn: top,
|
|
182
|
-
wIn: geom.bodyWIn - colWIn - geom.colGutterIn,
|
|
183
|
-
hIn: geom.bodyHIn - top,
|
|
184
|
-
};
|
|
185
|
-
} else {
|
|
186
|
-
const startY = placed.length > 0 ? textEnd + geom.gapIn : top;
|
|
187
|
-
region = { xIn: 0, yIn: startY, wIn: geom.bodyWIn, hIn: Math.max(0, geom.bodyHIn - startY) };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 3. Fit images into the region.
|
|
191
|
-
const { boxes, used } = fitImages(chunk.images.slice(ii), settings.imageLayout, region, geom);
|
|
192
|
-
ii += used;
|
|
193
|
-
if (boxes.some((b) => b.yIn + b.hIn > region.yIn + region.hIn + 0.01)) {
|
|
194
|
-
warnings.push("An image overflows the page and is clipped.");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Progress guard: unreachable with sane geometry (a text-free page always
|
|
198
|
-
// gets the full region, and both fitters place ≥1 there), but a config
|
|
199
|
-
// mistake must not hang the browser.
|
|
200
|
-
if (placed.length === 0 && used === 0) {
|
|
201
|
-
warnings.push("Pagination could not place remaining content.");
|
|
202
|
-
pages.push({
|
|
203
|
-
id: `${chunk.id}#${pages.length}`,
|
|
204
|
-
chunkId: chunk.id,
|
|
205
|
-
title: cont ? `${chunk.title} Cont.` : chunk.title,
|
|
206
|
-
cont,
|
|
207
|
-
kicker: chunk.kicker,
|
|
208
|
-
depth: chunk.depth,
|
|
209
|
-
textBlocks: placed,
|
|
210
|
-
textRegion,
|
|
211
|
-
imageBoxes: boxes,
|
|
212
|
-
warnings,
|
|
213
|
-
});
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
pages.push({
|
|
218
|
-
id: `${chunk.id}#${pages.length}`,
|
|
219
|
-
chunkId: chunk.id,
|
|
220
|
-
title: cont ? `${chunk.title} Cont.` : chunk.title,
|
|
221
|
-
cont,
|
|
222
|
-
kicker: chunk.kicker,
|
|
223
|
-
depth: chunk.depth,
|
|
224
|
-
textBlocks: placed,
|
|
225
|
-
textRegion,
|
|
226
|
-
imageBoxes: boxes,
|
|
227
|
-
warnings,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return pages;
|
|
232
|
-
}
|