postext 0.3.42 → 0.3.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__tests__/createLayout.test.js +5 -65
- package/dist/__tests__/createLayout.test.js.map +1 -1
- package/dist/__tests__/defaults/calloutStyles.test.js +3 -0
- package/dist/__tests__/defaults/calloutStyles.test.js.map +1 -1
- package/dist/__tests__/parse/containers.test.js +1 -1
- package/dist/__tests__/parse/containers.test.js.map +1 -1
- package/dist/__tests__/parse/scripts.test.d.ts +2 -0
- package/dist/__tests__/parse/scripts.test.d.ts.map +1 -0
- package/dist/__tests__/parse/scripts.test.js +68 -0
- package/dist/__tests__/parse/scripts.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutColumnsLabel.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutColumnsLabel.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutColumnsLabel.test.js +163 -0
- package/dist/__tests__/pipeline/calloutColumnsLabel.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutFloat.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutFloat.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutFloat.test.js +138 -0
- package/dist/__tests__/pipeline/calloutFloat.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutPlacement.test.js +10 -6
- package/dist/__tests__/pipeline/calloutPlacement.test.js.map +1 -1
- package/dist/__tests__/pipeline/floatPlacement.test.js +3 -3
- package/dist/__tests__/pipeline/floatPlacement.test.js.map +1 -1
- package/dist/__tests__/pipeline/sideColumn.test.d.ts +2 -0
- package/dist/__tests__/pipeline/sideColumn.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/sideColumn.test.js +130 -0
- package/dist/__tests__/pipeline/sideColumn.test.js.map +1 -0
- package/dist/__tests__/pipeline/spanBlocks.test.js +11 -3
- package/dist/__tests__/pipeline/spanBlocks.test.js.map +1 -1
- package/dist/canvas-backend/blockRender.js +2 -2
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +2 -2
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/columnRule.d.ts.map +1 -1
- package/dist/columnRule.js +3 -0
- package/dist/columnRule.js.map +1 -1
- package/dist/defaults/calloutStyles.d.ts +23 -0
- package/dist/defaults/calloutStyles.d.ts.map +1 -1
- package/dist/defaults/calloutStyles.js +68 -0
- package/dist/defaults/calloutStyles.js.map +1 -1
- package/dist/defaults/headerFooter.d.ts.map +1 -1
- package/dist/defaults/headerFooter.js +2 -0
- package/dist/defaults/headerFooter.js.map +1 -1
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +7 -1
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/layout.d.ts.map +1 -1
- package/dist/defaults/layout.js +12 -0
- package/dist/defaults/layout.js.map +1 -1
- package/dist/design/layout.d.ts +3 -0
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +149 -5
- package/dist/design/layout.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +8 -4
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/richAdapter.d.ts +3 -0
- package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/richAdapter.js +1 -0
- package/dist/knuthPlass/richAdapter.js.map +1 -1
- package/dist/measure/font.d.ts +9 -0
- package/dist/measure/font.d.ts.map +1 -1
- package/dist/measure/font.js +17 -1
- package/dist/measure/font.js.map +1 -1
- package/dist/measure/rich.d.ts +17 -0
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +45 -4
- package/dist/measure/rich.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/inlineFormatting.d.ts +5 -0
- package/dist/parse/inlineFormatting.d.ts.map +1 -1
- package/dist/parse/inlineFormatting.js +29 -4
- package/dist/parse/inlineFormatting.js.map +1 -1
- package/dist/parse/types.d.ts +4 -1
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +452 -85
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +4 -0
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +390 -131
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/columnBalancing.js +1 -1
- package/dist/pipeline/columnBalancing.js.map +1 -1
- package/dist/pipeline/config.d.ts +16 -1
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +35 -2
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +21 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js +12 -3
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/floatSlots.d.ts +17 -0
- package/dist/pipeline/floatSlots.d.ts.map +1 -1
- package/dist/pipeline/floatSlots.js +27 -3
- package/dist/pipeline/floatSlots.js.map +1 -1
- package/dist/pipeline/headerFooter.js +1 -1
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +13 -3
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/placement.d.ts +12 -1
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +50 -7
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +17 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +45 -11
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/types.d.ts +174 -62
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +11 -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
|
@@ -12,7 +12,7 @@ import { computeOutline, hasTocDirective, outlineFromDoc, sameOutline } from './
|
|
|
12
12
|
import { expandTocDirectives } from './toc';
|
|
13
13
|
import { resolveBodyStyle, resolveBlockquoteStyle } from './styles';
|
|
14
14
|
import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, } from './lists';
|
|
15
|
-
import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, } from './placement';
|
|
15
|
+
import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, sideColumnOf, sideColumns, sideUsedBottom, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, } from './placement';
|
|
16
16
|
import { chooseParagraphSplit } from './orphanWidow';
|
|
17
17
|
import { applyStyleAttrs, computePageMetrics, isMarkerBlock, nextNonMarkerBlock, prevNonMarkerBlock, rollbackTrailingBlocks, } from './buildHelpers';
|
|
18
18
|
import { measureContentBlock } from './measureContentBlock';
|
|
@@ -21,7 +21,7 @@ import { planParts, derivePartMeasureContext } from './parts';
|
|
|
21
21
|
import { layoutCallout, offsetCalloutToAbsolute, pickCalloutStyle, planCallouts, resolveCalloutAttrs, } from './calloutLayout';
|
|
22
22
|
import { layoutResourceBlock, planTableSlice } from './resourceLayout';
|
|
23
23
|
import { computeFloatPlan, floatedResourceIds, } from './floatPlacement';
|
|
24
|
-
import { enumerateCurrentPageSlots, measureFloatBand, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
|
|
24
|
+
import { enumerateCurrentPageSlots, measureFloatBand, measureSideStack, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
|
|
25
25
|
import { computeHeadingContext, computeResourceNumbering, } from './resourceNumbering';
|
|
26
26
|
import { defaultResourceTypes } from '../defaults/resourceTypes';
|
|
27
27
|
import { buildHeadersAndFooters, measureHeadingAdvancedDesignHeight } from './headerFooter';
|
|
@@ -211,6 +211,17 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
211
211
|
// Floats whose first reference has been passed but which are not yet placed
|
|
212
212
|
// into a page band, in reading order.
|
|
213
213
|
const pendingFloats = [];
|
|
214
|
+
/** `span: 'side'` boxes that found no room in the side column of their
|
|
215
|
+
* page: they take the side column of the next page the flow opens, in
|
|
216
|
+
* order (see `placeCalloutSide`). */
|
|
217
|
+
const pendingSideBoxes = [];
|
|
218
|
+
/** Whether the band the cursor is in lays out as a multi-column band:
|
|
219
|
+
* two or more text columns, or one beside a float-only side column — a
|
|
220
|
+
* page-span block then cuts the band across every column. */
|
|
221
|
+
const multiColumnBand = (page) => {
|
|
222
|
+
const band = currentBand(page, cursor);
|
|
223
|
+
return bandColumns(page, band).length > 1 || sideColumnOf(page, band) !== undefined;
|
|
224
|
+
};
|
|
214
225
|
/** Resource ids already enqueued in this pass — a keep-with-next rewind
|
|
215
226
|
* replays the loop top for the rolled-back blocks and must not enqueue
|
|
216
227
|
* (and later place) the same float twice. */
|
|
@@ -219,13 +230,56 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
219
230
|
const fl = floatsByFirstBlock.get(blockIdx);
|
|
220
231
|
if (!fl)
|
|
221
232
|
return;
|
|
233
|
+
const page = doc.pages[cursor.pageIndex];
|
|
234
|
+
const col = page?.columns[cursor.columnIndex];
|
|
222
235
|
for (const f of fl) {
|
|
223
236
|
if (enqueuedFloatIds.has(f.resourceId))
|
|
224
237
|
continue;
|
|
225
238
|
enqueuedFloatIds.add(f.resourceId);
|
|
226
|
-
|
|
239
|
+
// Where the citing block starts: a side float stacks beside it.
|
|
240
|
+
const stamped = col
|
|
241
|
+
? { ...f, refPageIndex: page.index, refY: col.bbox.y + (col.bbox.height - col.availableHeight) + (col.blocks.length > 0 ? pendingSpacing : 0) }
|
|
242
|
+
: f;
|
|
243
|
+
pendingFloats.push(stamped);
|
|
227
244
|
}
|
|
228
245
|
};
|
|
246
|
+
/** Floated boxes (`placement: 'top' | 'bottom'`, `span` column / page):
|
|
247
|
+
* a `:::callout` that leaves the flow like a resource and takes the
|
|
248
|
+
* first free band after the point it occurs at — the foot of the
|
|
249
|
+
* current page, or the head / foot of a page the flow opens later —
|
|
250
|
+
* while the text after it fills the page it left. Keyed by the fence's
|
|
251
|
+
* content index; the pending float carries the synthetic id
|
|
252
|
+
* `callout:<idx>` (see `calloutFloatOf`). */
|
|
253
|
+
const calloutFloats = new Map();
|
|
254
|
+
const CALLOUT_FLOAT_PREFIX = 'callout:';
|
|
255
|
+
const calloutFloatOf = (resourceId) => resourceId.startsWith(CALLOUT_FLOAT_PREFIX) ? calloutFloats.get(Number(resourceId.slice(CALLOUT_FLOAT_PREFIX.length))) : undefined;
|
|
256
|
+
/** Boxes laid out for a band, waiting for the band's `y` (built in
|
|
257
|
+
* `buildFloatBlock`, committed in `commitFloatBlock`). */
|
|
258
|
+
const calloutFloatResults = new Map();
|
|
259
|
+
/** Pages whose head or foot a floated box took (see the gallery rule in
|
|
260
|
+
* `placeFloatInColumns`). */
|
|
261
|
+
const calloutBandPages = new Set();
|
|
262
|
+
const enqueueCalloutFloat = (startIdx, plan, style, L, placement, span) => {
|
|
263
|
+
const key = `${CALLOUT_FLOAT_PREFIX}${startIdx}`;
|
|
264
|
+
// A keep-with-next replay passes the fence again: enqueue once.
|
|
265
|
+
if (!enqueuedFloatIds.has(key)) {
|
|
266
|
+
enqueuedFloatIds.add(key);
|
|
267
|
+
calloutFloats.set(startIdx, { plan, style, L });
|
|
268
|
+
const page = doc.pages[cursor.pageIndex];
|
|
269
|
+
const col = page?.columns[cursor.columnIndex];
|
|
270
|
+
pendingFloats.push({
|
|
271
|
+
resourceId: key,
|
|
272
|
+
firstBlockIdx: startIdx,
|
|
273
|
+
position: placement,
|
|
274
|
+
span: span === 'page' ? 'page' : 'column',
|
|
275
|
+
callout: { startIdx },
|
|
276
|
+
...(col ? { refPageIndex: page.index, refY: col.bbox.y + (col.bbox.height - col.availableHeight) } : {}),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// Floats first referenced inside the box enqueue in reading order.
|
|
280
|
+
for (let i = startIdx + 1; i <= plan.endIdx; i++)
|
|
281
|
+
enqueueFloatsFor(i);
|
|
282
|
+
};
|
|
229
283
|
const floatGapPx = bodyStyle.lineHeightPx;
|
|
230
284
|
const minTextPx = bodyStyle.lineHeightPx * 3;
|
|
231
285
|
/** Fewest body rows the closing slice of a split table carries. */
|
|
@@ -276,7 +330,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
276
330
|
}
|
|
277
331
|
};
|
|
278
332
|
const rotationKey = (rotated) => rotated ? `:${rotated.direction}${rotated.length.toFixed(2)}` : '';
|
|
279
|
-
const
|
|
333
|
+
const asideKey = (a) => (a ? `:aside${a.dx.toFixed(1)}x${a.width.toFixed(1)}${a.alignBottom ? 'b' : 't'}${(a.offsetY ?? 0).toFixed(1)}` : '');
|
|
334
|
+
const layoutFloat = (resourceId, width, slice, rotated, aside) => {
|
|
280
335
|
const resource = resourceById.get(resourceId);
|
|
281
336
|
if (!resource)
|
|
282
337
|
return null;
|
|
@@ -291,6 +346,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
291
346
|
resources,
|
|
292
347
|
...(slice ? { slice } : {}),
|
|
293
348
|
...(rotated ? { rotate: rotated.direction, rotatedLength: rotated.length } : {}),
|
|
349
|
+
...(aside ? { captionAside: aside } : {}),
|
|
294
350
|
});
|
|
295
351
|
};
|
|
296
352
|
/** The rotation of a pending float on a band whose columns keep `avail`
|
|
@@ -322,12 +378,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
322
378
|
/** Height (and caption baseline) of a float at a given width, memoised —
|
|
323
379
|
* fit checks run for every pending float on every loop iteration. */
|
|
324
380
|
const floatMeasureMemo = new Map();
|
|
325
|
-
const measureFloat = (resourceId, width, slice, rotated) => {
|
|
326
|
-
const key = `${resourceId}:${width.toFixed(2)}${sliceKey(slice)}${rotationKey(rotated)}`;
|
|
381
|
+
const measureFloat = (resourceId, width, slice, rotated, aside) => {
|
|
382
|
+
const key = `${resourceId}:${width.toFixed(2)}${sliceKey(slice)}${rotationKey(rotated)}${asideKey(aside)}`;
|
|
327
383
|
const memo = floatMeasureMemo.get(key);
|
|
328
384
|
if (memo !== undefined)
|
|
329
385
|
return memo;
|
|
330
|
-
const
|
|
386
|
+
const cf = calloutFloatOf(resourceId);
|
|
387
|
+
if (cf) {
|
|
388
|
+
// A floated box: its frame at the band's width, title and icon
|
|
389
|
+
// included; no caption baseline to align.
|
|
390
|
+
const r = cf.L.layoutRange(CUT_START, cf.L.end, width, 'float-probe', false);
|
|
391
|
+
const mc = { height: r.totalHeight };
|
|
392
|
+
floatMeasureMemo.set(key, mc);
|
|
393
|
+
return mc;
|
|
394
|
+
}
|
|
395
|
+
const laid = layoutFloat(resourceId, width, slice, rotated, aside);
|
|
331
396
|
let m = null;
|
|
332
397
|
if (laid && laid.block.rotation) {
|
|
333
398
|
// A rotated block takes its band whole: no caption baseline to align,
|
|
@@ -351,6 +416,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
351
416
|
m = {
|
|
352
417
|
height: laid.totalHeight,
|
|
353
418
|
...(lastBaseline !== undefined ? { lastCaptionBaseline: lastBaseline } : {}),
|
|
419
|
+
...(laid.asideHeight !== undefined ? { asideHeight: laid.asideHeight } : {}),
|
|
354
420
|
};
|
|
355
421
|
}
|
|
356
422
|
floatMeasureMemo.set(key, m);
|
|
@@ -361,8 +427,18 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
361
427
|
const buildFloatBlock = (resourceId, x, width, slice, rotated,
|
|
362
428
|
/** A rotated block sits flush to the band's right edge (the spine of a
|
|
363
429
|
* verso page) instead of its left. */
|
|
364
|
-
flushEnd = false
|
|
365
|
-
|
|
430
|
+
flushEnd = false, aside,
|
|
431
|
+
/** The page is a verso of mirrored margins (a floated box's `'outer'`
|
|
432
|
+
* corner icon hangs on the left there). */
|
|
433
|
+
mirrored = false) => {
|
|
434
|
+
const cf = calloutFloatOf(resourceId);
|
|
435
|
+
if (cf) {
|
|
436
|
+
const startIdx = Number(resourceId.slice(CALLOUT_FLOAT_PREFIX.length));
|
|
437
|
+
const result = cf.L.layoutRange(CUT_START, cf.L.end, width, `block-${blockIdCounter++}`, false, mirrored);
|
|
438
|
+
calloutFloatResults.set(result.frame, { result, startIdx, plan: cf.plan });
|
|
439
|
+
return { block: result.frame, height: result.totalHeight };
|
|
440
|
+
}
|
|
441
|
+
const laid = layoutFloat(resourceId, width, slice, rotated, aside);
|
|
366
442
|
if (!laid)
|
|
367
443
|
return null;
|
|
368
444
|
const { block: rb, totalHeight } = laid;
|
|
@@ -514,22 +590,56 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
514
590
|
/** The band a float would take in a slot — its height (`need`) and the
|
|
515
591
|
* float's `y` — without reserving it. `null` when the float cannot be
|
|
516
592
|
* measured. */
|
|
517
|
-
const probeFloatBand = (page, f, targetCols, position, pageSpan, anchorToCap) => {
|
|
593
|
+
const probeFloatBand = (page, f, targetCols, position, pageSpan, anchorToCap, side = false, refY) => {
|
|
518
594
|
const first = targetCols[0];
|
|
519
|
-
const
|
|
520
|
-
const
|
|
595
|
+
const slotWidth = pageSpan ? page.contentArea.width : first.bbox.width;
|
|
596
|
+
const slotX = pageSpan ? page.contentArea.x : first.bbox.x;
|
|
597
|
+
// A narrower float (`placement.width`) sits in its slot per `align`.
|
|
598
|
+
const width = f.widthFraction && f.widthFraction < 1 ? slotWidth * f.widthFraction : slotWidth;
|
|
599
|
+
const alignK = f.align === 'center' ? 0.5 : f.align === 'right' ? 1 : 0;
|
|
600
|
+
const xLeft = slotX + (slotWidth - width) * alignK;
|
|
521
601
|
const slice = sliceOf(f);
|
|
522
|
-
|
|
523
|
-
const
|
|
602
|
+
// A side float never turns: it stacks upright in the side column.
|
|
603
|
+
const rotated = side ? undefined : rotationFor(f, Math.min(...targetCols.map((c) => c.availableHeight)));
|
|
604
|
+
// The caption beside the figure, in the band's side column.
|
|
605
|
+
let aside;
|
|
606
|
+
let sideCol;
|
|
607
|
+
if (f.captionSide && !pageSpan && !side && !rotated) {
|
|
608
|
+
const sc = sideColumnOf(page, first.band ?? 0);
|
|
609
|
+
if (sc && sc.bbox.width > 0.5) {
|
|
610
|
+
sideCol = sc;
|
|
611
|
+
aside = { dx: sc.bbox.x - xLeft, width: sc.bbox.width, alignBottom: position === 'bottom' };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const measure = measureFloat(f.resourceId, width, slice, rotated, aside);
|
|
524
615
|
if (!measure)
|
|
525
616
|
return null;
|
|
617
|
+
// A side float stacks in the side column beside the citing text.
|
|
618
|
+
if (side) {
|
|
619
|
+
const { need, y } = measureSideStack(measure, first, refY, page.contentArea, baselineGrid, floatGapPx);
|
|
620
|
+
return { need, y, measure, slice, width, xLeft, rotated };
|
|
621
|
+
}
|
|
526
622
|
// A bottom band normally anchors to the column's true foot (under a
|
|
527
623
|
// trailing cap, the page bottom — the closing-page figure). Before a
|
|
528
624
|
// page-span box the cap IS the band's foot: the figure hugs the text
|
|
529
625
|
// and the box follows both.
|
|
530
626
|
const bottomOf = (c) => anchorToCap ? c.bbox.y + c.bbox.height : trueBottom(c, uncappedBottoms);
|
|
531
|
-
|
|
532
|
-
|
|
627
|
+
let { need, y } = measureFloatBand(position, measure, targetCols, page.contentArea, baselineGrid, floatGapPx, bottomOf);
|
|
628
|
+
// A caption level with a top float's head would overlap what the side
|
|
629
|
+
// column already holds: it drops under the stack instead.
|
|
630
|
+
if (aside && sideCol && position === 'top' && !aside.alignBottom) {
|
|
631
|
+
const used = sideUsedBottom(sideCol);
|
|
632
|
+
if (used > y + 0.5) {
|
|
633
|
+
const shifted = { ...aside, offsetY: used - y };
|
|
634
|
+
const m2 = measureFloat(f.resourceId, width, slice, rotated, shifted);
|
|
635
|
+
if (m2) {
|
|
636
|
+
aside = shifted;
|
|
637
|
+
({ need, y } = measureFloatBand(position, m2, targetCols, page.contentArea, baselineGrid, floatGapPx, bottomOf));
|
|
638
|
+
return { need, y, measure: m2, slice, width, xLeft, rotated, aside, sideCol };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { need, y, measure, slice, width, xLeft, rotated, ...(aside ? { aside, sideCol } : {}) };
|
|
533
643
|
};
|
|
534
644
|
/** Free height `col` keeps for the flow once a float band `probe` is
|
|
535
645
|
* reserved in it at `position` (mirrors the reservation arithmetic of
|
|
@@ -543,18 +653,68 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
543
653
|
const newHeight = Math.max(0, probe.y - floatGapPx - col.bbox.y);
|
|
544
654
|
return Math.max(0, col.availableHeight - (col.bbox.height - newHeight));
|
|
545
655
|
};
|
|
546
|
-
|
|
656
|
+
/** Set a built float at its band position and hand it to the page: a
|
|
657
|
+
* resource block into `page.floats`; a floated box's frame and children
|
|
658
|
+
* into `page.floats` and `doc.blocks`, the way a fixed box goes. */
|
|
659
|
+
const commitFloatBlock = (page, col, built, x, y, width) => {
|
|
660
|
+
const cf = calloutFloatResults.get(built.block);
|
|
661
|
+
if (cf) {
|
|
662
|
+
calloutFloatResults.delete(built.block);
|
|
663
|
+
const { result, startIdx, plan } = cf;
|
|
664
|
+
const frame = result.frame;
|
|
665
|
+
stampCalloutSource(frame, startIdx, plan);
|
|
666
|
+
frame.pageIndex = page.index;
|
|
667
|
+
frame.columnIndex = col.index;
|
|
668
|
+
offsetCalloutToAbsolute(result, x, y);
|
|
669
|
+
const floats = (page.floats ??= []);
|
|
670
|
+
doc.blocks.push(frame);
|
|
671
|
+
floats.push(frame);
|
|
672
|
+
for (const child of result.children) {
|
|
673
|
+
child.pageIndex = frame.pageIndex;
|
|
674
|
+
child.columnIndex = frame.columnIndex;
|
|
675
|
+
doc.blocks.push(child);
|
|
676
|
+
floats.push(child);
|
|
677
|
+
}
|
|
678
|
+
calloutBandPages.add(page);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
offsetResourceBlockToAbsolute(built.block.resourceBlock, 0, y);
|
|
682
|
+
built.block.bbox = createBoundingBox(x, y, width, built.height);
|
|
683
|
+
built.block.pageIndex = page.index;
|
|
684
|
+
built.block.columnIndex = col.index;
|
|
685
|
+
(page.floats ??= []).push(built.block);
|
|
686
|
+
};
|
|
687
|
+
const placeFloatInColumns = (page, f, targetCols, position, pageSpan, mode, anchorToCap = false, side = false, refY) => {
|
|
547
688
|
const first = targetCols[0];
|
|
548
|
-
const probe = probeFloatBand(page, f, targetCols, position, pageSpan, anchorToCap);
|
|
689
|
+
const probe = probeFloatBand(page, f, targetCols, position, pageSpan, anchorToCap, side, refY);
|
|
549
690
|
if (!probe)
|
|
550
691
|
return 'skip';
|
|
551
|
-
const { width, xLeft, rotated } = probe;
|
|
692
|
+
const { width, xLeft, rotated, aside, sideCol } = probe;
|
|
552
693
|
let { slice, measure, need, y } = probe;
|
|
553
694
|
let rest;
|
|
554
695
|
/** The float was cut to this slot (a table slice). */
|
|
555
696
|
let cut = false;
|
|
556
697
|
/** A rotated block wider than the band (its upright height). */
|
|
557
698
|
const tooWide = (m) => m.rotatedWidth !== undefined && m.rotatedWidth > width + 0.01;
|
|
699
|
+
if (side) {
|
|
700
|
+
// The side column's stack: the float goes under what the column holds
|
|
701
|
+
// when the rest of the column takes it; otherwise it waits for the
|
|
702
|
+
// side column of the next page — where it is set anyway, overflowing,
|
|
703
|
+
// when even an empty column cannot hold it (a dominating figure).
|
|
704
|
+
if (need > first.availableHeight + 0.01) {
|
|
705
|
+
if (mode === 'strict' || sideUsedBottom(first) > first.bbox.y + 0.5)
|
|
706
|
+
return 'defer';
|
|
707
|
+
}
|
|
708
|
+
const built = buildFloatBlock(f.resourceId, xLeft, width, slice);
|
|
709
|
+
if (!built)
|
|
710
|
+
return 'skip';
|
|
711
|
+
first.availableHeight = Math.max(0, first.availableHeight - need);
|
|
712
|
+
commitFloatBlock(page, first, built, xLeft, y, width);
|
|
713
|
+
floatsPlaced++;
|
|
714
|
+
return 'placed';
|
|
715
|
+
}
|
|
716
|
+
/** Gallery page (see below): the band takes the columns' whole room. */
|
|
717
|
+
let galleryFill = false;
|
|
558
718
|
if (mode === 'fresh') {
|
|
559
719
|
let minAvail = Infinity;
|
|
560
720
|
let anyReserved = false;
|
|
@@ -564,8 +724,19 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
564
724
|
if (r.top > 0 || r.bottom > 0)
|
|
565
725
|
anyReserved = true;
|
|
566
726
|
}
|
|
567
|
-
if (need > minAvail - minTextPx && anyReserved)
|
|
568
|
-
|
|
727
|
+
if (need > minAvail - minTextPx && anyReserved) {
|
|
728
|
+
// Gallery page: a float cited on an earlier page that arrives
|
|
729
|
+
// under a floated box heading this page takes the rest of the
|
|
730
|
+
// page — the compositor's box-plus-figure page — rather than
|
|
731
|
+
// waiting for the next one and leaving a line or two stranded
|
|
732
|
+
// between the bands. Only a float that fits whole.
|
|
733
|
+
const gallery = need <= minAvail + 0.01
|
|
734
|
+
&& f.refPageIndex !== undefined && f.refPageIndex < page.index
|
|
735
|
+
&& calloutBandPages.has(page) && !calloutFloatOf(f.resourceId);
|
|
736
|
+
if (!gallery)
|
|
737
|
+
return 'defer';
|
|
738
|
+
galleryFill = true;
|
|
739
|
+
}
|
|
569
740
|
if (need > minAvail + 0.01 || tooWide(measure)) {
|
|
570
741
|
// Dominating the band: a table is cut to it (a rotated table, to
|
|
571
742
|
// the band's width).
|
|
@@ -635,9 +806,26 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
635
806
|
if (rest) {
|
|
636
807
|
rest = { ...rest, notBefore: { pageIndex: page.index, columnIndex: targetCols[targetCols.length - 1].index } };
|
|
637
808
|
}
|
|
638
|
-
const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, rotated ? rotatedFlushEnd(page) : false);
|
|
809
|
+
const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, rotated ? rotatedFlushEnd(page) : false, aside, mirroredOf(page));
|
|
639
810
|
if (!built)
|
|
640
811
|
return 'skip';
|
|
812
|
+
// The caption beside the figure takes its band of the side column: a
|
|
813
|
+
// top float's caption is consumed from the stack's head (when the
|
|
814
|
+
// stack is still above it), a bottom float's cuts the column's foot.
|
|
815
|
+
if (aside && sideCol && measure.asideHeight !== undefined) {
|
|
816
|
+
const bandH = measure.asideHeight + floatGapPx;
|
|
817
|
+
if (position === 'top') {
|
|
818
|
+
const used = sideUsedBottom(sideCol);
|
|
819
|
+
const bottom = y + (aside.offsetY ?? 0) + bandH;
|
|
820
|
+
if (bottom > used)
|
|
821
|
+
sideCol.availableHeight = Math.max(0, sideCol.availableHeight - (bottom - used));
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const cut = Math.max(0, sideCol.bbox.y + sideCol.bbox.height - (y + built.height - measure.asideHeight - floatGapPx));
|
|
825
|
+
sideCol.bbox.height = Math.max(0, sideCol.bbox.height - cut);
|
|
826
|
+
sideCol.availableHeight = Math.max(0, sideCol.availableHeight - cut);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
641
829
|
for (const col of targetCols) {
|
|
642
830
|
const r = { ...reservedOf(col) };
|
|
643
831
|
if (position === 'top') {
|
|
@@ -667,11 +855,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
667
855
|
}
|
|
668
856
|
floatReserved.set(col, r);
|
|
669
857
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
(page
|
|
858
|
+
// A gallery page keeps no text room between its bands.
|
|
859
|
+
if (galleryFill)
|
|
860
|
+
for (const col of targetCols)
|
|
861
|
+
col.availableHeight = 0;
|
|
862
|
+
commitFloatBlock(page, first, built, xLeft, y, width);
|
|
675
863
|
floatsPlaced++;
|
|
676
864
|
return rest ? { rest } : 'placed';
|
|
677
865
|
};
|
|
@@ -715,11 +903,22 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
715
903
|
* is set. A float that does not fit holds up the ones behind it in its
|
|
716
904
|
* numbering sequence (see `heldBack`), never the other sequence. */
|
|
717
905
|
const flushFloatsIntoPage = (page) => {
|
|
718
|
-
if (pendingFloats.length === 0)
|
|
906
|
+
if (pendingFloats.length === 0) {
|
|
907
|
+
flushSideBoxesIntoPage(page);
|
|
719
908
|
return;
|
|
720
|
-
|
|
909
|
+
}
|
|
910
|
+
// A floated box heading the page goes first: the figures then take
|
|
911
|
+
// the foot under it (a figure set first would claim the foot and push
|
|
912
|
+
// the box on by the text-room rule). Stable: sequences keep their order.
|
|
913
|
+
const headFirst = (f) => (f.callout && f.position === 'top' ? 0 : 1);
|
|
914
|
+
pendingFloats.sort((a, b) => headFirst(a) - headFirst(b));
|
|
915
|
+
const textCols = page.columns.filter((c) => c.kind !== 'span' && c.kind !== 'side');
|
|
721
916
|
if (textCols.length === 0)
|
|
722
917
|
return;
|
|
918
|
+
const sideCols = sideColumns(page);
|
|
919
|
+
/** A page-span float on a fresh page takes the band of every column,
|
|
920
|
+
* the side column included. */
|
|
921
|
+
const pageCols = [...textCols, ...sideCols];
|
|
723
922
|
/** The least reserved text column a float may take (the rest of a
|
|
724
923
|
* split table: only columns after its previous slice on this page). */
|
|
725
924
|
const leastReserved = (f) => {
|
|
@@ -739,37 +938,62 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
739
938
|
}
|
|
740
939
|
return best;
|
|
741
940
|
};
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
941
|
+
// Order of the passes: page-span floats take the outer bands; column
|
|
942
|
+
// floats whose caption goes to the side column reserve it before the
|
|
943
|
+
// waiting side boxes and side floats stack there; the rest follow.
|
|
944
|
+
let sideBoxesFlushed = false;
|
|
945
|
+
for (let stage = 0; stage < 3; stage++) {
|
|
946
|
+
// Stage 0: page-span floats alone, until none is left to place (they
|
|
947
|
+
// take the outer bands of every column, the side column included);
|
|
948
|
+
// stage 1: column floats whose caption goes to the side column (they
|
|
949
|
+
// reserve their band there); then the waiting side boxes; stage 2:
|
|
950
|
+
// the side floats and the other column floats.
|
|
951
|
+
const passes = stage === 0 ? ['page'] : stage === 1 ? ['aside'] : ['side', 'column'];
|
|
952
|
+
if (stage === 2 && !sideBoxesFlushed) {
|
|
953
|
+
flushSideBoxesIntoPage(page);
|
|
954
|
+
sideBoxesFlushed = true;
|
|
955
|
+
}
|
|
956
|
+
for (let progress = true; progress;) {
|
|
957
|
+
const before = floatsPlaced;
|
|
958
|
+
for (const pass of passes) {
|
|
959
|
+
let i = 0;
|
|
960
|
+
while (i < pendingFloats.length) {
|
|
961
|
+
const f = pendingFloats[i];
|
|
962
|
+
const isPageSpan = f.span === 'page' && (textCols.length > 1 || sideCols.length > 0);
|
|
963
|
+
const isSide = !isPageSpan && f.span === 'side' && sideCols.length > 0;
|
|
964
|
+
const kind = isPageSpan ? 'page' : isSide ? 'side' : f.captionSide && sideCols.length > 0 ? 'aside' : 'column';
|
|
965
|
+
if (kind !== pass || heldBack(i)) {
|
|
966
|
+
i++;
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
// A page-span rest never shares the page of its previous slice.
|
|
970
|
+
if (isPageSpan && f.notBefore?.pageIndex === page.index) {
|
|
971
|
+
i++;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
let r = 'defer';
|
|
975
|
+
if (isSide) {
|
|
976
|
+
r = placeFloatInColumns(page, f, [sideCols[0]], 'top', false, 'fresh', false, true);
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
for (const pos of positionsFor(f)) {
|
|
980
|
+
const col = isPageSpan ? undefined : leastReserved(f);
|
|
981
|
+
if (!isPageSpan && !col)
|
|
982
|
+
break;
|
|
983
|
+
const cols = isPageSpan ? pageCols : [col];
|
|
984
|
+
r = placeFloatInColumns(page, f, cols, pos, isPageSpan, 'fresh');
|
|
985
|
+
if (r !== 'defer')
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
i = settle(i, r);
|
|
767
990
|
}
|
|
768
|
-
i = settle(i, r);
|
|
769
991
|
}
|
|
992
|
+
progress = floatsPlaced > before;
|
|
770
993
|
}
|
|
771
|
-
progress = floatsPlaced > before;
|
|
772
994
|
}
|
|
995
|
+
if (!sideBoxesFlushed)
|
|
996
|
+
flushSideBoxesIntoPage(page);
|
|
773
997
|
};
|
|
774
998
|
/** The keep-together box content block `idx` opens, when it is one that
|
|
775
999
|
* lays out inline in a column of the current band (`span: 'column'`,
|
|
@@ -787,7 +1011,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
787
1011
|
if (placement !== 'here')
|
|
788
1012
|
return null;
|
|
789
1013
|
const page = doc.pages[cursor.pageIndex];
|
|
790
|
-
if (span === 'page' &&
|
|
1014
|
+
if (span === 'page' && multiColumnBand(page))
|
|
1015
|
+
return null;
|
|
1016
|
+
if (span === 'side' && sideColumnOf(page, currentBand(page, cursor)))
|
|
791
1017
|
return null;
|
|
792
1018
|
const L = makeCalloutLayouter(idx, plan, style);
|
|
793
1019
|
const heights = new Map();
|
|
@@ -859,13 +1085,19 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
859
1085
|
// itself, which cuts the band under the text and sets the figure
|
|
860
1086
|
// there, above the box (`placeSpanFloatsAtCut`) — its only slot here
|
|
861
1087
|
// would be the band's foot, under the box.
|
|
862
|
-
if (preferTop && f.span === 'page' &&
|
|
1088
|
+
if (preferTop && f.span === 'page' && multiColumnBand(page)) {
|
|
863
1089
|
i++;
|
|
864
1090
|
continue;
|
|
865
1091
|
}
|
|
866
1092
|
if (preferTop)
|
|
867
1093
|
slots = [...slots.filter((s) => s.position === 'top'), ...slots.filter((s) => s.position !== 'top')];
|
|
868
1094
|
for (const slot of slots) {
|
|
1095
|
+
if (slot.side) {
|
|
1096
|
+
r = placeFloatInColumns(page, f, slot.cols, slot.position, false, 'strict', false, true, slot.refY);
|
|
1097
|
+
if (r !== 'defer')
|
|
1098
|
+
break;
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
869
1101
|
if (box && slotStarvesBox(page, f, slot, box, preferTop))
|
|
870
1102
|
continue;
|
|
871
1103
|
r = placeFloatInColumns(page, f, slot.cols, slot.position, slot.pageSpan, 'strict', preferTop);
|
|
@@ -890,10 +1122,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
890
1122
|
if (!style)
|
|
891
1123
|
return false;
|
|
892
1124
|
const { span, placement } = resolveCalloutAttrs(style, plan.attrs);
|
|
893
|
-
if (span !== 'page' || placement
|
|
1125
|
+
if (span !== 'page' || placement !== 'here')
|
|
894
1126
|
return false;
|
|
895
|
-
|
|
896
|
-
return bandColumns(page, currentBand(page, cursor)).length > 1;
|
|
1127
|
+
return multiColumnBand(doc.pages[cursor.pageIndex]);
|
|
897
1128
|
};
|
|
898
1129
|
/** Chapter barrier: place every pending float before the boundary — in
|
|
899
1130
|
* the current page's free slots, then on fresh pages opened ahead of it
|
|
@@ -902,7 +1133,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
902
1133
|
const drainPendingFloats = () => {
|
|
903
1134
|
tryPlacePendingFloatsOnCurrentPage();
|
|
904
1135
|
let guard = 0;
|
|
905
|
-
while (pendingFloats.length > 0 && guard++ < 1000) {
|
|
1136
|
+
while ((pendingFloats.length > 0 || pendingSideBoxes.length > 0) && guard++ < 1000) {
|
|
906
1137
|
const before = floatsPlaced;
|
|
907
1138
|
const startPageIndex = cursor.pageIndex;
|
|
908
1139
|
do {
|
|
@@ -1252,7 +1483,18 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1252
1483
|
if (c.type !== 'directive' && !isMarkerBlock(c))
|
|
1253
1484
|
realAt.push(k);
|
|
1254
1485
|
});
|
|
1255
|
-
|
|
1486
|
+
// `:::columns` groups among the children, as [start marker, end marker]
|
|
1487
|
+
// positions: a split never cuts inside one.
|
|
1488
|
+
const groups = [];
|
|
1489
|
+
children.forEach((c, k) => {
|
|
1490
|
+
if (c.type === 'containerStart' && c.containerName === 'columns') {
|
|
1491
|
+
let e = k + 1;
|
|
1492
|
+
while (e < children.length && !(children[e].type === 'containerEnd' && children[e].containerName === 'columns' && children[e].containerId === c.containerId))
|
|
1493
|
+
e++;
|
|
1494
|
+
groups.push([k, e]);
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
const layoutRange = (from, to, width, frameId, continuation, mirrored = false) => {
|
|
1256
1498
|
let n = 0;
|
|
1257
1499
|
// A cut inside child `to.child` includes that child (its first
|
|
1258
1500
|
// `to.line` lines); a cut at a child's head excludes it.
|
|
@@ -1272,9 +1514,20 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1272
1514
|
paragraphStyleFor: (idx) => paragraphContainers.byBlock[idx]?.style,
|
|
1273
1515
|
...(from.line > 0 ? { lineFrom: from.line } : {}),
|
|
1274
1516
|
...(to.line > 0 ? { lineTo: to.line } : {}),
|
|
1517
|
+
mirrored,
|
|
1275
1518
|
});
|
|
1276
1519
|
};
|
|
1277
|
-
return { children, childBase: startIdx + 1, realAt, end: { child: children.length, line: 0 }, layoutRange };
|
|
1520
|
+
return { children, childBase: startIdx + 1, realAt, end: { child: children.length, line: 0 }, layoutRange, groups };
|
|
1521
|
+
};
|
|
1522
|
+
/** Whether a page is a verso of mirrored margins (an `'outer'` corner
|
|
1523
|
+
* icon hangs on the left there). */
|
|
1524
|
+
const mirroredOf = (page) => resolved.page.margins.mirror === true && (page.index + pageIndexOffset + 1) % 2 === 0;
|
|
1525
|
+
/** Used bottom of a band, the float-only side column's stack included:
|
|
1526
|
+
* a span block cuts under the side boxes already set there. */
|
|
1527
|
+
const bandUsedBottomWithSide = (page, cols) => {
|
|
1528
|
+
const side = sideColumnOf(page, cols[0]?.band ?? 0);
|
|
1529
|
+
const withSide = side && sideUsedBottom(side) > side.bbox.y + 0.5 ? [...cols, side] : cols;
|
|
1530
|
+
return bandUsedBottom(withSide);
|
|
1278
1531
|
};
|
|
1279
1532
|
/** The longest leading fragment of the box from cut `from` on whose box
|
|
1280
1533
|
* is at most `roomPx` tall. Cuts fall between children or between the
|
|
@@ -1284,8 +1537,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1284
1537
|
* candidates (box bottom = content bottom at the cut + the box's tail
|
|
1285
1538
|
* below its last child); the deepest candidate that fits is laid out
|
|
1286
1539
|
* for real and taken when it truly fits. `null` when none does. */
|
|
1287
|
-
const splitCalloutFragment = (L, from, width, roomPx, frameId, continuation, minLines) => {
|
|
1288
|
-
const full = L.layoutRange(from, L.end, width, frameId, continuation);
|
|
1540
|
+
const splitCalloutFragment = (L, from, width, roomPx, frameId, continuation, minLines, mirrored = false) => {
|
|
1541
|
+
const full = L.layoutRange(from, L.end, width, frameId, continuation, mirrored);
|
|
1289
1542
|
const lastChild = full.children[full.children.length - 1];
|
|
1290
1543
|
if (!lastChild)
|
|
1291
1544
|
return null;
|
|
@@ -1313,13 +1566,14 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1313
1566
|
}
|
|
1314
1567
|
}
|
|
1315
1568
|
const min = Math.max(1, minLines);
|
|
1569
|
+
const insideGroup = (cut) => L.groups.some(([gs, ge]) => (cut.line === 0 ? gs < cut.child && cut.child <= ge : gs < cut.child && cut.child < ge));
|
|
1316
1570
|
const viable = candidates
|
|
1317
|
-
.filter((c) => c.headLines >= min && totalLines - c.headLines >= min)
|
|
1571
|
+
.filter((c) => c.headLines >= min && totalLines - c.headLines >= min && !insideGroup(c.cut))
|
|
1318
1572
|
.sort((a, b) => b.bottom - a.bottom);
|
|
1319
1573
|
for (const c of viable) {
|
|
1320
1574
|
if (c.bottom + tail > roomPx + 0.01)
|
|
1321
1575
|
continue;
|
|
1322
|
-
const result = L.layoutRange(from, c.cut, width, frameId, continuation);
|
|
1576
|
+
const result = L.layoutRange(from, c.cut, width, frameId, continuation, mirrored);
|
|
1323
1577
|
if (result.totalHeight <= roomPx + 0.01)
|
|
1324
1578
|
return { to: c.cut, result };
|
|
1325
1579
|
}
|
|
@@ -1456,7 +1710,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1456
1710
|
i++;
|
|
1457
1711
|
continue;
|
|
1458
1712
|
}
|
|
1459
|
-
const cutY = gridUp(page,
|
|
1713
|
+
const cutY = gridUp(page, bandUsedBottomWithSide(page, cols));
|
|
1460
1714
|
const spacing = cols.some((c) => c.blocks.length > 0) ? floatGapPx : 0;
|
|
1461
1715
|
const need = needFor(spacing, measure.height, floatGapPx);
|
|
1462
1716
|
const bandBottom = Math.min(...cols.map((c) => columnBottom(c, uncappedBottoms)));
|
|
@@ -1496,7 +1750,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1496
1750
|
for (;;) {
|
|
1497
1751
|
let page = doc.pages[cursor.pageIndex];
|
|
1498
1752
|
const continuation = part > 0;
|
|
1499
|
-
const layoutAt = (width) => L.layoutRange(from, L.end, width, frameId, continuation);
|
|
1753
|
+
const layoutAt = (width) => L.layoutRange(from, L.end, width, frameId, continuation, mirroredOf(page));
|
|
1500
1754
|
const result = layoutAt(page.contentArea.width);
|
|
1501
1755
|
/** Where the box would cut the current band, and whether it fits (room
|
|
1502
1756
|
* is measured against the columns' TRUE bottoms — a capped band keeps
|
|
@@ -1507,7 +1761,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1507
1761
|
const cols = bandColumns(page, currentBand(page, cursor));
|
|
1508
1762
|
if (cols.length === 0 || (requireLevel && !levelForBox(cols)))
|
|
1509
1763
|
return null;
|
|
1510
|
-
const cutY = gridUp(page,
|
|
1764
|
+
const cutY = gridUp(page, bandUsedBottomWithSide(page, cols));
|
|
1511
1765
|
const bandHasContent = cols.some((c) => c.blocks.length > 0);
|
|
1512
1766
|
const spacing = bandHasContent ? Math.max(pendingSpacing, result.marginTopPx) : 0;
|
|
1513
1767
|
const need = needFor(spacing, result.totalHeight, result.marginBottomPx);
|
|
@@ -1611,14 +1865,18 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1611
1865
|
}
|
|
1612
1866
|
if (!action && forceHere && fit) {
|
|
1613
1867
|
action = { kind: 'whole', fit, need: fit.need, result };
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1868
|
+
// A box that fills its band to the last grid line is not an overflow.
|
|
1869
|
+
const overflowPx = Math.max(0, fit.spacing + result.totalHeight - fit.roomPx);
|
|
1870
|
+
if (overflowPx > 0.5) {
|
|
1871
|
+
(doc.warnings ??= []).push({
|
|
1872
|
+
kind: 'calloutOverflow',
|
|
1873
|
+
pageIndex: cursor.pageIndex,
|
|
1874
|
+
columnIndex: cursor.columnIndex,
|
|
1875
|
+
sourceStart: contentBlocks[startIdx].sourceStart + bodyOffset,
|
|
1876
|
+
sourceEnd: contentBlocks[plan.endIdx].sourceEnd + bodyOffset,
|
|
1877
|
+
overflowPx,
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1622
1880
|
}
|
|
1623
1881
|
if (action) {
|
|
1624
1882
|
if (capActive) {
|
|
@@ -1899,6 +2157,92 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1899
2157
|
for (let i = startIdx + 1; i <= plan.endIdx; i++)
|
|
1900
2158
|
enqueueFloatsFor(i);
|
|
1901
2159
|
};
|
|
2160
|
+
/**
|
|
2161
|
+
* Set a `span: 'side'` box in the side column of the current band: laid
|
|
2162
|
+
* out at the side column's width and stacked under what the column holds,
|
|
2163
|
+
* no higher than the flow's current position (beside the text it
|
|
2164
|
+
* interrupts), consuming the column's free height like a side float. A
|
|
2165
|
+
* box the rest of the column cannot hold waits for the side column of the
|
|
2166
|
+
* next page the flow opens (`flushSideBoxesIntoPage`), where it is set
|
|
2167
|
+
* anyway — overflowing — when even an empty column cannot hold it. The
|
|
2168
|
+
* frame and its children leave the flow into `page.floats`, as a fixed
|
|
2169
|
+
* box does. Returns false when the page has no side column: the box then
|
|
2170
|
+
* lays out inline.
|
|
2171
|
+
*/
|
|
2172
|
+
const trySideBox = (page, side, box, refY, mode) => {
|
|
2173
|
+
const { startIdx, plan, style } = box;
|
|
2174
|
+
const L = makeCalloutLayouter(startIdx, plan, style);
|
|
2175
|
+
const frameId = `block-${blockIdCounter++}`;
|
|
2176
|
+
const result = L.layoutRange(CUT_START, L.end, side.bbox.width, frameId, false, mirroredOf(page));
|
|
2177
|
+
const used = sideUsedBottom(side);
|
|
2178
|
+
const raw = Math.max(used, refY ?? used);
|
|
2179
|
+
const gridUpSide = (v) => page.contentArea.y + Math.ceil((v - page.contentArea.y - 0.01) / baselineGrid) * baselineGrid;
|
|
2180
|
+
let y = gridUpSide(raw);
|
|
2181
|
+
const below = Math.max(result.marginBottomPx, floatGapPx);
|
|
2182
|
+
let need = y - used + result.totalHeight + below;
|
|
2183
|
+
// The gap under the box is owed only to what follows: a box whose foot
|
|
2184
|
+
// lands on the column's foot needs none.
|
|
2185
|
+
const fits = () => need <= side.availableHeight + 0.01 || y - used + result.totalHeight <= side.availableHeight + 0.01;
|
|
2186
|
+
if (!fits() && refY !== undefined && raw > used + 0.5) {
|
|
2187
|
+
// Beside its text the box runs off the column: it slides up — as far
|
|
2188
|
+
// as the stack above allows — to the lowest position that fits, its
|
|
2189
|
+
// foot on the column's foot (the bottom-aligned marginal box).
|
|
2190
|
+
const foot = side.bbox.y + side.bbox.height;
|
|
2191
|
+
const fit = page.contentArea.y + Math.floor((foot - result.totalHeight - page.contentArea.y + 0.01) / baselineGrid) * baselineGrid;
|
|
2192
|
+
if (fit >= used - 0.01) {
|
|
2193
|
+
y = fit;
|
|
2194
|
+
need = y - used + result.totalHeight + below;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (!fits()) {
|
|
2198
|
+
if (mode === 'strict' || used > side.bbox.y + 0.5)
|
|
2199
|
+
return false;
|
|
2200
|
+
}
|
|
2201
|
+
const frame = result.frame;
|
|
2202
|
+
stampCalloutSource(frame, startIdx, plan);
|
|
2203
|
+
frame.pageIndex = page.index;
|
|
2204
|
+
frame.columnIndex = side.index;
|
|
2205
|
+
offsetCalloutToAbsolute(result, side.bbox.x, y);
|
|
2206
|
+
const floats = (page.floats ??= []);
|
|
2207
|
+
doc.blocks.push(frame);
|
|
2208
|
+
floats.push(frame);
|
|
2209
|
+
for (const child of result.children) {
|
|
2210
|
+
child.pageIndex = frame.pageIndex;
|
|
2211
|
+
child.columnIndex = frame.columnIndex;
|
|
2212
|
+
doc.blocks.push(child);
|
|
2213
|
+
floats.push(child);
|
|
2214
|
+
}
|
|
2215
|
+
side.availableHeight = Math.max(0, side.availableHeight - need);
|
|
2216
|
+
for (let i = startIdx + 1; i <= plan.endIdx; i++)
|
|
2217
|
+
enqueueFloatsFor(i);
|
|
2218
|
+
return true;
|
|
2219
|
+
};
|
|
2220
|
+
const placeCalloutSide = (startIdx, plan, style) => {
|
|
2221
|
+
const page = doc.pages[cursor.pageIndex];
|
|
2222
|
+
const side = sideColumnOf(page, currentBand(page, cursor));
|
|
2223
|
+
if (!side)
|
|
2224
|
+
return false;
|
|
2225
|
+
const curCol = currentColumn(doc, cursor);
|
|
2226
|
+
const refY = curCol.bbox.y + (curCol.bbox.height - curCol.availableHeight) + (curCol.blocks.length > 0 ? pendingSpacing : 0);
|
|
2227
|
+
if (!trySideBox(page, side, { startIdx, plan, style }, refY, 'strict')) {
|
|
2228
|
+
pendingSideBoxes.push({ startIdx, plan, style });
|
|
2229
|
+
}
|
|
2230
|
+
return true;
|
|
2231
|
+
};
|
|
2232
|
+
/** Set the waiting side boxes in the side column of a freshly opened
|
|
2233
|
+
* page, in order; one that does not fit an empty column is set anyway. */
|
|
2234
|
+
const flushSideBoxesIntoPage = (page) => {
|
|
2235
|
+
if (pendingSideBoxes.length === 0)
|
|
2236
|
+
return;
|
|
2237
|
+
const side = sideColumnOf(page, 0);
|
|
2238
|
+
if (!side)
|
|
2239
|
+
return;
|
|
2240
|
+
while (pendingSideBoxes.length > 0) {
|
|
2241
|
+
if (!trySideBox(page, side, pendingSideBoxes[0], undefined, 'fresh'))
|
|
2242
|
+
break;
|
|
2243
|
+
pendingSideBoxes.shift();
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
1902
2246
|
/**
|
|
1903
2247
|
* Place a `:::callout` inline at the current column width as one atomic
|
|
1904
2248
|
* unit: the frame block followed by its children in the same column. The
|
|
@@ -1923,22 +2267,35 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1923
2267
|
const firstFrameId = `block-${blockIdCounter++}`;
|
|
1924
2268
|
const { span, placement } = resolveCalloutAttrs(style, plan.attrs);
|
|
1925
2269
|
const L = makeCalloutLayouter(startIdx, plan, style);
|
|
1926
|
-
const layoutAt = (width) => L.layoutRange(CUT_START, L.end, width, firstFrameId, false);
|
|
2270
|
+
const layoutAt = (width) => L.layoutRange(CUT_START, L.end, width, firstFrameId, false, mirroredOf(doc.pages[cursor.pageIndex]));
|
|
1927
2271
|
// Fixed boxes leave the flow entirely.
|
|
1928
2272
|
if (placement === 'fixed') {
|
|
1929
2273
|
placeCalloutFixed(startIdx, plan, style, layoutAt);
|
|
1930
2274
|
return undefined;
|
|
1931
2275
|
}
|
|
2276
|
+
// Floated boxes leave it too: they take the first free band after
|
|
2277
|
+
// this point (the foot of the current page, or the head / foot of a
|
|
2278
|
+
// page the flow opens later) and the text after them fills the page.
|
|
2279
|
+
// A side box always stacks beside the text it interrupts.
|
|
2280
|
+
if ((placement === 'top' || placement === 'bottom') && span !== 'side') {
|
|
2281
|
+
enqueueCalloutFloat(startIdx, plan, style, L, placement, span);
|
|
2282
|
+
return undefined;
|
|
2283
|
+
}
|
|
1932
2284
|
// Page-span boxes split a multi-column page into column bands (stage 1
|
|
1933
2285
|
// of span blocks). Floating placements keep the inline fallback.
|
|
1934
2286
|
{
|
|
1935
2287
|
const page = doc.pages[cursor.pageIndex];
|
|
1936
2288
|
if (span === 'page'
|
|
1937
2289
|
&& placement === 'here'
|
|
1938
|
-
&&
|
|
2290
|
+
&& multiColumnBand(page)
|
|
1939
2291
|
&& placeCalloutSpan(startIdx, plan, style, L, firstFrameId)) {
|
|
1940
2292
|
return undefined;
|
|
1941
2293
|
}
|
|
2294
|
+
// Side boxes stack in the float-only side column beside the text
|
|
2295
|
+
// (whatever their placement: a side box never floats to a band).
|
|
2296
|
+
if (span === 'side' && placeCalloutSide(startIdx, plan, style)) {
|
|
2297
|
+
return undefined;
|
|
2298
|
+
}
|
|
1942
2299
|
}
|
|
1943
2300
|
const splittable = !style.keepTogether;
|
|
1944
2301
|
let from = CUT_START;
|
|
@@ -1949,7 +2306,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1949
2306
|
for (;;) {
|
|
1950
2307
|
let curCol = currentColumn(doc, cursor);
|
|
1951
2308
|
const continuation = part > 0;
|
|
1952
|
-
const result = L.layoutRange(from, L.end, curCol.bbox.width, frameId, continuation);
|
|
2309
|
+
const result = L.layoutRange(from, L.end, curCol.bbox.width, frameId, continuation, mirroredOf(doc.pages[cursor.pageIndex]));
|
|
1953
2310
|
// Column balancing: a box closing its column takes the column's gap
|
|
1954
2311
|
// above it (the trailing-callout lever), so its foot lands on the
|
|
1955
2312
|
// last grid slot — level with the column beside it. Any fragment
|
|
@@ -1965,7 +2322,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1965
2322
|
// The (rest of the) box does not fit the column: a splittable box
|
|
1966
2323
|
// leaves the head that fits here…
|
|
1967
2324
|
if (splittable)
|
|
1968
|
-
fragment = splitCalloutFragment(L, from, curCol.bbox.width, roomPx, frameId, continuation, style.splitMinLines);
|
|
2325
|
+
fragment = splitCalloutFragment(L, from, curCol.bbox.width, roomPx, frameId, continuation, style.splitMinLines, mirroredOf(doc.pages[cursor.pageIndex]));
|
|
1969
2326
|
// …otherwise it moves whole to the next column — also out of an
|
|
1970
2327
|
// EMPTY column that float bands or a band cap have cut short, when
|
|
1971
2328
|
// a full column would hold it (bounded, so a run of short columns
|
|
@@ -2260,6 +2617,14 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2260
2617
|
const isContainerTail = paragraphContainer !== undefined
|
|
2261
2618
|
&& nextRaw?.type === 'containerEnd'
|
|
2262
2619
|
&& nextRaw.containerId === paragraphContainer.id;
|
|
2620
|
+
// A page-span box that left no band under it keeps the cursor on its
|
|
2621
|
+
// (full) span column. Move on before measuring: a block measured at the
|
|
2622
|
+
// span width and placed in the next page's text column would carry
|
|
2623
|
+
// lines wider than that column.
|
|
2624
|
+
if (currentColumn(doc, cursor).kind === 'span') {
|
|
2625
|
+
pendingSpacing = 0;
|
|
2626
|
+
advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
2627
|
+
}
|
|
2263
2628
|
// Measure against the current column width. `null` means there is nothing
|
|
2264
2629
|
// to place inline (empty text, unknown resource id, floated resource).
|
|
2265
2630
|
const col = currentColumn(doc, cursor);
|
|
@@ -2313,7 +2678,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2313
2678
|
segments: [],
|
|
2314
2679
|
isLastLine: true,
|
|
2315
2680
|
}];
|
|
2316
|
-
|
|
2681
|
+
// An inline resource keeps the float gap (a line) above it, as a
|
|
2682
|
+
// float would, unless the block before asked for more.
|
|
2683
|
+
const spacingBefore = Math.max(pendingSpacing, floatGapPx);
|
|
2317
2684
|
enterBand(blockIdx, 0);
|
|
2318
2685
|
placeAtomicBlock(blk, groupHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx);
|
|
2319
2686
|
enterBand(blockIdx, 0);
|
|
@@ -2383,7 +2750,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
2383
2750
|
const nextIsHeading = nextBlock?.type === 'heading';
|
|
2384
2751
|
// The contents (`:::toc`) keep their own rhythm: an entry set as a list
|
|
2385
2752
|
// item is not a list tail to realign the text after it.
|
|
2386
|
-
const shouldSnapToGrid = rawBlock.toc === undefined && ((vdtType === 'heading' && !nextIsHeading) ||
|
|
2753
|
+
const shouldSnapToGrid = rawBlock.toc === undefined && ((vdtType === 'heading' && !nextIsHeading && resolved.headings.snapToGrid) ||
|
|
2387
2754
|
(vdtType === 'listItem' && !nextIsListItem) ||
|
|
2388
2755
|
(vdtType === 'paragraph' && isContainerTail) ||
|
|
2389
2756
|
vdtType === 'mathDisplay');
|