postext 0.3.15 → 0.3.17
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__/createLayout.test.js +12 -13
- package/dist/__tests__/createLayout.test.js.map +1 -1
- package/dist/__tests__/defaults/resourceTypes.test.d.ts +2 -0
- package/dist/__tests__/defaults/resourceTypes.test.d.ts.map +1 -0
- package/dist/__tests__/defaults/resourceTypes.test.js +69 -0
- package/dist/__tests__/defaults/resourceTypes.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +54 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/parse/inlineRef.test.d.ts +2 -0
- package/dist/__tests__/parse/inlineRef.test.d.ts.map +1 -0
- package/dist/__tests__/parse/inlineRef.test.js +83 -0
- package/dist/__tests__/parse/inlineRef.test.js.map +1 -0
- package/dist/__tests__/parse/resourceDirective.test.d.ts +2 -0
- package/dist/__tests__/parse/resourceDirective.test.d.ts.map +1 -0
- package/dist/__tests__/parse/resourceDirective.test.js +55 -0
- package/dist/__tests__/parse/resourceDirective.test.js.map +1 -0
- package/dist/__tests__/pipeline/floatPlacement.test.d.ts +2 -0
- package/dist/__tests__/pipeline/floatPlacement.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/floatPlacement.test.js +141 -0
- package/dist/__tests__/pipeline/floatPlacement.test.js.map +1 -0
- package/dist/__tests__/pipeline/inlineRefRender.test.d.ts +2 -0
- package/dist/__tests__/pipeline/inlineRefRender.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/inlineRefRender.test.js +107 -0
- package/dist/__tests__/pipeline/inlineRefRender.test.js.map +1 -0
- package/dist/__tests__/pipeline/resourceNumbering.test.d.ts +2 -0
- package/dist/__tests__/pipeline/resourceNumbering.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/resourceNumbering.test.js +186 -0
- package/dist/__tests__/pipeline/resourceNumbering.test.js.map +1 -0
- package/dist/__tests__/table/model.test.d.ts +2 -0
- package/dist/__tests__/table/model.test.d.ts.map +1 -0
- package/dist/__tests__/table/model.test.js +187 -0
- package/dist/__tests__/table/model.test.js.map +1 -0
- package/dist/canvas-backend/blockRender.d.ts.map +1 -1
- package/dist/canvas-backend/blockRender.js +25 -6
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/headerFooter.d.ts +3 -2
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +63 -2
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/canvas-backend/index.d.ts +2 -0
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +11 -0
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts +28 -0
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -0
- package/dist/canvas-backend/renderResourceBlock.js +146 -0
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -0
- package/dist/defaults/bodyText.d.ts.map +1 -1
- package/dist/defaults/bodyText.js +30 -0
- package/dist/defaults/bodyText.js.map +1 -1
- package/dist/defaults/captionStyle.d.ts +9 -0
- package/dist/defaults/captionStyle.d.ts.map +1 -0
- package/dist/defaults/captionStyle.js +74 -0
- package/dist/defaults/captionStyle.js.map +1 -0
- package/dist/defaults/debug.d.ts.map +1 -1
- package/dist/defaults/debug.js +6 -0
- package/dist/defaults/debug.js.map +1 -1
- package/dist/defaults/headerFooter.d.ts +20 -18
- package/dist/defaults/headerFooter.d.ts.map +1 -1
- package/dist/defaults/headerFooter.js +269 -165
- package/dist/defaults/headerFooter.js.map +1 -1
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +29 -6
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/index.d.ts +3 -0
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +19 -0
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/resourceTypes.d.ts +8 -0
- package/dist/defaults/resourceTypes.d.ts.map +1 -0
- package/dist/defaults/resourceTypes.js +43 -0
- package/dist/defaults/resourceTypes.js.map +1 -0
- package/dist/defaults/shared.d.ts.map +1 -1
- package/dist/defaults/shared.js +30 -0
- package/dist/defaults/shared.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts +11 -0
- package/dist/defaults/tableStyle.d.ts.map +1 -0
- package/dist/defaults/tableStyle.js +114 -0
- package/dist/defaults/tableStyle.js.map +1 -0
- package/dist/design/layout.d.ts +94 -0
- package/dist/design/layout.d.ts.map +1 -0
- package/dist/design/layout.js +642 -0
- package/dist/design/layout.js.map +1 -0
- package/dist/design/placeholders.d.ts +29 -0
- package/dist/design/placeholders.d.ts.map +1 -0
- package/dist/design/placeholders.js +126 -0
- package/dist/design/placeholders.js.map +1 -0
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +91 -1
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/richAdapter.d.ts +1 -0
- package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/richAdapter.js +2 -1
- package/dist/knuthPlass/richAdapter.js.map +1 -1
- package/dist/measure/cache.js +1 -1
- package/dist/measure/cache.js.map +1 -1
- package/dist/measure/rich.d.ts +7 -0
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +30 -0
- package/dist/measure/rich.js.map +1 -1
- package/dist/numbering.d.ts +16 -0
- package/dist/numbering.d.ts.map +1 -1
- package/dist/numbering.js +28 -18
- package/dist/numbering.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +37 -9
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/inlineFormatting.d.ts +38 -0
- package/dist/parse/inlineFormatting.d.ts.map +1 -1
- package/dist/parse/inlineFormatting.js +84 -0
- package/dist/parse/inlineFormatting.js.map +1 -1
- package/dist/parse/sourceMapping.d.ts.map +1 -1
- package/dist/parse/sourceMapping.js +20 -0
- package/dist/parse/sourceMapping.js.map +1 -1
- package/dist/parse/types.d.ts +20 -1
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +389 -17
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildBlockKind.d.ts +14 -0
- package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
- package/dist/pipeline/buildBlockKind.js +16 -1
- package/dist/pipeline/buildBlockKind.js.map +1 -1
- package/dist/pipeline/buildHelpers.d.ts.map +1 -1
- package/dist/pipeline/buildHelpers.js +7 -1
- package/dist/pipeline/buildHelpers.js.map +1 -1
- package/dist/pipeline/buildMeasurement.d.ts +17 -1
- package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
- package/dist/pipeline/buildMeasurement.js +32 -0
- package/dist/pipeline/buildMeasurement.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/floatPlacement.d.ts +45 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -0
- package/dist/pipeline/floatPlacement.js +68 -0
- package/dist/pipeline/floatPlacement.js.map +1 -0
- package/dist/pipeline/headerFooter.d.ts +23 -7
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +258 -100
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/placeholders.d.ts +6 -0
- package/dist/pipeline/placeholders.d.ts.map +1 -1
- package/dist/pipeline/placeholders.js +46 -0
- package/dist/pipeline/placeholders.js.map +1 -1
- package/dist/pipeline/placement.d.ts +15 -2
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +38 -3
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +58 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -0
- package/dist/pipeline/resourceLayout.js +338 -0
- package/dist/pipeline/resourceLayout.js.map +1 -0
- package/dist/pipeline/resourceNumbering.d.ts +54 -0
- package/dist/pipeline/resourceNumbering.d.ts.map +1 -0
- package/dist/pipeline/resourceNumbering.js +218 -0
- package/dist/pipeline/resourceNumbering.js.map +1 -0
- package/dist/pipeline/styles.d.ts +6 -0
- package/dist/pipeline/styles.d.ts.map +1 -1
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/styles.js.map +1 -1
- package/dist/table/model.d.ts +53 -0
- package/dist/table/model.d.ts.map +1 -0
- package/dist/table/model.js +253 -0
- package/dist/table/model.js.map +1 -0
- package/dist/types.d.ts +397 -41
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +165 -18
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dimensionToPx } from '../units';
|
|
2
|
-
import { createVDTDocument, createVDTBlock, } from '../vdt';
|
|
2
|
+
import { createVDTDocument, createVDTBlock, createBoundingBox, } from '../vdt';
|
|
3
3
|
import { parseMarkdownMemo } from '../parse';
|
|
4
4
|
import { buildPageLabels, computeHeadingNumbers, } from '../numbering';
|
|
5
5
|
import { extractFrontmatter } from '../frontmatter';
|
|
@@ -7,12 +7,16 @@ import { initHyphenator } from '../measure';
|
|
|
7
7
|
import { resolveAllConfig, computeBaselineGrid } from './config';
|
|
8
8
|
import { resolveBodyStyle, resolveBlockquoteStyle } from './styles';
|
|
9
9
|
import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, } from './lists';
|
|
10
|
-
import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, } from './placement';
|
|
10
|
+
import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeResourceBlock, } from './placement';
|
|
11
11
|
import { chooseParagraphSplit } from './orphanWidow';
|
|
12
12
|
import { applyStyleAttrs, computeMeasureViewport, computePageMetrics, enrichMathSpans, rollbackTrailingBlocks, stampSourceRanges, } from './buildHelpers';
|
|
13
13
|
import { resolveBlockKind } from './buildBlockKind';
|
|
14
14
|
import { runMeasurement } from './buildMeasurement';
|
|
15
|
-
import {
|
|
15
|
+
import { resolveRefSpans, layoutResourceBlock } from './resourceLayout';
|
|
16
|
+
import { computeFloatPlan, floatedResourceIds, } from './floatPlacement';
|
|
17
|
+
import { computeHeadingContext, computeResourceNumbering, } from './resourceNumbering';
|
|
18
|
+
import { defaultResourceTypes } from '../defaults/resourceTypes';
|
|
19
|
+
import { buildHeadersAndFooters, measureHeadingAdvancedDesignHeight } from './headerFooter';
|
|
16
20
|
export class BuildCancelledError extends Error {
|
|
17
21
|
constructor() {
|
|
18
22
|
super('Build cancelled');
|
|
@@ -46,12 +50,209 @@ export function buildDocument(content, config, cache, options) {
|
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
const headingPrefixes = computeHeadingNumbers(contentBlocks, headingTemplates);
|
|
53
|
+
// Resource numbering — computed up front (before the placement loop) so that
|
|
54
|
+
// captions and inline `:ref`s can resolve their rendered number strings
|
|
55
|
+
// before measurement. Numbering follows order of first reference in the
|
|
56
|
+
// document.
|
|
57
|
+
const resourceTypes = config?.resourceTypes ?? defaultResourceTypes();
|
|
58
|
+
const resources = content.resources ?? [];
|
|
59
|
+
const headingContext = computeHeadingContext(contentBlocks);
|
|
60
|
+
const resourceNumbering = computeResourceNumbering(contentBlocks, resourceTypes, resources, headingContext);
|
|
61
|
+
// Lookups threaded into block-kind resolution + measurement.
|
|
62
|
+
const resourceById = new Map();
|
|
63
|
+
for (const r of resources)
|
|
64
|
+
resourceById.set(r.id, r);
|
|
65
|
+
const resourceTypeById = new Map();
|
|
66
|
+
for (const t of resourceTypes)
|
|
67
|
+
resourceTypeById.set(t.id, t);
|
|
68
|
+
const resourceNumberById = new Map();
|
|
69
|
+
for (const [id, entry] of Object.entries(resourceNumbering)) {
|
|
70
|
+
resourceNumberById.set(id, entry.number);
|
|
71
|
+
}
|
|
49
72
|
// Resolve styles
|
|
50
73
|
const bodyStyle = resolveBodyStyle(resolved);
|
|
51
74
|
const blockquoteStyle = resolveBlockquoteStyle(resolved);
|
|
52
75
|
const listLevelIndentsPx = computeLevelIndentsPx(resolved, bodyStyle.fontSizePx);
|
|
53
76
|
const orderedMetrics = computeOrderedListRunMetrics(contentBlocks, resolved, bodyStyle.fontSizePx);
|
|
54
77
|
const orderedLevelIndentsPx = computeOrderedLevelIndentsPx(resolved, bodyStyle.fontSizePx, orderedMetrics.maxWidthByDepth);
|
|
78
|
+
// --- Float planning (issue #49 — resources float to page bands) ----------
|
|
79
|
+
// A resource is incorporated by its first reference (an inline `:ref` or a
|
|
80
|
+
// `::resource` directive, whichever comes first in reading order). Floated
|
|
81
|
+
// resources detach from the running text and reserve a band at the top or
|
|
82
|
+
// bottom of the next page opened after that reference; the text flows past
|
|
83
|
+
// the reference uninterrupted. `position: 'here'` resources keep inline
|
|
84
|
+
// `::resource` placement and are not floated.
|
|
85
|
+
const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes);
|
|
86
|
+
const floatedIds = floatedResourceIds(floatPlan);
|
|
87
|
+
const floatsByFirstBlock = new Map();
|
|
88
|
+
for (const f of floatPlan) {
|
|
89
|
+
const list = floatsByFirstBlock.get(f.firstBlockIdx);
|
|
90
|
+
if (list)
|
|
91
|
+
list.push(f);
|
|
92
|
+
else
|
|
93
|
+
floatsByFirstBlock.set(f.firstBlockIdx, [f]);
|
|
94
|
+
}
|
|
95
|
+
// Floats whose first reference has been passed but which are not yet placed
|
|
96
|
+
// into a page band, in reading order.
|
|
97
|
+
const pendingFloats = [];
|
|
98
|
+
const floatGapPx = bodyStyle.lineHeightPx;
|
|
99
|
+
const minTextPx = bodyStyle.lineHeightPx * 3;
|
|
100
|
+
/** Offset a resolved resource block's caption/table geometry from
|
|
101
|
+
* block-relative to absolute page coordinates (mirrors inline placement). */
|
|
102
|
+
const offsetResourceBlockToAbsolute = (rb, ox, oy) => {
|
|
103
|
+
for (const ln of rb.captionLines) {
|
|
104
|
+
ln.bbox.x += ox;
|
|
105
|
+
ln.bbox.y += oy;
|
|
106
|
+
ln.baseline += oy;
|
|
107
|
+
}
|
|
108
|
+
if (rb.table) {
|
|
109
|
+
for (const cell of rb.table.cells) {
|
|
110
|
+
cell.rect.x += ox;
|
|
111
|
+
cell.rect.y += oy;
|
|
112
|
+
for (const cl of cell.lines) {
|
|
113
|
+
cl.bbox.x += ox;
|
|
114
|
+
cl.bbox.y += oy;
|
|
115
|
+
cl.baseline += oy;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/** Measure + build a float block at horizontal offset `x` (y = 0), or null
|
|
121
|
+
* when the resource id is unknown. Caller offsets it to its final `y`. */
|
|
122
|
+
const buildFloatBlock = (resourceId, x, width) => {
|
|
123
|
+
const resource = resourceById.get(resourceId);
|
|
124
|
+
if (!resource)
|
|
125
|
+
return null;
|
|
126
|
+
const { block: rb, totalHeight } = layoutResourceBlock({
|
|
127
|
+
resource,
|
|
128
|
+
resourceType: resourceTypeById.get(resource.typeId),
|
|
129
|
+
number: resourceNumberById.get(resourceId) ?? '',
|
|
130
|
+
resolved,
|
|
131
|
+
columnWidth: width,
|
|
132
|
+
resourceNumbering,
|
|
133
|
+
resourceTypes,
|
|
134
|
+
resources,
|
|
135
|
+
});
|
|
136
|
+
const blk = createVDTBlock(`float-${resourceId}`, 'resource', bodyStyle.fontString, bodyStyle.color, bodyStyle.textAlign);
|
|
137
|
+
blk.resourceBlock = rb;
|
|
138
|
+
blk.dirty = false;
|
|
139
|
+
blk.snappedToGrid = false;
|
|
140
|
+
blk.bbox = createBoundingBox(x, 0, width, totalHeight);
|
|
141
|
+
blk.lines = [];
|
|
142
|
+
offsetResourceBlockToAbsolute(rb, x, 0);
|
|
143
|
+
return { block: blk, height: totalHeight };
|
|
144
|
+
};
|
|
145
|
+
/** Reserve top/bottom bands on a freshly opened page and position as many
|
|
146
|
+
* pending floats as fit, shrinking the affected columns so body text flows
|
|
147
|
+
* around them. Preserves reading order: stops at the first float that does
|
|
148
|
+
* not fit (so figures never reorder relative to their references), except
|
|
149
|
+
* on a band that is still all-text, where a dominating/oversized float is
|
|
150
|
+
* force-placed so the queue always makes progress. */
|
|
151
|
+
const flushFloatsIntoPage = (page) => {
|
|
152
|
+
if (pendingFloats.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
const topUsed = page.columns.map(() => 0);
|
|
155
|
+
const botUsed = page.columns.map(() => 0);
|
|
156
|
+
const floats = page.floats ?? [];
|
|
157
|
+
/** Try to place one float on this page. Returns whether it was placed,
|
|
158
|
+
* must be deferred (does not fit), or skipped (unknown id). Only mutates
|
|
159
|
+
* page geometry when it actually places. */
|
|
160
|
+
const attemptFloat = (f) => {
|
|
161
|
+
const pageSpan = f.span === 'page' && page.columns.length > 1;
|
|
162
|
+
let targetCols;
|
|
163
|
+
if (pageSpan) {
|
|
164
|
+
targetCols = page.columns.map((_, i) => i);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// Single-column float: pick the column with the most room left.
|
|
168
|
+
let best = 0;
|
|
169
|
+
for (let i = 1; i < page.columns.length; i++) {
|
|
170
|
+
if (topUsed[i] + botUsed[i] < topUsed[best] + botUsed[best])
|
|
171
|
+
best = i;
|
|
172
|
+
}
|
|
173
|
+
targetCols = [best];
|
|
174
|
+
}
|
|
175
|
+
const firstCol = page.columns[targetCols[0]];
|
|
176
|
+
const width = pageSpan ? contentArea.width : firstCol.bbox.width;
|
|
177
|
+
const xLeft = pageSpan ? contentArea.x : firstCol.bbox.x;
|
|
178
|
+
const built = buildFloatBlock(f.resourceId, xLeft, width);
|
|
179
|
+
if (!built)
|
|
180
|
+
return 'skip';
|
|
181
|
+
const need = built.height + floatGapPx;
|
|
182
|
+
let minAvail = Infinity;
|
|
183
|
+
let anyReserved = false;
|
|
184
|
+
for (const c of targetCols) {
|
|
185
|
+
minAvail = Math.min(minAvail, page.columns[c].availableHeight);
|
|
186
|
+
if (topUsed[c] > 0 || botUsed[c] > 0)
|
|
187
|
+
anyReserved = true;
|
|
188
|
+
}
|
|
189
|
+
// Keep some text room, unless this band is still all-text (then a
|
|
190
|
+
// dominating / oversized float is force-placed so the queue progresses).
|
|
191
|
+
if (need > minAvail - minTextPx && anyReserved)
|
|
192
|
+
return 'defer';
|
|
193
|
+
let y = 0;
|
|
194
|
+
for (const c of targetCols) {
|
|
195
|
+
const col = page.columns[c];
|
|
196
|
+
if (f.position === 'top') {
|
|
197
|
+
y = col.bbox.y; // float sits at the current top edge
|
|
198
|
+
col.bbox.y += need; // push column content below the band
|
|
199
|
+
col.bbox.height = Math.max(0, col.bbox.height - need);
|
|
200
|
+
topUsed[c] += need;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
col.bbox.height = Math.max(0, col.bbox.height - need);
|
|
204
|
+
y = col.bbox.y + col.bbox.height + floatGapPx; // flush toward the bottom
|
|
205
|
+
botUsed[c] += need;
|
|
206
|
+
}
|
|
207
|
+
col.availableHeight = Math.max(0, col.availableHeight - need);
|
|
208
|
+
}
|
|
209
|
+
offsetResourceBlockToAbsolute(built.block.resourceBlock, 0, y);
|
|
210
|
+
built.block.bbox = createBoundingBox(xLeft, y, width, built.height);
|
|
211
|
+
built.block.pageIndex = page.index;
|
|
212
|
+
built.block.columnIndex = targetCols[0];
|
|
213
|
+
floats.push(built.block);
|
|
214
|
+
return 'placed';
|
|
215
|
+
};
|
|
216
|
+
// Full-width (page-span) floats reserve the outermost bands first, so a
|
|
217
|
+
// later single-column float nests inside the remaining column space rather
|
|
218
|
+
// than overlapping a full-width band. Within each pass, stop at the first
|
|
219
|
+
// float that does not fit to preserve reading order.
|
|
220
|
+
for (const pageSpanPass of [true, false]) {
|
|
221
|
+
let i = 0;
|
|
222
|
+
while (i < pendingFloats.length) {
|
|
223
|
+
const f = pendingFloats[i];
|
|
224
|
+
const isPageSpan = f.span === 'page' && page.columns.length > 1;
|
|
225
|
+
if (isPageSpan !== pageSpanPass) {
|
|
226
|
+
i++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const r = attemptFloat(f);
|
|
230
|
+
if (r === 'placed' || r === 'skip')
|
|
231
|
+
pendingFloats.splice(i, 1);
|
|
232
|
+
else
|
|
233
|
+
break; // defer: leave this and the rest of the pass for a later page
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (floats.length > 0)
|
|
237
|
+
page.floats = floats;
|
|
238
|
+
};
|
|
239
|
+
/** Drain floats still pending after body placement (referenced on the last
|
|
240
|
+
* page, or never followed by a content-overflow page break) onto freshly
|
|
241
|
+
* appended pages. Each new page force-places at least one float. */
|
|
242
|
+
const finalizeFloats = () => {
|
|
243
|
+
let guard = 0;
|
|
244
|
+
while (pendingFloats.length > 0 && guard++ < 1000) {
|
|
245
|
+
const before = pendingFloats.length;
|
|
246
|
+
const page = createPageWithColumns(doc.pages.length, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
247
|
+
doc.pages.push(page);
|
|
248
|
+
flushFloatsIntoPage(page);
|
|
249
|
+
if (pendingFloats.length === before)
|
|
250
|
+
break; // safety: no progress
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
/** Reserve floats on each freshly opened content page. Passed only to the
|
|
254
|
+
* content-flow column advances — parity / force-blank pages never get it. */
|
|
255
|
+
const onNewPage = (page) => flushFloatsIntoPage(page);
|
|
55
256
|
// Placement cursor
|
|
56
257
|
const cursor = { pageIndex: 0, columnIndex: 0 };
|
|
57
258
|
let blockIdCounter = 0;
|
|
@@ -93,6 +294,13 @@ export function buildDocument(content, config, cache, options) {
|
|
|
93
294
|
if (options?.shouldCancel?.())
|
|
94
295
|
throw new BuildCancelledError();
|
|
95
296
|
const rawBlock = contentBlocks[blockIdx];
|
|
297
|
+
// Enqueue floats first-referenced in this block so the next page opened
|
|
298
|
+
// while placing it (or any later block) reserves their band. Done before
|
|
299
|
+
// placement so a reference near a column/page boundary still floats onto
|
|
300
|
+
// the page that follows it.
|
|
301
|
+
const floatsHere = floatsByFirstBlock.get(blockIdx);
|
|
302
|
+
if (floatsHere)
|
|
303
|
+
pendingFloats.push(...floatsHere);
|
|
96
304
|
// --- Directives ----------------------------------------------------
|
|
97
305
|
if (rawBlock.type === 'directive') {
|
|
98
306
|
const name = rawBlock.directiveName;
|
|
@@ -136,6 +344,16 @@ export function buildDocument(content, config, cache, options) {
|
|
|
136
344
|
}
|
|
137
345
|
flushPendingNumberingAtBoundary();
|
|
138
346
|
}
|
|
347
|
+
// `span: 'page'` headings open a chapter band across the full content
|
|
348
|
+
// width. Always start on a fresh page boundary so the band sits at the
|
|
349
|
+
// page top, and reset the cursor to column 0 so all other columns will
|
|
350
|
+
// have their availableHeight reduced symmetrically after placement.
|
|
351
|
+
if (level?.span === 'page') {
|
|
352
|
+
pendingSpacing = 0;
|
|
353
|
+
advanceToNextPageBoundary(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
354
|
+
cursor.columnIndex = 0;
|
|
355
|
+
flushPendingNumberingAtBoundary();
|
|
356
|
+
}
|
|
139
357
|
}
|
|
140
358
|
const id = `block-${blockIdCounter++}`;
|
|
141
359
|
const kind = resolveBlockKind(rawBlock, {
|
|
@@ -147,24 +365,140 @@ export function buildDocument(content, config, cache, options) {
|
|
|
147
365
|
listLevelIndentsPx,
|
|
148
366
|
orderedLevelIndentsPx,
|
|
149
367
|
orderedMetrics,
|
|
368
|
+
resourceById,
|
|
369
|
+
resourceTypeById,
|
|
370
|
+
resourceNumberById,
|
|
150
371
|
});
|
|
151
372
|
const { style, vdtType, headingLevel, numberPrefix, listBullet, listDepth, listKind, bulletXOffsetInColumn, strikethroughText } = kind;
|
|
152
373
|
let contentBlock = kind.contentBlock;
|
|
374
|
+
// --- Resource blocks (image / svg / table + caption) -----------------
|
|
375
|
+
// Measured and placed atomically (kept-together) — no mid-content split
|
|
376
|
+
// for v1. An unknown resource id produces no output (warnings handle it).
|
|
377
|
+
if (vdtType === 'resource') {
|
|
378
|
+
if (!kind.resource) {
|
|
379
|
+
flushPendingNumberingAtBoundary();
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
// Floated resources are not placed inline at their `::resource`
|
|
383
|
+
// directive — the directive is just an anchor (already enqueued above);
|
|
384
|
+
// the float lands in a page band. Only `position: 'here'` resources fall
|
|
385
|
+
// through to inline placement.
|
|
386
|
+
if (floatedIds.has(kind.resource.id)) {
|
|
387
|
+
flushPendingNumberingAtBoundary();
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const rCol = currentColumn(doc, cursor);
|
|
391
|
+
const { resourceBlock, measured } = runMeasurement({
|
|
392
|
+
vdtType,
|
|
393
|
+
rawBlock,
|
|
394
|
+
contentBlock,
|
|
395
|
+
style,
|
|
396
|
+
measureMaxWidth: rCol.bbox.width,
|
|
397
|
+
measureOptions: { textAlign: style.textAlign },
|
|
398
|
+
mathEnabled: resolved.math.enabled,
|
|
399
|
+
useRich: false,
|
|
400
|
+
resolved,
|
|
401
|
+
resources,
|
|
402
|
+
resourceTypes,
|
|
403
|
+
resourceNumbering,
|
|
404
|
+
resource: kind.resource,
|
|
405
|
+
resourceType: kind.resourceType,
|
|
406
|
+
resourceNumber: kind.resourceNumber,
|
|
407
|
+
});
|
|
408
|
+
if (!resourceBlock) {
|
|
409
|
+
flushPendingNumberingAtBoundary();
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const groupHeight = measured.totalHeight;
|
|
413
|
+
const blk = createVDTBlock(id, 'resource', style.fontString, style.color, style.textAlign);
|
|
414
|
+
blk.resourceBlock = resourceBlock;
|
|
415
|
+
blk.dirty = false;
|
|
416
|
+
blk.snappedToGrid = false;
|
|
417
|
+
blk.sourceStart = rawBlock.sourceStart + bodyOffset;
|
|
418
|
+
blk.sourceEnd = rawBlock.sourceEnd + bodyOffset;
|
|
419
|
+
// The single placeholder line carries the group height; caption lines are
|
|
420
|
+
// carried on `resourceBlock` and offset to absolute coords below.
|
|
421
|
+
blk.lines = [{
|
|
422
|
+
text: '',
|
|
423
|
+
bbox: { x: 0, y: 0, width: resourceBlock.bodyRect.width, height: groupHeight },
|
|
424
|
+
baseline: 0,
|
|
425
|
+
hyphenated: false,
|
|
426
|
+
segments: [],
|
|
427
|
+
isLastLine: true,
|
|
428
|
+
}];
|
|
429
|
+
const spacingBefore = pendingSpacing;
|
|
430
|
+
placeResourceBlock(blk, groupHeight, spacingBefore, cursor, doc, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
431
|
+
// `placeBlockInColumn` (inside placeResourceBlock) shifts `blk.lines`; the
|
|
432
|
+
// resource's own caption/table lines live on `resourceBlock` and must be
|
|
433
|
+
// offset to absolute page coordinates here using the placed bbox origin.
|
|
434
|
+
const ox = blk.bbox.x;
|
|
435
|
+
const oy = blk.bbox.y;
|
|
436
|
+
for (const ln of resourceBlock.captionLines) {
|
|
437
|
+
ln.bbox.x += ox;
|
|
438
|
+
ln.bbox.y += oy;
|
|
439
|
+
ln.baseline += oy;
|
|
440
|
+
}
|
|
441
|
+
if (resourceBlock.table) {
|
|
442
|
+
for (const cell of resourceBlock.table.cells) {
|
|
443
|
+
cell.rect.x += ox;
|
|
444
|
+
cell.rect.y += oy;
|
|
445
|
+
for (const cl of cell.lines) {
|
|
446
|
+
cl.bbox.x += ox;
|
|
447
|
+
cl.bbox.y += oy;
|
|
448
|
+
cl.baseline += oy;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
doc.blocks.push(blk);
|
|
453
|
+
pendingSpacing = style.marginBottomPx;
|
|
454
|
+
flushPendingNumberingAtBoundary();
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
153
457
|
// Measure text — use rich measurement for blocks with bold spans
|
|
154
458
|
const col = currentColumn(doc, cursor);
|
|
155
459
|
// Resolve inline math on spans (no-op when the block has no math).
|
|
156
460
|
const mathEnabled = resolved.math.enabled;
|
|
157
461
|
contentBlock = enrichMathSpans(contentBlock, style, resolved);
|
|
158
|
-
|
|
462
|
+
// Resolve inline `:ref{…}` spans to their computed label so references
|
|
463
|
+
// print their number in the running text. Each label becomes one atomic,
|
|
464
|
+
// non-breaking token tagged with its `refResourceId` (handled by the
|
|
465
|
+
// rich-text measurer), so we always take the rich path for ref blocks.
|
|
466
|
+
if (contentBlock.spans.some((s) => s.ref)) {
|
|
467
|
+
contentBlock = {
|
|
468
|
+
...contentBlock,
|
|
469
|
+
spans: resolveRefSpans(contentBlock.spans, resourceNumbering, resourceTypes, resources, {
|
|
470
|
+
bold: bodyStyle.referenceBold ?? true,
|
|
471
|
+
italic: bodyStyle.referenceItalic ?? false,
|
|
472
|
+
}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const hasRichSpans = contentBlock.spans.some((s) => s.bold || s.italic || s.mathRender || s.ref);
|
|
159
476
|
// List items reserve horizontal space for indent + bullet + gap.
|
|
160
477
|
const { measureMaxWidth, lineXShift, measureFirstLineIndent, measureHangingIndent, } = computeMeasureViewport(col.bbox.width, style, listBullet);
|
|
478
|
+
// First-paragraph-after-heading: typographic convention used in many
|
|
479
|
+
// scientific publications and book styles where the paragraph that
|
|
480
|
+
// immediately follows a heading is rendered without first-line indent.
|
|
481
|
+
// Only applies to regular paragraphs without hanging indent; list items
|
|
482
|
+
// and hanging-indent paragraphs are unaffected.
|
|
483
|
+
let effectiveFirstLineIndent = measureFirstLineIndent;
|
|
484
|
+
if (vdtType === 'paragraph'
|
|
485
|
+
&& !resolved.bodyText.indentAfterHeading
|
|
486
|
+
&& !resolved.bodyText.hangingIndent
|
|
487
|
+
&& blockIdx > 0) {
|
|
488
|
+
let prevIdx = blockIdx - 1;
|
|
489
|
+
while (prevIdx >= 0 && contentBlocks[prevIdx].type === 'directive')
|
|
490
|
+
prevIdx--;
|
|
491
|
+
if (prevIdx >= 0 && contentBlocks[prevIdx].type === 'heading') {
|
|
492
|
+
effectiveFirstLineIndent = 0;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
161
495
|
const runtActive = resolved.bodyText.avoidRunts
|
|
162
496
|
&& (vdtType === 'paragraph'
|
|
163
497
|
|| (vdtType === 'listItem' && resolved.bodyText.avoidRuntsInLists));
|
|
164
498
|
const measureOptions = {
|
|
165
499
|
textAlign: style.textAlign,
|
|
166
500
|
hyphenate: style.hyphenate,
|
|
167
|
-
firstLineIndentPx:
|
|
501
|
+
firstLineIndentPx: effectiveFirstLineIndent,
|
|
168
502
|
hangingIndent: measureHangingIndent,
|
|
169
503
|
optimal: resolved.bodyText.optimalLineBreaking,
|
|
170
504
|
maxStretchRatio: resolved.bodyText.maxWordSpacing,
|
|
@@ -264,6 +598,27 @@ export function buildDocument(content, config, cache, options) {
|
|
|
264
598
|
const totalRemainHeight = vdtType === 'mathDisplay'
|
|
265
599
|
? (remainingLines[0]?.bbox.height ?? style.lineHeightPx)
|
|
266
600
|
: remainingLines.length * style.lineHeightPx;
|
|
601
|
+
// For headings with an enabled advanced-design slot, the rendered
|
|
602
|
+
// overlay may extend below the natural text bottom. Measure the slot's
|
|
603
|
+
// actual content bottom so the reserved block height (and the
|
|
604
|
+
// subsequent marginBottom + grid snap) starts from there.
|
|
605
|
+
let effectiveRemainHeight = totalRemainHeight;
|
|
606
|
+
if (vdtType === 'heading' && headingLevel !== undefined && partIndex === 0) {
|
|
607
|
+
const lvl = resolved.headings.levels.find((l) => l.level === headingLevel);
|
|
608
|
+
if (lvl) {
|
|
609
|
+
const full = remainingLines
|
|
610
|
+
.map((ln) => (ln.segments ?? []).map((s) => s.text).join(''))
|
|
611
|
+
.join(' ');
|
|
612
|
+
const pref = numberPrefix ?? '';
|
|
613
|
+
const title = pref && full.startsWith(`${pref} `) ? full.slice(pref.length + 1) : full;
|
|
614
|
+
// Span-page openers lay out across the full content area (both
|
|
615
|
+
// columns); in-column headings use just the column width.
|
|
616
|
+
const measureWidth = lvl.span === 'page' ? contentArea.width : curCol.bbox.width;
|
|
617
|
+
const designBottom = measureHeadingAdvancedDesignHeight(lvl, { titleText: title, formattedNumber: pref, chapterNumber: pref }, measureWidth, resolved.page.dpi, doc.metadata, cursor.pageIndex);
|
|
618
|
+
if (designBottom > effectiveRemainHeight)
|
|
619
|
+
effectiveRemainHeight = designBottom;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
267
622
|
// Keep-with-list: if this colon-paragraph would fit but would leave no
|
|
268
623
|
// room for the first list item, split off the colon line (or push the
|
|
269
624
|
// whole paragraph when it is a single line or the split would leave a
|
|
@@ -309,7 +664,7 @@ export function buildDocument(content, config, cache, options) {
|
|
|
309
664
|
remainingLines = remainingLines.slice(splitAt);
|
|
310
665
|
partIndex++;
|
|
311
666
|
pendingSpacing = 0;
|
|
312
|
-
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
667
|
+
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
313
668
|
continue;
|
|
314
669
|
}
|
|
315
670
|
// Can't cleanly split the colon line off — would create a widow.
|
|
@@ -339,19 +694,19 @@ export function buildDocument(content, config, cache, options) {
|
|
|
339
694
|
}
|
|
340
695
|
blockIdx -= headingRunCount + 1;
|
|
341
696
|
pendingSpacing = 0;
|
|
342
|
-
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
697
|
+
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
343
698
|
break;
|
|
344
699
|
}
|
|
345
700
|
if (headingRunCount === 0) {
|
|
346
701
|
pendingSpacing = 0;
|
|
347
|
-
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
702
|
+
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
348
703
|
continue;
|
|
349
704
|
}
|
|
350
705
|
// headingRunCount === curCol.blocks.length: fall through to place.
|
|
351
706
|
}
|
|
352
707
|
}
|
|
353
708
|
// Block fits in current column
|
|
354
|
-
if (
|
|
709
|
+
if (effectiveRemainHeight <= effectiveAvailable) {
|
|
355
710
|
// Heading keep-with-next: never leave a heading as the last block of a
|
|
356
711
|
// column. If the following (non-heading) block wouldn't have room to
|
|
357
712
|
// place at least its widow-minimum number of lines after this heading,
|
|
@@ -368,7 +723,7 @@ export function buildDocument(content, config, cache, options) {
|
|
|
368
723
|
&& nextBlock !== null
|
|
369
724
|
&& curCol.blocks.length > 0) {
|
|
370
725
|
const wouldUsedHeight = (curCol.bbox.height - curCol.availableHeight) + spacingBefore;
|
|
371
|
-
const naturalBottom = wouldUsedHeight +
|
|
726
|
+
const naturalBottom = wouldUsedHeight + effectiveRemainHeight + style.marginBottomPx;
|
|
372
727
|
const snappedBottom = shouldSnapToGrid
|
|
373
728
|
? Math.ceil((naturalBottom - 0.01) / baselineGrid) * baselineGrid
|
|
374
729
|
: naturalBottom;
|
|
@@ -386,11 +741,11 @@ export function buildDocument(content, config, cache, options) {
|
|
|
386
741
|
// rolled-back heading.
|
|
387
742
|
blockIdx -= rollbackCount + 1;
|
|
388
743
|
pendingSpacing = 0;
|
|
389
|
-
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
744
|
+
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
390
745
|
break;
|
|
391
746
|
}
|
|
392
747
|
pendingSpacing = 0;
|
|
393
|
-
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
748
|
+
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
394
749
|
continue;
|
|
395
750
|
}
|
|
396
751
|
}
|
|
@@ -428,7 +783,7 @@ export function buildDocument(content, config, cache, options) {
|
|
|
428
783
|
}
|
|
429
784
|
blk.sourceMap = absoluteSourceMap;
|
|
430
785
|
blk.plainPrefixLen = prefixLen;
|
|
431
|
-
let h =
|
|
786
|
+
let h = effectiveRemainHeight;
|
|
432
787
|
if (shouldSnapToGrid && partIndex === 0) {
|
|
433
788
|
// Snap using the absolute position in the column so that the block
|
|
434
789
|
// bottom lands on a baseline grid line. This accounts for off-grid
|
|
@@ -436,7 +791,7 @@ export function buildDocument(content, config, cache, options) {
|
|
|
436
791
|
// the minimum marginBottom — the grid always wins, but the margin
|
|
437
792
|
// below is guaranteed to be at least marginBottomPx.
|
|
438
793
|
const usedHeight = curCol.bbox.height - curCol.availableHeight;
|
|
439
|
-
const naturalBottom = usedHeight +
|
|
794
|
+
const naturalBottom = usedHeight + effectiveRemainHeight + style.marginBottomPx;
|
|
440
795
|
// Tolerance guards against FP drift: if naturalBottom is already on
|
|
441
796
|
// the grid (e.g. marginBottom is an exact multiple of baselineGrid),
|
|
442
797
|
// don't round up to the next line.
|
|
@@ -446,6 +801,20 @@ export function buildDocument(content, config, cache, options) {
|
|
|
446
801
|
placeBlockInColumn(blk, h, curCol, cursor);
|
|
447
802
|
finalizeListItem(blk, partIndex === 0);
|
|
448
803
|
doc.blocks.push(blk);
|
|
804
|
+
// Page-spanning heading: reserve the same vertical band in every
|
|
805
|
+
// other column on this page so body text under the opener band
|
|
806
|
+
// starts below it in ALL columns, not just the one it was placed in.
|
|
807
|
+
if (vdtType === 'heading' && headingLevel !== undefined) {
|
|
808
|
+
const lvl = resolved.headings.levels.find((l) => l.level === headingLevel);
|
|
809
|
+
if (lvl?.span === 'page') {
|
|
810
|
+
const page = doc.pages[cursor.pageIndex];
|
|
811
|
+
for (const otherCol of page.columns) {
|
|
812
|
+
if (otherCol !== curCol) {
|
|
813
|
+
otherCol.availableHeight = Math.max(0, otherCol.availableHeight - h);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
449
818
|
// For snapped headings/list-tails the margin is baked into the snap;
|
|
450
819
|
// for unsnapped ones (consecutive) track it for collapsing
|
|
451
820
|
if (vdtType === 'listItem' && nextIsListItem) {
|
|
@@ -502,7 +871,7 @@ export function buildDocument(content, config, cache, options) {
|
|
|
502
871
|
remainingLines = remainingLines.slice(choice.splitAt);
|
|
503
872
|
partIndex++;
|
|
504
873
|
pendingSpacing = 0;
|
|
505
|
-
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
874
|
+
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
506
875
|
continue;
|
|
507
876
|
}
|
|
508
877
|
// choice.splitAt === 0: fall through to push whole paragraph to next column
|
|
@@ -518,12 +887,12 @@ export function buildDocument(content, config, cache, options) {
|
|
|
518
887
|
if (rollbackCount > 0) {
|
|
519
888
|
blockIdx -= rollbackCount + 1;
|
|
520
889
|
pendingSpacing = 0;
|
|
521
|
-
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
890
|
+
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
522
891
|
break;
|
|
523
892
|
}
|
|
524
893
|
}
|
|
525
894
|
pendingSpacing = 0;
|
|
526
|
-
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
895
|
+
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
527
896
|
continue;
|
|
528
897
|
}
|
|
529
898
|
// Empty column but block still doesn't fit (block taller than page) — place anyway
|
|
@@ -554,6 +923,9 @@ export function buildDocument(content, config, cache, options) {
|
|
|
554
923
|
}
|
|
555
924
|
flushPendingNumberingAtBoundary();
|
|
556
925
|
}
|
|
926
|
+
// Place any floats still pending (referenced on the last page, or never
|
|
927
|
+
// followed by a content-overflow page break) onto freshly appended pages.
|
|
928
|
+
finalizeFloats();
|
|
557
929
|
// Stamp page-number info onto every page (including blank parity pages).
|
|
558
930
|
const labels = buildPageLabels(doc.pages.length, pageNumberSegments);
|
|
559
931
|
for (let i = 0; i < doc.pages.length; i++) {
|