postext 0.3.35 → 0.3.37
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/__tests__/columnBalancing.test.js +28 -0
- package/dist/__tests__/columnBalancing.test.js.map +1 -1
- package/dist/__tests__/continuation.test.js +21 -0
- package/dist/__tests__/continuation.test.js.map +1 -1
- package/dist/__tests__/exports.test.js +13 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/headingStyles.test.d.ts +2 -0
- package/dist/__tests__/headingStyles.test.d.ts.map +1 -0
- package/dist/__tests__/headingStyles.test.js +134 -0
- package/dist/__tests__/headingStyles.test.js.map +1 -0
- package/dist/__tests__/parse/containers.test.js +1 -1
- package/dist/__tests__/parse/containers.test.js.map +1 -1
- package/dist/__tests__/pipeline/segmentBalancing.test.d.ts +2 -0
- package/dist/__tests__/pipeline/segmentBalancing.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/segmentBalancing.test.js +70 -0
- package/dist/__tests__/pipeline/segmentBalancing.test.js.map +1 -0
- package/dist/__tests__/toc.test.d.ts +2 -0
- package/dist/__tests__/toc.test.d.ts.map +1 -0
- package/dist/__tests__/toc.test.js +199 -0
- package/dist/__tests__/toc.test.js.map +1 -0
- package/dist/canvas-backend/blockRender.d.ts.map +1 -1
- package/dist/canvas-backend/blockRender.js +21 -11
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +9 -0
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/defaults/headerFooter.d.ts.map +1 -1
- package/dist/defaults/headerFooter.js +17 -0
- package/dist/defaults/headerFooter.js.map +1 -1
- package/dist/defaults/headingStyles.d.ts +10 -0
- package/dist/defaults/headingStyles.d.ts.map +1 -0
- package/dist/defaults/headingStyles.js +85 -0
- package/dist/defaults/headingStyles.js.map +1 -0
- package/dist/defaults/headings.d.ts +6 -1
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +32 -0
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/index.d.ts +2 -0
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +18 -0
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/paragraphStyles.d.ts.map +1 -1
- package/dist/defaults/paragraphStyles.js +4 -1
- package/dist/defaults/paragraphStyles.js.map +1 -1
- package/dist/defaults/toc.d.ts +26 -0
- package/dist/defaults/toc.d.ts.map +1 -0
- package/dist/defaults/toc.js +167 -0
- package/dist/defaults/toc.js.map +1 -0
- package/dist/design/layout.d.ts +15 -3
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +66 -1
- package/dist/design/layout.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +16 -5
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/numbering.d.ts +5 -1
- package/dist/numbering.d.ts.map +1 -1
- package/dist/numbering.js +7 -1
- package/dist/numbering.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +1 -1
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/index.d.ts +1 -1
- package/dist/parse/index.d.ts.map +1 -1
- package/dist/parse/index.js.map +1 -1
- package/dist/parse/types.d.ts +24 -1
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +459 -142
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildBlockKind.d.ts +4 -0
- package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
- package/dist/pipeline/buildBlockKind.js +2 -2
- package/dist/pipeline/buildBlockKind.js.map +1 -1
- package/dist/pipeline/columnBalancing.d.ts +27 -5
- package/dist/pipeline/columnBalancing.d.ts.map +1 -1
- package/dist/pipeline/columnBalancing.js +51 -10
- package/dist/pipeline/columnBalancing.js.map +1 -1
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +3 -1
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/continuation.d.ts +10 -1
- package/dist/pipeline/continuation.d.ts.map +1 -1
- package/dist/pipeline/continuation.js +16 -1
- package/dist/pipeline/continuation.js.map +1 -1
- package/dist/pipeline/headerFooter.d.ts +5 -2
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +90 -15
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/headingStyles.d.ts +58 -0
- package/dist/pipeline/headingStyles.d.ts.map +1 -0
- package/dist/pipeline/headingStyles.js +157 -0
- package/dist/pipeline/headingStyles.js.map +1 -0
- package/dist/pipeline/index.d.ts +2 -1
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +2 -1
- package/dist/pipeline/index.js.map +1 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +4 -0
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/outline.d.ts +37 -0
- package/dist/pipeline/outline.d.ts.map +1 -0
- package/dist/pipeline/outline.js +146 -0
- package/dist/pipeline/outline.js.map +1 -0
- package/dist/pipeline/pageRoles.d.ts +2 -1
- package/dist/pipeline/pageRoles.d.ts.map +1 -1
- package/dist/pipeline/pageRoles.js +5 -4
- package/dist/pipeline/pageRoles.js.map +1 -1
- package/dist/pipeline/placeholders.d.ts.map +1 -1
- package/dist/pipeline/placeholders.js +6 -0
- package/dist/pipeline/placeholders.js.map +1 -1
- package/dist/pipeline/resourceNumbering.d.ts +3 -1
- package/dist/pipeline/resourceNumbering.d.ts.map +1 -1
- package/dist/pipeline/resourceNumbering.js +4 -2
- package/dist/pipeline/resourceNumbering.js.map +1 -1
- package/dist/pipeline/styles.d.ts +5 -2
- package/dist/pipeline/styles.d.ts.map +1 -1
- package/dist/pipeline/styles.js +7 -3
- package/dist/pipeline/styles.js.map +1 -1
- package/dist/pipeline/toc.d.ts +22 -0
- package/dist/pipeline/toc.d.ts.map +1 -0
- package/dist/pipeline/toc.js +249 -0
- package/dist/pipeline/toc.js.map +1 -0
- package/dist/types.d.ts +273 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +37 -2
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/package.json +1 -1
package/dist/pipeline/build.js
CHANGED
|
@@ -6,7 +6,10 @@ import { parseMarkdownMemo } from '../parse';
|
|
|
6
6
|
import { buildPageLabels, computeHeadingNumbers, } from '../numbering';
|
|
7
7
|
import { extractFrontmatter } from '../frontmatter';
|
|
8
8
|
import { initHyphenator } from '../measure';
|
|
9
|
-
import { resolveAllConfig, computeBaselineGrid
|
|
9
|
+
import { resolveAllConfig, computeBaselineGrid } from './config';
|
|
10
|
+
import { createHeadingLevelResolver, deriveSectionGeometryConfig, deriveSectionMeasureContext, headingIsNumbered, headingStyleOf, planHeadingSections, } from './headingStyles';
|
|
11
|
+
import { computeOutline, hasTocDirective, outlineFromDoc, sameOutline } from './outline';
|
|
12
|
+
import { expandTocDirectives } from './toc';
|
|
10
13
|
import { resolveBodyStyle, resolveBlockquoteStyle } from './styles';
|
|
11
14
|
import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, } from './lists';
|
|
12
15
|
import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, } from './placement';
|
|
@@ -22,7 +25,7 @@ import { enumerateCurrentPageSlots, measureFloatBand, columnHasFloatBand, fitsSt
|
|
|
22
25
|
import { computeHeadingContext, computeResourceNumbering, } from './resourceNumbering';
|
|
23
26
|
import { defaultResourceTypes } from '../defaults/resourceTypes';
|
|
24
27
|
import { buildHeadersAndFooters, measureHeadingAdvancedDesignHeight } from './headerFooter';
|
|
25
|
-
import {
|
|
28
|
+
import { proposeBalanceLines, collectColumnGaps, firstDivergentColumn, gapLinesIn, pageSegments, MAX_BALANCING_PASSES, MAX_BALANCING_PASSES_PER_DOCUMENT, balanceKey } from './columnBalancing';
|
|
26
29
|
import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, resolveBandCaps, resolveTrailingCaps, bandCapLinesAroundZone, } from './bandCaps';
|
|
27
30
|
import { raggedLooseLines } from './raggedLines';
|
|
28
31
|
/** Tolerance for "does this block fit" checks against a column's free
|
|
@@ -97,7 +100,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
97
100
|
const looseOutcome = new Map();
|
|
98
101
|
// Lines gained so far per column budget group (see `LooseBudget`).
|
|
99
102
|
const looseGained = new Map();
|
|
100
|
-
|
|
103
|
+
// Level configs with heading-style overrides merged in (`{style="…"}`).
|
|
104
|
+
const headingLevels = createHeadingLevelResolver(resolved);
|
|
101
105
|
const dpi = resolved.page.dpi;
|
|
102
106
|
// Initialize hyphenator if needed (body text, or any justified paragraph
|
|
103
107
|
// style that hyphenates — they share the document locale).
|
|
@@ -123,7 +127,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
123
127
|
if (continuation?.part)
|
|
124
128
|
doc.partStart = continuation.part;
|
|
125
129
|
const pageMetrics = computePageMetrics(resolved);
|
|
126
|
-
const { pageWidthPx, pageHeightPx, trimOffset
|
|
130
|
+
const { pageWidthPx, pageHeightPx, trimOffset } = pageMetrics;
|
|
131
|
+
// The geometry pages are opened with: the document's, or — inside a
|
|
132
|
+
// styled section with its own margins / layout — the section's.
|
|
133
|
+
let contentArea = pageMetrics.contentArea;
|
|
134
|
+
let geomResolved = resolved;
|
|
127
135
|
// Page/bleed frames for design elements anchored to `'page'` / `'bleed'`.
|
|
128
136
|
const designFrames = { page: pageMetrics.trimBox, bleed: pageMetrics.bleedBox };
|
|
129
137
|
doc.trimOffset = trimOffset;
|
|
@@ -133,22 +141,29 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
133
141
|
// Extract frontmatter, then parse the remaining markdown body
|
|
134
142
|
const { metadata: frontmatterMeta, content: markdownBody, contentOffset: bodyOffset } = extractFrontmatter(content.markdown);
|
|
135
143
|
doc.metadata = { ...(content.metadata ?? {}), ...frontmatterMeta };
|
|
136
|
-
const
|
|
144
|
+
const parsedBlocks = parseMarkdownMemo(markdownBody);
|
|
145
|
+
const headingStart = continuation?.headings;
|
|
146
|
+
// `:::toc` expands into the entries of the book's outline — the one the
|
|
147
|
+
// host supplied, else this document's own (page labels unknown on the
|
|
148
|
+
// first pass; `buildDocument` lays the document out again with them).
|
|
149
|
+
const outline = content.outline
|
|
150
|
+
?? (hasTocDirective(parsedBlocks) ? computeOutline(parsedBlocks, resolved, headingStart) : undefined);
|
|
151
|
+
const contentBlocks = expandTocDirectives(parsedBlocks, outline, resolved);
|
|
152
|
+
const isNumbered = (b) => headingIsNumbered(b, resolved);
|
|
137
153
|
const headingTemplates = {};
|
|
138
154
|
for (const lvl of resolved.headings.levels) {
|
|
139
155
|
if (lvl.numberingTemplate && lvl.numberingTemplate.length > 0) {
|
|
140
156
|
headingTemplates[lvl.level] = lvl.numberingTemplate;
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
|
-
const
|
|
144
|
-
const headingPrefixes = computeHeadingNumbers(contentBlocks, headingTemplates, headingStart ? [headingStart.h1, headingStart.h2, headingStart.h3, headingStart.h4, headingStart.h5, headingStart.h6] : undefined);
|
|
159
|
+
const headingPrefixes = computeHeadingNumbers(contentBlocks, headingTemplates, headingStart ? [headingStart.h1, headingStart.h2, headingStart.h3, headingStart.h4, headingStart.h5, headingStart.h6] : undefined, isNumbered);
|
|
145
160
|
// Resource numbering — computed up front (before the placement loop) so that
|
|
146
161
|
// captions and inline `:ref`s can resolve their rendered number strings
|
|
147
162
|
// before measurement. Numbering follows order of first reference in the
|
|
148
163
|
// document.
|
|
149
164
|
const resourceTypes = config?.resourceTypes ?? defaultResourceTypes();
|
|
150
165
|
const resources = content.resources ?? [];
|
|
151
|
-
const headingContext = computeHeadingContext(contentBlocks, headingStart);
|
|
166
|
+
const headingContext = computeHeadingContext(contentBlocks, headingStart, isNumbered);
|
|
152
167
|
const resourceNumbering = computeResourceNumbering(contentBlocks, resourceTypes, resources, headingContext, continuation ? { counters: continuation.resourceCounters, numbered: continuation.resourceNumbers } : undefined);
|
|
153
168
|
// Lookups threaded into block-kind resolution + measurement.
|
|
154
169
|
const resourceById = new Map();
|
|
@@ -173,6 +188,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
173
188
|
const calloutPlan = planCallouts(contentBlocks);
|
|
174
189
|
// `:::part` ranges: start/end marker indices and the enclosed blocks.
|
|
175
190
|
const partPlan = planParts(contentBlocks);
|
|
191
|
+
// Styled sections (`{style="…"}` headings): geometry, running heads and
|
|
192
|
+
// body typography per content-block index.
|
|
193
|
+
const sectionPlan = planHeadingSections(contentBlocks, resolved);
|
|
176
194
|
// --- Float planning (issue #49 — resources float to page bands) ----------
|
|
177
195
|
// A resource is incorporated by its first reference (an inline `:ref` or a
|
|
178
196
|
// `::resource` directive, whichever comes first in reading order). Floated
|
|
@@ -837,7 +855,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
837
855
|
const before = floatsPlaced;
|
|
838
856
|
const startPageIndex = cursor.pageIndex;
|
|
839
857
|
do {
|
|
840
|
-
advanceToNextColumn(doc, cursor,
|
|
858
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
841
859
|
} while (cursor.pageIndex === startPageIndex);
|
|
842
860
|
if (floatsPlaced === before)
|
|
843
861
|
break; // safety: no progress
|
|
@@ -846,6 +864,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
846
864
|
// Everything per-block measurement needs that is constant for this pass.
|
|
847
865
|
const measureCtx = {
|
|
848
866
|
resolved,
|
|
867
|
+
headingLevels,
|
|
849
868
|
bodyStyle,
|
|
850
869
|
blockquoteStyle,
|
|
851
870
|
headingPrefixes,
|
|
@@ -869,6 +888,59 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
869
888
|
: measureCtx;
|
|
870
889
|
// Placement cursor
|
|
871
890
|
const cursor = { pageIndex: 0, columnIndex: 0 };
|
|
891
|
+
// Blocks inside a styled section with a body style measure with it.
|
|
892
|
+
const sectionMeasureCtxs = new Map();
|
|
893
|
+
const sectionMeasureCtx = (style) => {
|
|
894
|
+
if (!style?.bodyStyle)
|
|
895
|
+
return measureCtx;
|
|
896
|
+
let ctx = sectionMeasureCtxs.get(style);
|
|
897
|
+
if (!ctx) {
|
|
898
|
+
ctx = deriveSectionMeasureContext(measureCtx, style);
|
|
899
|
+
sectionMeasureCtxs.set(style, ctx);
|
|
900
|
+
}
|
|
901
|
+
return ctx;
|
|
902
|
+
};
|
|
903
|
+
/** The styled section whose geometry the pages opened from now on take. */
|
|
904
|
+
let currentSection;
|
|
905
|
+
const enterSection = (style) => {
|
|
906
|
+
currentSection = style;
|
|
907
|
+
geomResolved = style ? deriveSectionGeometryConfig(resolved, style) : resolved;
|
|
908
|
+
contentArea = geomResolved === resolved ? pageMetrics.contentArea : computePageMetrics(geomResolved).contentArea;
|
|
909
|
+
// A page still empty takes the geometry right away — the document (or
|
|
910
|
+
// a chapter laid out on its own) opening with a styled heading.
|
|
911
|
+
const page = doc.pages[cursor.pageIndex];
|
|
912
|
+
if (page && !page.partInfo && cursor.columnIndex === 0
|
|
913
|
+
&& !pageHasContent(page) && !(page.floats && page.floats.length > 0)) {
|
|
914
|
+
const fresh = createPageWithColumns(page.index, geomResolved, contentArea, pageWidthPx, pageHeightPx, pageIndexOffset);
|
|
915
|
+
if (page.blankForParity)
|
|
916
|
+
fresh.blankForParity = true;
|
|
917
|
+
if (page.blankForForce)
|
|
918
|
+
fresh.blankForForce = true;
|
|
919
|
+
doc.pages[cursor.pageIndex] = fresh;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
/** Heading-style and contents-row stamps a placed block carries for the
|
|
923
|
+
* running heads (`computeSectionStyles`) and the contents' part rows. */
|
|
924
|
+
const stampBlockExtras = (blk, raw) => {
|
|
925
|
+
if (raw.type === 'heading') {
|
|
926
|
+
const style = headingStyleOf(raw, resolved);
|
|
927
|
+
if (style) {
|
|
928
|
+
blk.headingStyleId = style.id;
|
|
929
|
+
if (!style.numbered)
|
|
930
|
+
blk.unnumbered = true;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (raw.toc?.kind === 'entry')
|
|
934
|
+
blk.tocEntry = true;
|
|
935
|
+
if (raw.toc?.kind === 'part') {
|
|
936
|
+
blk.tocPart = {
|
|
937
|
+
number: raw.toc.number,
|
|
938
|
+
title: raw.toc.title ?? '',
|
|
939
|
+
pageLabel: raw.toc.pageLabel ?? '',
|
|
940
|
+
...(raw.toc.palette ? { palette: raw.toc.palette } : {}),
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
};
|
|
872
944
|
let blockIdCounter = 0;
|
|
873
945
|
let pendingSpacing = 0;
|
|
874
946
|
// Pages whose break into the next page is explicit rather than natural
|
|
@@ -1029,11 +1101,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1029
1101
|
if (curPage.partInfo && !pageHasContent(curPage)) {
|
|
1030
1102
|
const startPageIndex = cursor.pageIndex;
|
|
1031
1103
|
do {
|
|
1032
|
-
advanceToNextColumn(doc, cursor,
|
|
1104
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx);
|
|
1033
1105
|
} while (cursor.pageIndex === startPageIndex);
|
|
1034
1106
|
return;
|
|
1035
1107
|
}
|
|
1036
|
-
advanceToNextPageBoundary(doc, cursor,
|
|
1108
|
+
advanceToNextPageBoundary(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx);
|
|
1037
1109
|
};
|
|
1038
1110
|
// Page-numbering segments. The implicit first segment comes from
|
|
1039
1111
|
// `cfg.page.pageNumbering`; `:::numbering` directives append more,
|
|
@@ -1049,18 +1121,27 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1049
1121
|
let pendingNumberingChange = null;
|
|
1050
1122
|
let lastSeenPageIndex = 0;
|
|
1051
1123
|
/** Commits any pending `:::numbering` change once we've crossed into a
|
|
1052
|
-
* new page
|
|
1124
|
+
* new page — or while the current page is still empty: the directive at
|
|
1125
|
+
* the head of a chapter (or right after a page break) numbers the page
|
|
1126
|
+
* it opens, not the one after. Called after every block iteration. */
|
|
1053
1127
|
const flushPendingNumberingAtBoundary = () => {
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
1128
|
+
if (pendingNumberingChange) {
|
|
1129
|
+
const page = doc.pages[cursor.pageIndex];
|
|
1130
|
+
const pageStillEmpty = !pageHasContent(page) && !(page.floats && page.floats.length > 0);
|
|
1131
|
+
if (cursor.pageIndex > lastSeenPageIndex || pageStillEmpty) {
|
|
1132
|
+
// A change already recorded for this page is replaced.
|
|
1133
|
+
const last = pageNumberSegments[pageNumberSegments.length - 1];
|
|
1134
|
+
if (last.startPageIndex === cursor.pageIndex && pageNumberSegments.length > 1)
|
|
1135
|
+
pageNumberSegments.pop();
|
|
1056
1136
|
pageNumberSegments.push({
|
|
1057
1137
|
startPageIndex: cursor.pageIndex,
|
|
1058
1138
|
...pendingNumberingChange,
|
|
1059
1139
|
});
|
|
1060
1140
|
pendingNumberingChange = null;
|
|
1061
1141
|
}
|
|
1062
|
-
lastSeenPageIndex = cursor.pageIndex;
|
|
1063
1142
|
}
|
|
1143
|
+
if (cursor.pageIndex > lastSeenPageIndex)
|
|
1144
|
+
lastSeenPageIndex = cursor.pageIndex;
|
|
1064
1145
|
};
|
|
1065
1146
|
/** Heading blocks that are not part of a callout — the only ones the
|
|
1066
1147
|
* keep-with-next rollbacks may pull along (a callout is one unbreakable
|
|
@@ -1508,7 +1589,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1508
1589
|
frameId = `block-${blockIdCounter++}`;
|
|
1509
1590
|
const startPageIndex = cursor.pageIndex;
|
|
1510
1591
|
do {
|
|
1511
|
-
advanceToNextColumn(doc, cursor,
|
|
1592
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
1512
1593
|
} while (cursor.pageIndex === startPageIndex);
|
|
1513
1594
|
forceHere = true;
|
|
1514
1595
|
continue;
|
|
@@ -1580,7 +1661,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1580
1661
|
pendingSpacing = 0;
|
|
1581
1662
|
const startPageIndex = cursor.pageIndex;
|
|
1582
1663
|
do {
|
|
1583
|
-
advanceToNextColumn(doc, cursor,
|
|
1664
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
1584
1665
|
} while (cursor.pageIndex === startPageIndex);
|
|
1585
1666
|
page = doc.pages[cursor.pageIndex];
|
|
1586
1667
|
}
|
|
@@ -1610,7 +1691,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1610
1691
|
continue;
|
|
1611
1692
|
}
|
|
1612
1693
|
if (b.type === 'heading' && b.level) {
|
|
1613
|
-
const level =
|
|
1694
|
+
const level = headingLevels.forBlock(b);
|
|
1614
1695
|
return level?.breakBefore?.enabled === true || level?.span === 'page';
|
|
1615
1696
|
}
|
|
1616
1697
|
return false;
|
|
@@ -1716,7 +1797,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1716
1797
|
pendingSpacing = 0;
|
|
1717
1798
|
const startPageIndex = cursor.pageIndex;
|
|
1718
1799
|
do {
|
|
1719
|
-
advanceToNextColumn(doc, cursor,
|
|
1800
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
1720
1801
|
} while (cursor.pageIndex === startPageIndex);
|
|
1721
1802
|
page = doc.pages[cursor.pageIndex];
|
|
1722
1803
|
fit = attempt(page, false) ?? attempt(page, true);
|
|
@@ -1854,10 +1935,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1854
1935
|
pendingSpacing = 0;
|
|
1855
1936
|
if (part === 0 && resolved.headings.keepWithNext && run > 0 && run < curCol.blocks.length) {
|
|
1856
1937
|
const rolledBack = rollbackTrailingBlocks(curCol, doc.blocks, isFreeHeading);
|
|
1857
|
-
advanceToNextColumn(doc, cursor,
|
|
1938
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
1858
1939
|
return (rolledBack[0].contentIndex ?? startIdx - rolledBack.length) - 1;
|
|
1859
1940
|
}
|
|
1860
|
-
advanceToNextColumn(doc, cursor,
|
|
1941
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
1861
1942
|
// Try the next column afresh: it may be short too (a float band
|
|
1862
1943
|
// reserved on the page it opened), or of another width (oneAndHalf).
|
|
1863
1944
|
continue;
|
|
@@ -1893,7 +1974,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1893
1974
|
curCol.availableHeight = Math.max(0, curCol.availableHeight - balanceBefore);
|
|
1894
1975
|
}
|
|
1895
1976
|
enterBand(startIdx, 0);
|
|
1896
|
-
placeAtomicBlock(placed.frame, placed.totalHeight, spacingBefore, cursor, doc,
|
|
1977
|
+
placeAtomicBlock(placed.frame, placed.totalHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx);
|
|
1897
1978
|
enterBand(startIdx, 0);
|
|
1898
1979
|
curCol = currentColumn(doc, cursor);
|
|
1899
1980
|
commitCallout(placed, startIdx, plan, curCol, part > 0 || fragment ? fragmentRange(L, from, to) : undefined);
|
|
@@ -1912,7 +1993,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1912
1993
|
from = to;
|
|
1913
1994
|
part++;
|
|
1914
1995
|
frameId = `block-${blockIdCounter++}`;
|
|
1915
|
-
advanceToNextColumn(doc, cursor,
|
|
1996
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
1916
1997
|
}
|
|
1917
1998
|
};
|
|
1918
1999
|
for (let blockIdx = 0; blockIdx < contentBlocks.length; blockIdx++) {
|
|
@@ -1920,6 +2001,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1920
2001
|
throw new BuildCancelledError();
|
|
1921
2002
|
options?.onProgress?.({ pass: 1, blocks: blockIdx, totalBlocks: contentBlocks.length, pages: doc.pages.length });
|
|
1922
2003
|
const rawBlock = contentBlocks[blockIdx];
|
|
2004
|
+
// The styled section this block sits in: its geometry applies to the
|
|
2005
|
+
// pages opened from here (a heading with `breakBefore` opens one).
|
|
2006
|
+
const blockSection = sectionPlan.byBlock[blockIdx];
|
|
2007
|
+
if (blockSection !== currentSection)
|
|
2008
|
+
enterSection(blockSection);
|
|
1923
2009
|
// Floats whose reference landed in an earlier iteration take the first
|
|
1924
2010
|
// free slot of the current page now — after their reference in reading
|
|
1925
2011
|
// order. Then enqueue the floats first-referenced in this block, so the
|
|
@@ -1934,13 +2020,13 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1934
2020
|
if (name === 'pagebreak') {
|
|
1935
2021
|
pendingSpacing = 0;
|
|
1936
2022
|
markForcedBreak();
|
|
1937
|
-
advanceToNextPageBoundary(doc, cursor,
|
|
2023
|
+
advanceToNextPageBoundary(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx);
|
|
1938
2024
|
const parity = attrs.parity;
|
|
1939
2025
|
if (parity === 'odd'
|
|
1940
2026
|
|| parity === 'even'
|
|
1941
2027
|
|| parity === 'always-odd'
|
|
1942
2028
|
|| parity === 'always-even') {
|
|
1943
|
-
enforcePageParity(doc, cursor,
|
|
2029
|
+
enforcePageParity(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, parity);
|
|
1944
2030
|
}
|
|
1945
2031
|
// Pending floats land on the page that follows the break — after
|
|
1946
2032
|
// parity padding, so a blank parity page never carries a float.
|
|
@@ -1959,7 +2045,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1959
2045
|
const page = doc.pages[cursor.pageIndex];
|
|
1960
2046
|
if (cursor.columnIndex === page.columns.length - 1)
|
|
1961
2047
|
markForcedBreak();
|
|
1962
|
-
advanceToNextColumn(doc, cursor,
|
|
2048
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
1963
2049
|
flushPendingNumberingAtBoundary();
|
|
1964
2050
|
}
|
|
1965
2051
|
}
|
|
@@ -1975,6 +2061,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1975
2061
|
}
|
|
1976
2062
|
if (Object.keys(change).length > 0)
|
|
1977
2063
|
pendingNumberingChange = change;
|
|
2064
|
+
// A page opened just before (a `:::pagebreak`, the chapter's head)
|
|
2065
|
+
// is still empty: it takes the change now. Otherwise the change
|
|
2066
|
+
// waits for the next page boundary.
|
|
2067
|
+
flushPendingNumberingAtBoundary();
|
|
1978
2068
|
}
|
|
1979
2069
|
continue;
|
|
1980
2070
|
}
|
|
@@ -1997,7 +2087,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1997
2087
|
pendingSpacing = 0;
|
|
1998
2088
|
closeFlowSegment(blockIdx);
|
|
1999
2089
|
leaveCurrentPage();
|
|
2000
|
-
enforcePageParity(doc, cursor,
|
|
2090
|
+
enforcePageParity(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, resolved.parts.breakBefore.parity);
|
|
2001
2091
|
cursor.columnIndex = 0;
|
|
2002
2092
|
// Map the opener's title back to the `title="…"` attribute of the
|
|
2003
2093
|
// fence so the editor can place the cursor from a click on the band.
|
|
@@ -2034,7 +2124,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2034
2124
|
pendingSpacing = 0;
|
|
2035
2125
|
closeFlowSegment(blockIdx);
|
|
2036
2126
|
leaveCurrentPage();
|
|
2037
|
-
enforcePageParity(doc, cursor,
|
|
2127
|
+
enforcePageParity(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, parity);
|
|
2038
2128
|
flushPendingNumberingAtBoundary();
|
|
2039
2129
|
}
|
|
2040
2130
|
if (rawBlock.type === 'containerStart' && rawBlock.containerName === 'callout') {
|
|
@@ -2082,14 +2172,14 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2082
2172
|
}
|
|
2083
2173
|
// --- Heading `breakBefore` ----------------------------------------
|
|
2084
2174
|
if (rawBlock.type === 'heading' && rawBlock.level) {
|
|
2085
|
-
const level =
|
|
2175
|
+
const level = headingLevels.forBlock(rawBlock);
|
|
2086
2176
|
const bb = level?.breakBefore;
|
|
2087
2177
|
if (bb && bb.enabled) {
|
|
2088
2178
|
pendingSpacing = 0;
|
|
2089
2179
|
closeFlowSegment(blockIdx);
|
|
2090
|
-
advanceToNextPageBoundary(doc, cursor,
|
|
2180
|
+
advanceToNextPageBoundary(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx);
|
|
2091
2181
|
if (bb.parity !== 'any') {
|
|
2092
|
-
enforcePageParity(doc, cursor,
|
|
2182
|
+
enforcePageParity(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, bb.parity);
|
|
2093
2183
|
}
|
|
2094
2184
|
flushPendingNumberingAtBoundary();
|
|
2095
2185
|
}
|
|
@@ -2100,7 +2190,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2100
2190
|
if (level?.span === 'page') {
|
|
2101
2191
|
pendingSpacing = 0;
|
|
2102
2192
|
closeFlowSegment(blockIdx);
|
|
2103
|
-
advanceToNextPageBoundary(doc, cursor,
|
|
2193
|
+
advanceToNextPageBoundary(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx);
|
|
2104
2194
|
cursor.columnIndex = 0;
|
|
2105
2195
|
flushPendingNumberingAtBoundary();
|
|
2106
2196
|
}
|
|
@@ -2117,7 +2207,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2117
2207
|
// Measure against the current column width. `null` means there is nothing
|
|
2118
2208
|
// to place inline (empty text, unknown resource id, floated resource).
|
|
2119
2209
|
const col = currentColumn(doc, cursor);
|
|
2120
|
-
const blockMeasureCtx = partPlan.byBlock[blockIdx] ? partMeasureCtx :
|
|
2210
|
+
const blockMeasureCtx = partPlan.byBlock[blockIdx] ? partMeasureCtx : sectionMeasureCtx(sectionPlan.byBlock[blockIdx]);
|
|
2121
2211
|
const styleOverride = paragraphContainer
|
|
2122
2212
|
? (isContainerTail ? paragraphContainer.tailStyle : paragraphContainer.style)
|
|
2123
2213
|
: undefined;
|
|
@@ -2151,6 +2241,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2151
2241
|
const groupHeight = measured.totalHeight;
|
|
2152
2242
|
const blk = createVDTBlock(id, 'resource', style.fontString, style.color, style.textAlign);
|
|
2153
2243
|
blk.contentIndex = blockIdx;
|
|
2244
|
+
stampBlockExtras(blk, rawBlock);
|
|
2154
2245
|
blk.resourceBlock = resourceBlock;
|
|
2155
2246
|
blk.dirty = false;
|
|
2156
2247
|
blk.snappedToGrid = false;
|
|
@@ -2168,7 +2259,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2168
2259
|
}];
|
|
2169
2260
|
const spacingBefore = pendingSpacing;
|
|
2170
2261
|
enterBand(blockIdx, 0);
|
|
2171
|
-
placeAtomicBlock(blk, groupHeight, spacingBefore, cursor, doc,
|
|
2262
|
+
placeAtomicBlock(blk, groupHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx);
|
|
2172
2263
|
enterBand(blockIdx, 0);
|
|
2173
2264
|
// `placeBlockInColumn` (inside placeAtomicBlock) shifts `blk.lines`; the
|
|
2174
2265
|
// resource's own caption/table lines live on `resourceBlock` and must be
|
|
@@ -2234,10 +2325,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2234
2325
|
// paragraph of a `:::paragraphs` container, whose leading and spacing
|
|
2235
2326
|
// are off-grid by design.
|
|
2236
2327
|
const nextIsHeading = nextBlock?.type === 'heading';
|
|
2237
|
-
|
|
2328
|
+
// The contents (`:::toc`) keep their own rhythm: an entry set as a list
|
|
2329
|
+
// item is not a list tail to realign the text after it.
|
|
2330
|
+
const shouldSnapToGrid = rawBlock.toc === undefined && ((vdtType === 'heading' && !nextIsHeading) ||
|
|
2238
2331
|
(vdtType === 'listItem' && !nextIsListItem) ||
|
|
2239
2332
|
(vdtType === 'paragraph' && isContainerTail) ||
|
|
2240
|
-
vdtType === 'mathDisplay';
|
|
2333
|
+
vdtType === 'mathDisplay');
|
|
2241
2334
|
// Place block, splitting across columns/pages if needed.
|
|
2242
2335
|
// List items may split too — orphan/widow protection per-list is gated by
|
|
2243
2336
|
// `avoidOrphansInLists` / `avoidWidowsInLists`; bullet stays on first part.
|
|
@@ -2290,6 +2383,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2290
2383
|
spacingBefore = Math.max(spacingBefore, style.marginTopPx);
|
|
2291
2384
|
}
|
|
2292
2385
|
}
|
|
2386
|
+
else if (rawBlock.toc) {
|
|
2387
|
+
// A part row or an unnumbered entry of the contents: its top
|
|
2388
|
+
// margin (`toc.parts.marginTop`, the level's `marginTop`) applies
|
|
2389
|
+
// like a heading's.
|
|
2390
|
+
spacingBefore = Math.max(spacingBefore, style.marginTopPx);
|
|
2391
|
+
}
|
|
2293
2392
|
// Column balancing: extra grid lines above a non-heading balance
|
|
2294
2393
|
// target — the first block after a list end. Heading targets are
|
|
2295
2394
|
// handled inside the heading branch above (after margin collapsing).
|
|
@@ -2328,7 +2427,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2328
2427
|
// subsequent marginBottom + grid snap) starts from there.
|
|
2329
2428
|
let effectiveRemainHeight = totalRemainHeight;
|
|
2330
2429
|
if (vdtType === 'heading' && headingLevel !== undefined && partIndex === 0) {
|
|
2331
|
-
const lvl =
|
|
2430
|
+
const lvl = headingLevels.forBlock(rawBlock);
|
|
2332
2431
|
if (lvl) {
|
|
2333
2432
|
const full = remainingLines
|
|
2334
2433
|
.map((ln) => (ln.segments ?? []).map((s) => s.text).join(''))
|
|
@@ -2382,6 +2481,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2382
2481
|
if (letterSpacingPx !== undefined)
|
|
2383
2482
|
blk.letterSpacing = letterSpacingPx;
|
|
2384
2483
|
blk.contentIndex = blockIdx;
|
|
2484
|
+
stampBlockExtras(blk, rawBlock);
|
|
2385
2485
|
blk.headingLevel = headingLevel;
|
|
2386
2486
|
if (numberPrefix)
|
|
2387
2487
|
blk.numberPrefix = numberPrefix;
|
|
@@ -2397,7 +2497,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2397
2497
|
remainingLines = remainingLines.slice(splitAt);
|
|
2398
2498
|
partIndex++;
|
|
2399
2499
|
pendingSpacing = 0;
|
|
2400
|
-
advanceToNextColumn(doc, cursor,
|
|
2500
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2401
2501
|
continue;
|
|
2402
2502
|
}
|
|
2403
2503
|
// Can't cleanly split the colon line off — would create a widow.
|
|
@@ -2429,12 +2529,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2429
2529
|
// rolled-back heading (marker blocks in between are replayed).
|
|
2430
2530
|
blockIdx = (popped[0].contentIndex ?? blockIdx - headingRunCount) - 1;
|
|
2431
2531
|
pendingSpacing = 0;
|
|
2432
|
-
advanceToNextColumn(doc, cursor,
|
|
2532
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2433
2533
|
break;
|
|
2434
2534
|
}
|
|
2435
2535
|
if (headingRunCount === 0) {
|
|
2436
2536
|
pendingSpacing = 0;
|
|
2437
|
-
advanceToNextColumn(doc, cursor,
|
|
2537
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2438
2538
|
continue;
|
|
2439
2539
|
}
|
|
2440
2540
|
// headingRunCount === curCol.blocks.length: fall through to place.
|
|
@@ -2490,11 +2590,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2490
2590
|
// rolled-back heading (marker blocks in between are replayed).
|
|
2491
2591
|
blockIdx = (rolledBack[0].contentIndex ?? blockIdx - rolledBack.length) - 1;
|
|
2492
2592
|
pendingSpacing = 0;
|
|
2493
|
-
advanceToNextColumn(doc, cursor,
|
|
2593
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2494
2594
|
break;
|
|
2495
2595
|
}
|
|
2496
2596
|
pendingSpacing = 0;
|
|
2497
|
-
advanceToNextColumn(doc, cursor,
|
|
2597
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2498
2598
|
continue;
|
|
2499
2599
|
}
|
|
2500
2600
|
}
|
|
@@ -2508,6 +2608,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2508
2608
|
if (letterSpacingPx !== undefined)
|
|
2509
2609
|
blk.letterSpacing = letterSpacingPx;
|
|
2510
2610
|
blk.contentIndex = blockIdx;
|
|
2611
|
+
stampBlockExtras(blk, rawBlock);
|
|
2511
2612
|
if (partIndex === 0) {
|
|
2512
2613
|
blk.headingLevel = headingLevel;
|
|
2513
2614
|
if (numberPrefix)
|
|
@@ -2573,7 +2674,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2573
2674
|
// other column on this page so body text under the opener band
|
|
2574
2675
|
// starts below it in ALL columns, not just the one it was placed in.
|
|
2575
2676
|
if (vdtType === 'heading' && headingLevel !== undefined) {
|
|
2576
|
-
const lvl =
|
|
2677
|
+
const lvl = headingLevels.forBlock(rawBlock);
|
|
2577
2678
|
if (lvl?.span === 'page') {
|
|
2578
2679
|
const page = doc.pages[cursor.pageIndex];
|
|
2579
2680
|
for (const otherCol of page.columns) {
|
|
@@ -2633,6 +2734,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2633
2734
|
if (letterSpacingPx !== undefined)
|
|
2634
2735
|
blk.letterSpacing = letterSpacingPx;
|
|
2635
2736
|
blk.contentIndex = blockIdx;
|
|
2737
|
+
stampBlockExtras(blk, rawBlock);
|
|
2636
2738
|
if (partIndex === 0) {
|
|
2637
2739
|
blk.headingLevel = headingLevel;
|
|
2638
2740
|
if (numberPrefix)
|
|
@@ -2654,7 +2756,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2654
2756
|
remainingLines = remainingLines.slice(choice.splitAt);
|
|
2655
2757
|
partIndex++;
|
|
2656
2758
|
pendingSpacing = 0;
|
|
2657
|
-
advanceToNextColumn(doc, cursor,
|
|
2759
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2658
2760
|
continue;
|
|
2659
2761
|
}
|
|
2660
2762
|
// choice.splitAt === 0: fall through to push whole paragraph to next column
|
|
@@ -2679,12 +2781,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2679
2781
|
if (rolledBack.length > 0) {
|
|
2680
2782
|
blockIdx = (rolledBack[0].contentIndex ?? blockIdx - rolledBack.length) - 1;
|
|
2681
2783
|
pendingSpacing = 0;
|
|
2682
|
-
advanceToNextColumn(doc, cursor,
|
|
2784
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2683
2785
|
break;
|
|
2684
2786
|
}
|
|
2685
2787
|
}
|
|
2686
2788
|
pendingSpacing = 0;
|
|
2687
|
-
advanceToNextColumn(doc, cursor,
|
|
2789
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2688
2790
|
continue;
|
|
2689
2791
|
}
|
|
2690
2792
|
// Empty column with less than a line of room (a band cap cutting right
|
|
@@ -2692,7 +2794,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2692
2794
|
// go here — move on. The next column, or a fresh page, has room.
|
|
2693
2795
|
if (curCol.availableHeight < style.lineHeightPx - 0.01 && totalRemainHeight > curCol.availableHeight + 0.01) {
|
|
2694
2796
|
pendingSpacing = 0;
|
|
2695
|
-
advanceToNextColumn(doc, cursor,
|
|
2797
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2696
2798
|
continue;
|
|
2697
2799
|
}
|
|
2698
2800
|
// Empty column but block still doesn't fit (block taller than page) — place anyway
|
|
@@ -2702,6 +2804,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2702
2804
|
if (letterSpacingPx !== undefined)
|
|
2703
2805
|
blk.letterSpacing = letterSpacingPx;
|
|
2704
2806
|
blk.contentIndex = blockIdx;
|
|
2807
|
+
stampBlockExtras(blk, rawBlock);
|
|
2705
2808
|
if (partIndex === 0)
|
|
2706
2809
|
blk.headingLevel = headingLevel;
|
|
2707
2810
|
if (partIndex === 0 && vdtType === 'heading' && rawBlock.attrs)
|
|
@@ -2750,12 +2853,43 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2750
2853
|
page.pageLabel = info.label;
|
|
2751
2854
|
page.pageNumberFormat = info.format;
|
|
2752
2855
|
}
|
|
2753
|
-
|
|
2856
|
+
const restarts = pageNumberSegments
|
|
2857
|
+
.filter((s, i) => i > 0 && s.startAt !== undefined && s.startPageIndex < doc.pages.length)
|
|
2858
|
+
.map((s) => s.startPageIndex);
|
|
2859
|
+
if (restarts.length > 0)
|
|
2860
|
+
doc.pageNumberRestarts = restarts;
|
|
2861
|
+
buildHeadersAndFooters(doc, resourceById);
|
|
2754
2862
|
doc.converged = true;
|
|
2755
2863
|
doc.iterationCount = 1;
|
|
2756
2864
|
return { doc, forcedBreakPages, bandCapProposals, spanPlacedInBand, bandCapsApplied, looseOutcome };
|
|
2757
2865
|
}
|
|
2866
|
+
/** Passes a document printing its own contents gets at most, beyond the
|
|
2867
|
+
* first, for the page labels it prints to settle. */
|
|
2868
|
+
const MAX_TOC_ROUNDS = 3;
|
|
2758
2869
|
export function buildDocument(content, config, cache, options) {
|
|
2870
|
+
// A document printing its own table of contents (`:::toc` with no
|
|
2871
|
+
// host-supplied outline) is laid out with the page labels of the previous
|
|
2872
|
+
// build until they no longer change: the contents' own length moves what
|
|
2873
|
+
// follows, and a numbering restart after the front matter usually settles
|
|
2874
|
+
// it in one extra round.
|
|
2875
|
+
if (content.outline === undefined) {
|
|
2876
|
+
const parsed = parseMarkdownMemo(extractFrontmatter(content.markdown).content);
|
|
2877
|
+
if (hasTocDirective(parsed)) {
|
|
2878
|
+
let outline = computeOutline(parsed, resolveAllConfig(config), content.continuation?.headings);
|
|
2879
|
+
let doc = buildDocumentBalanced({ ...content, outline }, config, cache, options);
|
|
2880
|
+
for (let round = 0; round < MAX_TOC_ROUNDS; round++) {
|
|
2881
|
+
const after = outlineFromDoc(doc, outline);
|
|
2882
|
+
if (sameOutline(after, outline))
|
|
2883
|
+
break;
|
|
2884
|
+
outline = after;
|
|
2885
|
+
doc = buildDocumentBalanced({ ...content, outline }, config, cache, options);
|
|
2886
|
+
}
|
|
2887
|
+
return doc;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return buildDocumentBalanced(content, config, cache, options);
|
|
2891
|
+
}
|
|
2892
|
+
function buildDocumentBalanced(content, config, cache, options) {
|
|
2759
2893
|
// Each pass reports its own progress, numbered in build order.
|
|
2760
2894
|
let passIndex = 0;
|
|
2761
2895
|
const onProgress = options?.onProgress;
|
|
@@ -2776,72 +2910,209 @@ export function buildDocument(content, config, cache, options) {
|
|
|
2776
2910
|
const bandCaps = bands.bandCaps;
|
|
2777
2911
|
let passCount = bands.passCount;
|
|
2778
2912
|
best.doc.iterationCount = passCount;
|
|
2779
|
-
/** Hints that produced `best` (replayed by the trailing-cap passes). */
|
|
2780
|
-
let bestHints = { bandCaps };
|
|
2781
2913
|
// --- Column balancing (vertical justification) ------------------------
|
|
2782
2914
|
// Iteratively re-place the document with extra grid lines above headings
|
|
2783
|
-
// until every balanceable column ends flush with
|
|
2784
|
-
// further adjustment is possible
|
|
2785
|
-
// gaps on the freshly placed document, so
|
|
2786
|
-
// that shift under the new spacing are
|
|
2787
|
-
// (fewest leftover gap lines) always wins
|
|
2788
|
-
// discarded.
|
|
2915
|
+
// (and the other levers) until every balanceable column ends flush with
|
|
2916
|
+
// the page bottom, or no further adjustment is possible. Each retry
|
|
2917
|
+
// recomputes the remaining gaps on the freshly placed document, so
|
|
2918
|
+
// split / keep-with-next decisions that shift under the new spacing are
|
|
2919
|
+
// accounted for. The best layout (fewest leftover gap lines) always wins
|
|
2920
|
+
// — a retry that regresses is discarded.
|
|
2921
|
+
//
|
|
2922
|
+
// The pages between explicit breaks (chapter openers, `:::pagebreak`)
|
|
2923
|
+
// are laid out independently of one another — nothing flows across such
|
|
2924
|
+
// a break, so a lever inside one run of pages cannot move a line of any
|
|
2925
|
+
// other. The loop therefore judges every run (a *segment*, see
|
|
2926
|
+
// `pageSegments`) on its own: a pass places the whole document, but each
|
|
2927
|
+
// segment keeps or rejects its share of the levers by its own score,
|
|
2928
|
+
// blacklists its own cascades, plateaus on its own and spends its own
|
|
2929
|
+
// budget of attempts; a rejected segment gets its pages back from the
|
|
2930
|
+
// best pass it had (`spliceSegments`). A book of thirty chapters thus
|
|
2931
|
+
// balances exactly as its chapters would one by one, instead of one
|
|
2932
|
+
// cascade anywhere costing every page of the book a pass.
|
|
2789
2933
|
const balancing = best.doc.config.headings.balancing;
|
|
2790
2934
|
if (!balancing.enabled)
|
|
2791
2935
|
return best.doc;
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2936
|
+
/** Levers accepted so far, over every segment (keyed by content index /
|
|
2937
|
+
* `balanceKey`; each key belongs to exactly one segment). */
|
|
2938
|
+
const applied = { lines: new Map(), loose: new Map() };
|
|
2939
|
+
let segments = [];
|
|
2940
|
+
/** (Re)derive the segments of the current best layout, keeping the
|
|
2941
|
+
* attempts and blacklists of the segment at the same position. */
|
|
2942
|
+
const resetSegments = () => {
|
|
2943
|
+
const gaps = collectColumnGaps(best.doc, best.forcedBreakPages);
|
|
2944
|
+
const prev = segments;
|
|
2945
|
+
segments = pageSegments(best.doc.pages.length, best.forcedBreakPages).map((range, i) => {
|
|
2946
|
+
const score = gapLinesIn(gaps, range);
|
|
2947
|
+
const old = prev[i];
|
|
2948
|
+
return {
|
|
2949
|
+
range,
|
|
2950
|
+
bestScore: score,
|
|
2951
|
+
attempts: old?.attempts ?? 0,
|
|
2952
|
+
done: score === 0,
|
|
2953
|
+
stable: score === 0,
|
|
2954
|
+
failedLoose: old?.failedLoose ?? new Set(),
|
|
2955
|
+
failedLines: old?.failedLines ?? new Set(),
|
|
2956
|
+
};
|
|
2957
|
+
});
|
|
2958
|
+
};
|
|
2959
|
+
const hintsFrom = (lines, loose, looseBudget) => {
|
|
2960
|
+
const extraPx = new Map();
|
|
2961
|
+
for (const [idx, n] of lines)
|
|
2962
|
+
if (n > 0)
|
|
2963
|
+
extraPx.set(idx, n * best.doc.baselineGrid);
|
|
2964
|
+
return {
|
|
2965
|
+
balanceExtraPx: extraPx,
|
|
2966
|
+
balanceLooseness: loose,
|
|
2967
|
+
...(looseBudget ? { balanceLooseBudget: looseBudget } : {}),
|
|
2968
|
+
bandCaps,
|
|
2969
|
+
};
|
|
2970
|
+
};
|
|
2971
|
+
const segmentAt = (pageIndex) => segments.findIndex((s) => pageIndex >= s.range.from && pageIndex <= s.range.to);
|
|
2972
|
+
/** Segment owning each lever key of the current best layout. */
|
|
2973
|
+
const keyOwners = (gaps) => {
|
|
2974
|
+
const owner = new Map();
|
|
2975
|
+
for (const g of gaps) {
|
|
2976
|
+
const si = segmentAt(g.pageIndex);
|
|
2977
|
+
if (si < 0)
|
|
2978
|
+
continue;
|
|
2979
|
+
for (const c of g.candidates)
|
|
2980
|
+
owner.set(balanceKey(c.contentIndex, c.part ?? 0), si);
|
|
2981
|
+
}
|
|
2982
|
+
return owner;
|
|
2983
|
+
};
|
|
2984
|
+
/** First page each content index was placed on. */
|
|
2985
|
+
const pageOfContent = (doc) => {
|
|
2986
|
+
const m = new Map();
|
|
2987
|
+
for (const b of doc.blocks) {
|
|
2988
|
+
if (b.contentIndex !== undefined && b.pageIndex !== undefined && !m.has(b.contentIndex))
|
|
2989
|
+
m.set(b.contentIndex, b.pageIndex);
|
|
2990
|
+
}
|
|
2991
|
+
return m;
|
|
2992
|
+
};
|
|
2993
|
+
const inRange = (r, p) => p !== undefined && p >= r.from && p <= r.to;
|
|
2994
|
+
/** Whether the explicit breaks before / around a segment fell on the same
|
|
2995
|
+
* pages in `next` as in the best layout — the segment's pages then line
|
|
2996
|
+
* up and can be compared or spliced. `before` checks only the pages
|
|
2997
|
+
* ahead of it (an earlier segment's cascade shifts everything after). */
|
|
2998
|
+
const breaksMatch = (next, upTo) => {
|
|
2999
|
+
for (let p = 0; p < upTo; p++) {
|
|
3000
|
+
if (best.forcedBreakPages.has(p) !== next.forcedBreakPages.has(p))
|
|
3001
|
+
return false;
|
|
3002
|
+
}
|
|
3003
|
+
return true;
|
|
3004
|
+
};
|
|
3005
|
+
const startIntact = (next, s) => breaksMatch(next, s.range.from);
|
|
3006
|
+
const wholeIntact = (next, s, last) => {
|
|
3007
|
+
if (!breaksMatch(next, s.range.to + 1))
|
|
3008
|
+
return false;
|
|
3009
|
+
if (next.doc.pages.length <= s.range.to)
|
|
3010
|
+
return false;
|
|
3011
|
+
return !last || next.doc.pages.length === best.doc.pages.length;
|
|
3012
|
+
};
|
|
3013
|
+
/**
|
|
3014
|
+
* The best layout with the pages of `ranges` taken from `next` (whose
|
|
3015
|
+
* breaks line up with it there): pages, the blocks and warnings on them,
|
|
3016
|
+
* and the pass report entries whose block sits on them.
|
|
3017
|
+
*/
|
|
3018
|
+
const spliceSegments = (next, ranges) => {
|
|
3019
|
+
const taken = (p) => ranges.some((r) => inRange(r, p));
|
|
3020
|
+
const pages = best.doc.pages.map((pg, i) => (taken(i) ? next.doc.pages[i] : pg));
|
|
3021
|
+
const blocks = [
|
|
3022
|
+
...best.doc.blocks.filter((b) => !taken(b.pageIndex)),
|
|
3023
|
+
...next.doc.blocks.filter((b) => taken(b.pageIndex)),
|
|
3024
|
+
].sort((a, b) => (a.pageIndex ?? -1) - (b.pageIndex ?? -1));
|
|
3025
|
+
const warnings = [
|
|
3026
|
+
...(best.doc.warnings ?? []).filter((w) => !taken(w.pageIndex)),
|
|
3027
|
+
...(next.doc.warnings ?? []).filter((w) => taken(w.pageIndex)),
|
|
3028
|
+
].sort((a, b) => a.pageIndex - b.pageIndex);
|
|
3029
|
+
const doc = { ...best.doc, pages, blocks, ...(warnings.length > 0 ? { warnings } : { warnings: undefined }) };
|
|
3030
|
+
const pageBest = pageOfContent(best.doc);
|
|
3031
|
+
const pageNext = pageOfContent(next.doc);
|
|
3032
|
+
const mergeMap = (a, b) => {
|
|
3033
|
+
const out = new Map();
|
|
3034
|
+
for (const [k, v] of a)
|
|
3035
|
+
if (!taken(pageBest.get(k)))
|
|
3036
|
+
out.set(k, v);
|
|
3037
|
+
for (const [k, v] of b)
|
|
3038
|
+
if (taken(pageNext.get(k)))
|
|
3039
|
+
out.set(k, v);
|
|
3040
|
+
return out;
|
|
3041
|
+
};
|
|
3042
|
+
const mergeSet = (a, b) => {
|
|
3043
|
+
const out = new Set();
|
|
3044
|
+
for (const k of a)
|
|
3045
|
+
if (!taken(pageBest.get(k)))
|
|
3046
|
+
out.add(k);
|
|
3047
|
+
for (const k of b)
|
|
3048
|
+
if (taken(pageNext.get(k)))
|
|
3049
|
+
out.add(k);
|
|
3050
|
+
return out;
|
|
3051
|
+
};
|
|
3052
|
+
return {
|
|
3053
|
+
doc,
|
|
3054
|
+
forcedBreakPages: best.forcedBreakPages,
|
|
3055
|
+
bandCapProposals: mergeMap(best.bandCapProposals, next.bandCapProposals),
|
|
3056
|
+
spanPlacedInBand: mergeSet(best.spanPlacedInBand, next.spanPlacedInBand),
|
|
3057
|
+
bandCapsApplied: mergeSet(best.bandCapsApplied, next.bandCapsApplied),
|
|
3058
|
+
looseOutcome: mergeMap(best.looseOutcome, next.looseOutcome),
|
|
3059
|
+
};
|
|
3060
|
+
};
|
|
2797
3061
|
/**
|
|
2798
|
-
* A rejected
|
|
2799
|
-
* split paragraph whose head no longer fits, a float that
|
|
2800
|
-
* a lead-in that left with its list…):
|
|
2801
|
-
* re-
|
|
2802
|
-
* levers are meant to be local, so contain the damage: find
|
|
2803
|
-
* column whose content changed and blacklist
|
|
2804
|
-
* applied there (failing that, on its page;
|
|
2805
|
-
*
|
|
2806
|
-
*
|
|
3062
|
+
* A rejected segment moved content across a column break somewhere in
|
|
3063
|
+
* its pages (a split paragraph whose head no longer fits, a float that
|
|
3064
|
+
* lost its slot, a lead-in that left with its list…): the pages after
|
|
3065
|
+
* that point re-flow, gaps open elsewhere and a span cap may miss its
|
|
3066
|
+
* band. The levers are meant to be local, so contain the damage: find
|
|
3067
|
+
* the first column of the segment whose content changed and blacklist
|
|
3068
|
+
* the levers this pass newly applied there (failing that, on its page;
|
|
3069
|
+
* failing that, in the whole segment), so the next proposal keeps the
|
|
3070
|
+
* working levers before it and tries again without the one that
|
|
3071
|
+
* cascaded. Returns whether anything was blacklisted.
|
|
2807
3072
|
*/
|
|
2808
|
-
const containCascade = (next,
|
|
2809
|
-
const div = firstDivergentColumn(best.doc, next.doc);
|
|
3073
|
+
const containCascade = (next, s, newLines, newLoose, gaps) => {
|
|
3074
|
+
const div = firstDivergentColumn(best.doc, next.doc, s.range);
|
|
2810
3075
|
if (!div)
|
|
2811
3076
|
return false;
|
|
2812
|
-
const newLines = [...proposal.lines].filter(([k, n]) => n > (applied.lines.get(k) ?? 0)).map(([k]) => k);
|
|
2813
|
-
const newLoose = [...proposal.loose.keys()].filter((k) => !applied.loose.has(k));
|
|
2814
3077
|
if (newLines.length === 0 && newLoose.length === 0)
|
|
2815
3078
|
return false;
|
|
2816
|
-
const gaps = collectColumnGaps(best.doc, best.forcedBreakPages);
|
|
2817
3079
|
const blacklist = (cands) => {
|
|
2818
3080
|
let hit = false;
|
|
2819
3081
|
for (const k of newLines)
|
|
2820
3082
|
if (!cands || cands.has(k)) {
|
|
2821
|
-
failedLines.add(k);
|
|
3083
|
+
s.failedLines.add(k);
|
|
2822
3084
|
hit = true;
|
|
2823
3085
|
}
|
|
2824
3086
|
for (const k of newLoose)
|
|
2825
3087
|
if (!cands || cands.has(k)) {
|
|
2826
|
-
failedLoose.add(k);
|
|
3088
|
+
s.failedLoose.add(k);
|
|
2827
3089
|
hit = true;
|
|
2828
3090
|
}
|
|
2829
3091
|
return hit;
|
|
2830
3092
|
};
|
|
2831
|
-
const
|
|
2832
|
-
|
|
2833
|
-
.flatMap((g) => g.candidates.map((c) => balanceKey(c.contentIndex, c.part ?? 0)));
|
|
2834
|
-
if (blacklist(new Set(inColumn)))
|
|
3093
|
+
const keysOf = (pick) => new Set(gaps.filter(pick).flatMap((g) => g.candidates.map((c) => balanceKey(c.contentIndex, c.part ?? 0))));
|
|
3094
|
+
if (blacklist(keysOf((g) => g.pageIndex === div.pageIndex && g.columnIndex === div.columnIndex)))
|
|
2835
3095
|
return true;
|
|
2836
|
-
|
|
2837
|
-
.filter((g) => g.pageIndex === div.pageIndex)
|
|
2838
|
-
.flatMap((g) => g.candidates.map((c) => balanceKey(c.contentIndex, c.part ?? 0)));
|
|
2839
|
-
if (blacklist(new Set(onPage)))
|
|
3096
|
+
if (blacklist(keysOf((g) => g.pageIndex === div.pageIndex)))
|
|
2840
3097
|
return true;
|
|
2841
3098
|
return blacklist(null);
|
|
2842
3099
|
};
|
|
3100
|
+
let balancingPasses = 0;
|
|
2843
3101
|
const balance = () => {
|
|
2844
|
-
while (
|
|
3102
|
+
while (balancingPasses < MAX_BALANCING_PASSES_PER_DOCUMENT) {
|
|
3103
|
+
const active = segments.filter((s) => !s.done);
|
|
3104
|
+
if (active.length === 0)
|
|
3105
|
+
break;
|
|
3106
|
+
const gaps = collectColumnGaps(best.doc, best.forcedBreakPages);
|
|
3107
|
+
const owner = keyOwners(gaps);
|
|
3108
|
+
const failedLoose = new Set();
|
|
3109
|
+
const failedLines = new Set();
|
|
3110
|
+
for (const s of segments) {
|
|
3111
|
+
for (const k of s.failedLoose)
|
|
3112
|
+
failedLoose.add(k);
|
|
3113
|
+
for (const k of s.failedLines)
|
|
3114
|
+
failedLines.add(k);
|
|
3115
|
+
}
|
|
2845
3116
|
const proposal = proposeBalanceLines(best.doc, best.forcedBreakPages, applied, {
|
|
2846
3117
|
maxLinesPerHeading: balancing.maxLinesPerHeading,
|
|
2847
3118
|
stretchAfterLists: balancing.stretchAfterLists,
|
|
@@ -2854,67 +3125,114 @@ export function buildDocument(content, config, cache, options) {
|
|
|
2854
3125
|
failedLoose,
|
|
2855
3126
|
failedLines,
|
|
2856
3127
|
});
|
|
2857
|
-
|
|
3128
|
+
// The levers newly proposed, by segment; those of a segment that is
|
|
3129
|
+
// done (plateaued, out of attempts) are withdrawn from the pass.
|
|
3130
|
+
const newLines = [];
|
|
3131
|
+
const newLoose = [];
|
|
3132
|
+
for (const [k, n] of proposal.lines) {
|
|
3133
|
+
const cur = applied.lines.get(k) ?? 0;
|
|
3134
|
+
if (n <= cur)
|
|
3135
|
+
continue;
|
|
3136
|
+
const si = owner.get(k);
|
|
3137
|
+
if (si === undefined || segments[si].done) {
|
|
3138
|
+
if (cur > 0)
|
|
3139
|
+
proposal.lines.set(k, cur);
|
|
3140
|
+
else
|
|
3141
|
+
proposal.lines.delete(k);
|
|
3142
|
+
continue;
|
|
3143
|
+
}
|
|
3144
|
+
newLines.push(k);
|
|
3145
|
+
}
|
|
3146
|
+
for (const k of [...proposal.loose.keys()]) {
|
|
3147
|
+
if (applied.loose.has(k))
|
|
3148
|
+
continue;
|
|
3149
|
+
const si = owner.get(k);
|
|
3150
|
+
if (si === undefined || segments[si].done) {
|
|
3151
|
+
proposal.loose.delete(k);
|
|
3152
|
+
proposal.looseBudget.delete(k);
|
|
3153
|
+
continue;
|
|
3154
|
+
}
|
|
3155
|
+
newLoose.push(k);
|
|
3156
|
+
}
|
|
3157
|
+
const trying = new Set([...newLines, ...newLoose].map((k) => owner.get(k)));
|
|
3158
|
+
if (trying.size === 0) {
|
|
2858
3159
|
// No stretch point can absorb the remaining gaps — stable.
|
|
2859
|
-
|
|
3160
|
+
for (const s of active) {
|
|
3161
|
+
s.done = true;
|
|
3162
|
+
s.stable = true;
|
|
3163
|
+
}
|
|
2860
3164
|
break;
|
|
2861
3165
|
}
|
|
2862
|
-
const
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
const hints = {
|
|
2866
|
-
balanceExtraPx: extraPx,
|
|
2867
|
-
balanceLooseness: proposal.loose,
|
|
2868
|
-
balanceLooseBudget: proposal.looseBudget,
|
|
2869
|
-
bandCaps,
|
|
2870
|
-
};
|
|
2871
|
-
const next = runPass(hints);
|
|
3166
|
+
for (const si of trying)
|
|
3167
|
+
segments[si].attempts++;
|
|
3168
|
+
const next = runPass(hintsFrom(proposal.lines, proposal.loose, proposal.looseBudget));
|
|
2872
3169
|
passCount++;
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
const looseFailed = newlyLoose.filter((k) => next.looseOutcome.get(k) === null);
|
|
2885
|
-
for (const k of looseFailed)
|
|
2886
|
-
failedLoose.add(k);
|
|
2887
|
-
const looseWon = newlyLoose.filter((k) => typeof next.looseOutcome.get(k) === 'number');
|
|
2888
|
-
if (score < bestScore) {
|
|
2889
|
-
best = next;
|
|
2890
|
-
bestHints = hints;
|
|
2891
|
-
bestScore = score;
|
|
2892
|
-
applied = {
|
|
2893
|
-
lines: proposal.lines,
|
|
2894
|
-
loose: new Map([...proposal.loose].filter(([k]) => applied.loose.has(k) || looseWon.includes(k))),
|
|
2895
|
-
};
|
|
2896
|
-
converged = score === 0;
|
|
2897
|
-
}
|
|
2898
|
-
else {
|
|
2899
|
-
// Plateau or regression. First contain a cascade: a lever that
|
|
2900
|
-
// moved content across a column break is blacklisted and the loop
|
|
2901
|
-
// retries without it. Otherwise retry when a loose candidate was
|
|
2902
|
-
// just blacklisted (the proposer falls through to the next one), or
|
|
2903
|
-
// when the new loose paragraphs gained their lines yet the layout
|
|
2904
|
-
// did not improve (the gain landed elsewhere — drop them too). A
|
|
2905
|
-
// pure spacing plateau means we're done: keep the best layout found
|
|
2906
|
-
// so far.
|
|
2907
|
-
if (containCascade(next, proposal))
|
|
3170
|
+
balancingPasses++;
|
|
3171
|
+
const nextGaps = collectColumnGaps(next.doc, next.forcedBreakPages);
|
|
3172
|
+
const capPage = pageOfContent(best.doc);
|
|
3173
|
+
const accepted = [];
|
|
3174
|
+
for (const si of trying) {
|
|
3175
|
+
const s = segments[si];
|
|
3176
|
+
// An earlier segment's cascade shifted this one's pages: the pass
|
|
3177
|
+
// says nothing about its levers. They are offered again once the
|
|
3178
|
+
// culprit is blacklisted.
|
|
3179
|
+
if (!startIntact(next, s)) {
|
|
3180
|
+
s.attempts--;
|
|
2908
3181
|
continue;
|
|
2909
|
-
|
|
3182
|
+
}
|
|
3183
|
+
const keysLines = newLines.filter((k) => owner.get(k) === si);
|
|
3184
|
+
const keysLoose = newLoose.filter((k) => owner.get(k) === si);
|
|
3185
|
+
// Band caps ride along unchanged; a retry that unsettles one of the
|
|
3186
|
+
// segment's (its span block no longer lands in the capped band, or
|
|
3187
|
+
// a levelled closing band spills past its cut) is a regression —
|
|
3188
|
+
// capped columns without their box are not a layout we may keep.
|
|
3189
|
+
const capsDelivered = [...bandCaps.keys()].every((i) => !inRange(s.range, capPage.get(i)) || next.spanPlacedInBand.has(i));
|
|
3190
|
+
const score = capsDelivered && wholeIntact(next, s, si === segments.length - 1)
|
|
3191
|
+
? gapLinesIn(nextGaps, s.range)
|
|
3192
|
+
: Infinity;
|
|
3193
|
+
// Loose paragraphs that gained no line at any tracking rung are
|
|
3194
|
+
// blacklisted whatever the score did, and never counted as applied.
|
|
3195
|
+
// Candidates the pass never tried (their column's budget was met
|
|
3196
|
+
// first) stay eligible for a later proposal.
|
|
3197
|
+
const looseFailed = keysLoose.filter((k) => next.looseOutcome.get(k) === null);
|
|
3198
|
+
for (const k of looseFailed)
|
|
3199
|
+
s.failedLoose.add(k);
|
|
3200
|
+
const looseWon = keysLoose.filter((k) => typeof next.looseOutcome.get(k) === 'number');
|
|
3201
|
+
if (score < s.bestScore) {
|
|
3202
|
+
for (const k of keysLines)
|
|
3203
|
+
applied.lines.set(k, proposal.lines.get(k));
|
|
2910
3204
|
for (const k of looseWon)
|
|
2911
|
-
|
|
2912
|
-
|
|
3205
|
+
applied.loose.set(k, proposal.loose.get(k));
|
|
3206
|
+
s.bestScore = score;
|
|
3207
|
+
if (score === 0) {
|
|
3208
|
+
s.done = true;
|
|
3209
|
+
s.stable = true;
|
|
3210
|
+
}
|
|
3211
|
+
accepted.push(s.range);
|
|
2913
3212
|
}
|
|
2914
|
-
|
|
3213
|
+
else if (!containCascade(next, s, keysLines, keysLoose, gaps)) {
|
|
3214
|
+
// Plateau or regression without a cascade to contain: retry when
|
|
3215
|
+
// a loose candidate was just blacklisted (the proposer falls
|
|
3216
|
+
// through to the next one), or when the new loose paragraphs
|
|
3217
|
+
// gained their lines yet the segment did not improve (the gain
|
|
3218
|
+
// landed elsewhere — drop them too). A pure spacing plateau means
|
|
3219
|
+
// the segment is done: it keeps the best layout found so far.
|
|
3220
|
+
if (looseFailed.length > 0 || looseWon.length > 0) {
|
|
3221
|
+
for (const k of looseWon)
|
|
3222
|
+
s.failedLoose.add(k);
|
|
3223
|
+
}
|
|
3224
|
+
else {
|
|
3225
|
+
s.done = true;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
if (!s.done && s.attempts >= MAX_BALANCING_PASSES)
|
|
3229
|
+
s.done = true;
|
|
2915
3230
|
}
|
|
3231
|
+
if (accepted.length > 0)
|
|
3232
|
+
best = spliceSegments(next, accepted);
|
|
2916
3233
|
}
|
|
2917
3234
|
};
|
|
3235
|
+
resetSegments();
|
|
2918
3236
|
balance();
|
|
2919
3237
|
// --- Trailing bands (closing columns cut level) -------------------------
|
|
2920
3238
|
// Once the balancing levers have settled the earlier pages, level the
|
|
@@ -2925,20 +3243,19 @@ export function buildDocument(content, config, cache, options) {
|
|
|
2925
3243
|
// block and the cap applies. A short polish round then lets the levers
|
|
2926
3244
|
// fill what the cut left short (a column ending a line under the cap).
|
|
2927
3245
|
if (balancing.trailing) {
|
|
2928
|
-
const
|
|
3246
|
+
const frozen = hintsFrom(applied.lines, applied.loose);
|
|
3247
|
+
const trailing = resolveTrailingCaps(best, bandCaps, (caps) => runPass({ ...frozen, bandCaps: caps }));
|
|
2929
3248
|
passCount += trailing.passCount;
|
|
2930
3249
|
if (trailing.result !== best) {
|
|
2931
3250
|
best = trailing.result;
|
|
2932
3251
|
for (const [i, cap] of trailing.caps)
|
|
2933
3252
|
bandCaps.set(i, cap);
|
|
2934
|
-
|
|
2935
|
-
bestScore = totalGapLines(best.doc, best.forcedBreakPages);
|
|
2936
|
-
converged = bestScore === 0;
|
|
3253
|
+
resetSegments();
|
|
2937
3254
|
balance();
|
|
2938
3255
|
}
|
|
2939
3256
|
}
|
|
2940
3257
|
best.doc.iterationCount = passCount;
|
|
2941
|
-
best.doc.converged =
|
|
3258
|
+
best.doc.converged = segments.every((s) => s.stable || s.bestScore === 0);
|
|
2942
3259
|
return best.doc;
|
|
2943
3260
|
}
|
|
2944
3261
|
//# sourceMappingURL=build.js.map
|