/** * Vertical column balancing — editorial bottom alignment. * * Publishers expect every column of a page to start at the top of the page * and end flush with the bottom. The placement pass naturally leaves short * columns: orphan/widow rules, keep-with-next headings, and unsplittable * groups all push content to the next column before the current one is full, * leaving one or more empty grid lines at its bottom. * * The fix mirrors what a human compositor does: add whole baseline-grid * lines to the top margin of the headings inside the short column, pushing * the content below them down until the column ends on the last grid slot. * Because the column break is element-bound (the element that opens the next * column did not fit in the gap, so it still won't fit in a smaller one), * shifting a column's tail down by at most its gap never moves content into * the next column or page — the adjustment is local and exact. * * When a column needs several lines and holds several headings, the extra * lines are distributed round-robin in importance order (lowest heading * level first), so the most important heading always receives the largest * share — e.g. 3 lines over an h2 and an h3 become +2 above the h2 and +1 * above the h3. A heading takes at most `maxLinesBeforeLoose` lines (two) * that way before the levers below are tried, and its further lines, up to * `maxLinesPerHeading`, only when they have nothing left to give (#707). * * When the column's headings cannot absorb the whole gap, further levers * apply in editorial priority order: * - a picture with a safe area (`Resource.safeArea`, #442) set inline in * the column, or floated at its head or foot over that column alone, * grows by whole grid lines, cropped within its safe area: a taller * picture leaves no hole anywhere, so it is tried right after the * closing box and before any space is added; * - first of all, a callout box closing the column takes the room under * its foot as space above it, so the foot lands on the last grid slot of * the page, level with the last line of the column beside it (with * `headings.balancing.closingBox: 'last'` it runs after the spacing * levers and takes only what they leave; with `'off'` never); * - extra grid lines where a list/enumeration ends (space after a list * reads naturally), capped per list end; * - one grid line under a display formula or a callout box (it reads as * the element's own margin), unless `stretchAfterDisplays` is off; * - extra grid lines between a top float band (a figure or table at the * head of the column) and the text under it, capped per band — a * little more air under a figure is invisible. Under a figure across * several columns that holds only while their heads stay level: the * columns under it move together, or one moves alone to end flush * (`stretchAfterWideFloats`, #708); * - as a last resort, up to `maxLooseParagraphs` paragraphs of the column * are each re-broken one line looser (TeX \looseness=+1) via the * Knuth-Plass `looseness` option — always within the configured * `maxWordSpacing`, so type colour never exceeds the user's limit. When * word spacing alone cannot gain the line, the placement pass also tries * a little positive tracking on that paragraph (the compositor's classic * fix), keeping the smallest value that works, never above `maxTracking`. * * On a character grid (`cjk.grid`, #632) every character stands in a cell * one em wide and every line on the grid's line pitch, which is the * baseline grid: the levers that move text by whole grid lines keep it on * the grid, and `headings.balancing.gridLines: 'off'` keeps them out (a * standard that counts lines has no empty row to give). The closing box * and the pictures with a safe area are off the grid by design and work as * anywhere. A loose paragraph is the exception: a CJK paragraph gains its * line by breaking each line short and spreading the characters over the * measure, which moves them off their cells; there it is accepted only * when no gap between characters spreads past `maxTracking`, and no * tracking is added to its letters (`measureLooseParagraph`). Balancing is * off by default on a grid (`balancingOnByDefault`). */ import type { BalanceLever, VDTDocument } from '../vdt.js'; import type { ClosingBoxLever, StretchAfterWideFloats } from '../types.js'; /** Balancing attempts a segment of pages (see {@link pageSegments}) may * spend: passes in which it tried new levers. */ export declare const MAX_BALANCING_PASSES = 10; /** Hard cap on the balancing passes of one document, whatever its segments * still want to try — every pass places the whole document. */ export declare const MAX_BALANCING_PASSES_PER_DOCUMENT: number; /** Inclusive run of page indices. */ export interface PageRange { from: number; to: number; } /** * The runs of pages laid out independently of one another: every page in * `forcedBreakPages` (a `:::pagebreak`, a heading `breakBefore`, a chapter * opener) closes a segment and the next page opens another. Nothing flows * across such a break, so a balancing lever inside one segment cannot * move a line of any other — the convergence loop judges each on its own. */ export declare function pageSegments(pageCount: number, forcedBreakPages: ReadonlySet): PageRange[]; /** Empty grid lines over the balanceable columns of the pages in `range`. */ export declare function gapLinesIn(gaps: readonly ColumnGap[], range: PageRange): number; /** Room (px) still open under the callout boxes that close a column of the * pages in `range` — the `trailingCallout` candidates' gaps. Often less * than a line, so {@link gapLinesIn} does not see it: the balancing loop * uses it to break a tie between two layouts with the same empty lines. */ export declare function boxRoomIn(gaps: readonly ColumnGap[], range: PageRange): number; /** Lever keys (see {@link balanceKey}) of the boxes closing a column — the * `trailingCallout` candidates of `gaps`. */ export declare function boxLeverKeys(gaps: readonly ColumnGap[]): Set; /** The kind of stretch point a candidate is: one of the balancing levers, * which the placement pass records on the block it applies to * (`VDTBlock.balancing`). */ export type BalanceCandidateKind = BalanceLever; interface BalanceCandidate { /** Stable content-block index keying the adjustment across passes. */ contentIndex: number; kind: BalanceCandidateKind; /** Heading level (1–6) for `kind: 'heading'`; 0 otherwise. */ level: number; /** Reading-order tiebreak within the column. */ order: number; /** Line count of the block — loose-paragraph preference (more lines = * more glue = least visible loosening). */ lineCount: number; /** `trailingCallout` only: room (px) between the box's foot and the last * grid slot of the column — the exact space to add above the box. */ gapPx?: number; /** The fragment of a split block the candidate is (0 = the block or its * head): the fragments of one split box — or of a paragraph continuing * under a float band — share a content index and are levered on their * own (see {@link balanceKey}). */ part?: number; /** `flexFigure`: the lever's own key ({@link flexFigureKey} / * {@link flexFloatKey}), apart from the spacing keys of the blocks. */ key?: number; /** `flexFigure`: whole grid lines the picture can still grow. */ maxLines?: number; /** `afterFloat`, when the float the column starts under also stands over * another text column of its band (#708): a key the columns under that * float share, and how many text columns it stands over. */ wideFloat?: { key: string; under: number; }; } /** The key a candidate's adjustment is stored under. */ export declare function candidateKey(c: { contentIndex: number; part?: number; key?: number; }): number; /** Key of the `flexFigure` lever of an inline picture: its lines are body * growth, not space above (a part no split block reaches). */ export declare function flexFigureKey(contentIndex: number): number; /** Key of the `flexFigure` lever of a floated picture, from its resource * id (floats carry no content index): below every block key. */ export declare function flexFloatKey(resourceId: string): number; /** Key of a balancing adjustment: the content index of the block, or, for a * continuation fragment (of a split callout, or of a paragraph resuming * under a float band), a composite that keeps the fragments of one block * apart — they share its content index. */ export declare function balanceKey(contentIndex: number, part: number): number; export interface ColumnGap { pageIndex: number; columnIndex: number; /** Whole empty grid lines at the bottom of the column. */ gapLines: number; /** Stretch points inside this column that can absorb extra lines. */ candidates: BalanceCandidate[]; } /** * Collect the bottom gaps of every balanceable column. * * A column is balanceable when content flowed past it into a later column of * the same page, or — for the page's last non-empty column — when the page * flows naturally into a later content page (no `:::pagebreak`, heading * `breakBefore`, or chapter opener forced the break, and it is not the last * content page: a short final column at the end of a chapter is legitimate). * * Eligible headings are heading blocks that are neither the first block of * their column (their top margin is suppressed there so the column keeps * starting at the page top) nor the last (space above a trailing heading * would strand it at the column bottom); a heading that opens a column * under a float band is eligible too (it is a heading lever, not an * after-float point). In a closing band (a page that does not flow on, a * band cut level by a trailing cap) the heading at its column's head keeps * its place, under a float band and under a page-span opener alike: the * hidden block the opener leaves in its own column does not make the * heading after it a mid-column one (#709). List-end points are the first * non-list block after a run of list items — they may be last in column * (pushing a trailing paragraph down by at most the gap is local: the * element that opened the next column still does not fit). After-float * points are the first block of a column that starts under a float band (a * whole block, not a continuation, and not a callout). Loose-paragraph * candidates are justified paragraphs wholly contained in the column (never * split parts — loosening those would reshuffle lines across columns); with * `headings.balancing.fillShortColumns` (#1236) ragged ones too, when * Knuth–Plass breaks them (`bodyText.optimalRagged`). */ export declare function collectColumnGaps(doc: VDTDocument, forcedBreakPages: ReadonlySet): ColumnGap[]; /** Total empty grid lines across all balanceable columns — the quantity the * convergence loop minimises. */ export declare function totalGapLines(doc: VDTDocument, forcedBreakPages: ReadonlySet): number; /** Adjustments already applied in the layout being inspected. */ export interface BalanceState { /** Extra spacing grid lines per content index (headings + list ends). */ lines: ReadonlyMap; /** Loose paragraphs: contentIndex → extra lines (1, or -1 for a * paragraph `fillShortColumns` sets a line short). */ loose: ReadonlyMap; } export interface BalanceProposalOptions { maxLinesPerHeading: number; /** `headings.balancing.maxLinesBeforeLoose` (#707): lines a heading may * take before the levers after it are tried. Omitted, the headings take * all their lines first. */ maxLinesBeforeLoose?: number; stretchAfterLists: boolean; maxLinesAfterList: number; /** `headings.balancing.stretchAfterDisplays` (#710); on when omitted. */ stretchAfterDisplays?: boolean; stretchAfterFloats: boolean; maxLinesAfterFloat: number; /** `headings.balancing.stretchAfterWideFloats` (#708): `'flush'` moves a * column down under a float over several columns only with the others * under it or to end it flush. Omitted, as `'always'`. */ stretchAfterWideFloats?: StretchAfterWideFloats; looseParagraphs: boolean; /** Loose paragraphs allowed per short column (one extra line each). */ maxLooseParagraphs: number; /** `bodyText.optimalLineBreaking` — the loose lever needs the K-P path. */ optimalLineBreaking: boolean; /** Loose candidates that failed to gain a line in a previous attempt. */ failedLoose: ReadonlySet; /** `headings.balancing.fillShortColumns` (#1236): when the column's loose * candidates have nothing left to give, offer one of them a line short * (looseness -1). Off when omitted. */ fillShortColumns?: boolean; /** Candidates set a line short in a previous attempt that lost their * line, or did not shorten the column's gap. */ failedTight?: ReadonlySet; /** Spacing candidates (headings, list ends, after-float points) that may * not take more lines than already applied: a pass that gave them more * moved content across a column break (see {@link firstDivergentColumn}) * and was discarded. */ failedLines?: ReadonlySet; /** Where the lever of a box closing a column runs * (`headings.balancing.closingBox`); the document's setting when * omitted. */ closingBox?: ClosingBoxLever; /** Whether the levers that add whole grid lines (headings, list ends, * after a display, after a float band) run: false on a character grid * with `headings.balancing.gridLines: 'off'` (#632). Default true. */ gridLines?: boolean; } /** The levers that add whole grid lines to a column, which * `headings.balancing.gridLines: 'off'` keeps out of a character grid. */ export declare const GRID_LINE_LEVERS: readonly BalanceCandidateKind[]; /** Position of a column in a document, in reading order. */ export interface ColumnPosition { pageIndex: number; columnIndex: number; } /** * First column, in reading order, whose content differs between two * layouts of the same document — where a balancing pass stopped being * local. Null when every column of `a` has its counterpart in `b`. With * `range`, only those pages of `a` are compared. */ export declare function firstDivergentColumn(a: VDTDocument, b: VDTDocument, range?: PageRange): ColumnPosition | null; /** Budget shared by the loose candidates of one column: the placement pass * tries them in order and stops loosening once `need` of them gained * their line, so offering every candidate at once costs no extra pass. */ export interface LooseBudget { /** Column identity (page × columns + column). */ group: number; /** Lines the column still needs from loose paragraphs. */ need: number; } export interface BalanceProposal { /** Extra spacing grid lines per content index (cumulative across passes). */ lines: Map; /** Loose paragraphs per content index (cumulative across passes): the * lines each runs long, -1 for one set a line short. */ loose: Map; /** Budget of the loose candidates newly offered by this proposal. */ looseBudget: Map; /** Whether the proposal adds anything over `current`. */ changed: boolean; } /** * Propose the next round of adjustments. Per column gap, levers apply in * strict editorial priority order, on top of the already-applied `current` * state: * 0. a callout box closing the column — the exact room under its foot * goes above it, so the foot meets the last grid slot (`closingBox: * 'last'` moves this step after step 3, where it takes the room the * whole lines before it left; `'off'` drops it); * 1. headings — round-robin in importance order (level asc, then reading * order), capped at `maxLinesBeforeLoose` lines each at this point and * at `maxLinesPerHeading` in step 5; * 2. list ends — round-robin in reading order, capped at * `maxLinesAfterList`; * 2b. after-display points — the block after a display formula or a * callout box, one line each (`stretchAfterDisplays: false` skips the * step); * 3. after-float points — the first block under a float band, capped at * `maxLinesAfterFloat`. Under a float over several text columns, with * `stretchAfterWideFloats: 'flush'` (#708), the columns under it take * the same number of lines together, and one takes lines alone only * when they close its gap; * 4. loose paragraphs — every eligible candidate of the column is offered, * longest first (most glue = least visible loosening), with a shared * budget: the pass stops loosening once the column has gained what it * needs, capped at `maxLooseParagraphs` (cumulative across passes). * Candidates that already failed to gain a line are skipped. With * `fillShortColumns` (#1236), once no candidate is left to run long, * every candidate the column has not set short yet is offered a line * short instead (budget one): the split after it takes the line the * widow and orphan rules held back, plus the one it gave up; * 5. headings again, up to `maxLinesPerHeading` (#707), only when a gap is * left and step 4 has nothing more to offer this column. */ export declare function proposeBalanceLines(doc: VDTDocument, forcedBreakPages: ReadonlySet, current: BalanceState, options: BalanceProposalOptions): BalanceProposal; export {}; //# sourceMappingURL=columnBalancing.d.ts.map