/** * Resource block layout (issue #49 §7 — Measurement). * * Turns a `Resource` + its computed number into a measured, placement-ready * {@link ResolvedResourceBlock}. The result is fully self-contained so the * canvas / PDF renderers can draw it synchronously without touching the parser * or numbering pipeline again. * * Layout rules for v1 (kept intentionally simple — see the issue plan): * - bitmap / svg: scale to the column width, preserving aspect ratio. The * height follows from the intrinsic dimensions; svgs without intrinsic * dimensions fall back to a 4:3 box at column width. * - table: columns split by `TableModel.columnWidths` weights (equal split * when unset); each cell measured via the shared rich-text measurer; row * height = max cell height (rowspans distribute across rows). * - caption: measured via the rich-text measurer with the caption font, with * the type's caption prefix + number prepended. Inline `:ref` spans resolve * to their computed label and are tagged so renderers can colour / link them. * Sits below the body by default or above it (`captionStyle.position`), * optionally on a background bar spanning the block width. A resource type * may override the caption style partially (`ResourceType.captionStyle`). * - note: an optional smaller run (`Resource.note`) placed under the body when * the caption is above, otherwise under the caption. * * Resource + caption + note are measured as one group; `totalHeight` is what * goes to placement. Figures paginate atomically; a table taller than the * page can be laid out as a *slice* of its rows (`ResourceLayoutInput.slice`) * — a continuation repeats the header rows and suffixes the caption, and a * slice that goes on sets a marker under it and holds the note back for the * last one. {@link planTableSlice} picks the rows a slice can carry from the * row metrics the full-table layout reports. */ import { type AnchorTargets, type CrossRefStrings } from './crossRefs.js'; import type { InlineSpan, SwatchBox, SwatchShape } from '../parse/index.js'; import type { ColorPaletteEntry, ResolvedCaptionStyleConfig, ResolvedMathConfig, Resource, ResourceRotation, ResourceType, TableModel } from '../types.js'; import type { ResolvedConfig, ResolvedResourceBlock, VDTLine } from '../vdt.js'; import type { MeasurementCache } from '../measure/index.js'; import type { ResourceNumberingMap } from './resourceNumbering.js'; import { type CaptionCitations } from './citations.js'; export interface ResourceLayoutInput { /** Where the caption, the note and the table cells are measured through * (#781): a table is laid out on every pass of every build. */ cache?: MeasurementCache; resource: Resource; resourceType: ResourceType | undefined; number: string; resolved: ResolvedConfig; /** Available column width in px — for a rotated block, the width of the * band it takes on the page (the extent its upright height may reach). */ columnWidth: number; /** Set the block turned a quarter turn on the page. Its upright frame is * then laid out `rotatedLength` px wide (the block's extent along the * page's height) and the block reports that length as its height on the * page; the upright height (the footprint's width) is * `block.rotation.height`. See {@link VDTResourceRotation}. */ rotate?: ResourceRotation; /** Upright layout width of a rotated block (px). Defaults to `columnWidth`. */ rotatedLength?: number; /** Full numbering map — lets inline `:ref`s inside the caption resolve. */ resourceNumbering: ResourceNumberingMap; resourceTypes: ResourceType[]; resources: Resource[]; /** The formatted citations of resource captions and notes, by resource * id (#529); a citation without one prints as written. */ captionCitations?: ReadonlyMap; /** Lay out only these rows of a table (a slice of a table split across * pages). Ignored for figures. */ slice?: TableSliceSpec; /** Set the caption (and note) beside the body instead of under it: in a * band `width` wide whose left edge is `dx` from the block's left (the * side column of a one-and-a-half layout), level with the body's top, * or with its bottom when `alignBottom`. The block's height is then the * body's alone; `asideHeight` reports the caption band's. */ captionAside?: { dx: number; width: number; alignBottom: boolean; offsetY?: number; }; /** Widest a figure's image (bitmap or SVG) may be set; the caption and * note keep `columnWidth`. Defaults to `columnWidth`. */ maxBodyWidth?: number; /** Tallest a picture's body (bitmap, SVG, video poster) may be set: a * float scaled to the room of its slot (`placement.shrink`, #626). The * picture is cropped within its safe area first (keeping its width), * then scaled, width and height together, never below `minBodyScale`; * the result records the scale (`shrinkScale`). The caption and note * are not counted: the caller takes them off the slot's room. Ignored * for tables and turned blocks. */ maxBodyHeight?: number; /** The smallest scale `maxBodyHeight` sets the picture at (0–1); the * body then stands taller than the cap. Default 0. */ minBodyScale?: number; /** Measure the caption and note at this width instead of the slot's, * set under the picture per `placement.align`: the layout's own rounds * for `placement.captionMeasure: 'body'`. */ captionWidth?: number; /** Make a picture with a safe area (`Resource.safeArea`) this many px * taller (or shorter, when negative) than it would be set, by cropping * outside its safe area; clamped to the range the safe area allows (see * `bodyFlex` in the result). Ignored for pictures without one, tables * and turned blocks. */ bodyHeightDelta?: number; /** Set the block upright on a vertical page (`VDTPage.flow`): laid out * in an upright frame counter-rotated in the flow (`rotation.direction: * 'ccw'`, which the page's clockwise frame turns back), so the picture * and its caption read as on a horizontal page. `columnWidth` (the * column's width in the flow: the tier's height on the sheet) bounds * the frame's height; its width is the picture's (a bitmap at its size, * an SVG as tall as the tier leaves room for with its caption), at most * `maxLength` px, and the block reports that width as its height in the * flow. A table is laid out `maxLength` wide, its rows cut to the tier * by the placer. `rotate` and `rotatedLength` are ignored. */ upright?: { maxLength: number; }; } /** The rows a table slice carries. `startRow > 0` makes it a continuation: * the header rows are repeated above `startRow` and the caption gets the * continued suffix. `continues` sets the marker under the slice (and holds * the note back). */ export interface TableSliceSpec { startRow: number; /** Exclusive. */ endRow: number; continues: boolean; } /** Per-row metrics of a table laid out in full at some width, from which * {@link planTableSlice} decides where a slice can end. Row heights are * exact for any slice at the same width: every row is measured at the same * column edges, and rowspans never straddle a break. */ export interface TableRowMetrics { /** Height of each model row in px (rowspan overflow already distributed). */ rowHeights: number[]; /** Leading header rows, repeated at the top of every continuation. */ headerRowCount: number; /** Whether a slice may end after row `i` — no rowspan crosses the edge * between `i` and `i + 1`. */ breakableAfter: boolean[]; /** Rows that head the rows below them (a single cell across every column, * or all header cells): a slice never ends on one when it can help it. */ groupHeaderRow: boolean[]; } /** Resolve the colour of every inline `:swatch{…}` span: a `#rgb` / `#rgba` * hex is normalised to six / eight digits, a `#rrggbb` / `#rrggbbaa` one * lower-cased, an `rgb()` / `rgba()` colour kept as written; any other * value is looked up as a document palette entry id. An unresolved colour * is left as written (the measurer then draws an empty outline). Spans * without a swatch pass through. */ export declare function resolveSwatchSpans(spans: InlineSpan[], palette: ColorPaletteEntry[] | undefined, resources?: readonly Resource[], metrics?: SwatchMetrics): InlineSpan[]; /** What a swatch's lengths are read against (#1104): the document's DPI * and the font size of the text the swatch sits in (its em). */ export interface SwatchMetrics { dpi: number; fontSizePx: number; } /** A swatch's shape in px and hex (#1104): an outline colour that names * no palette entry is left out (the text colour outlines it). */ export declare function resolveSwatchBox(shape: SwatchShape, palette: ColorPaletteEntry[] | undefined, metrics: SwatchMetrics): SwatchBox; /** Build a label for an inline `:ref` to a resource, honouring its `style` and * optional override `text`. Mirrors the caption-prefix conventions. */ export declare function resolveRefLabel(ref: NonNullable, resourceNumbering: ResourceNumberingMap, resourceTypes: ResourceType[], resources: Resource[], /** Between the label and the number (`captionStyle.labelNumberGap`); a * type's own caption style may set another. */ labelNumberGap?: string): string; /** Anchor targets and the words a cross-reference prints, threaded into * {@link resolveRefSpans} (#262). */ export interface AnchorRefContext { targets: AnchorTargets; strings: CrossRefStrings; } /** Resolve inline `:ref` spans to their computed label, keeping the `ref` * metadata so the rich-text measurer treats each label as one atomic, * non-breaking token and tags the produced segment with `refResourceId`. * A reference whose id names no resource but an anchor (a heading's * `{#id}`, an inline anchor, a container) prints the anchor's label and is * flagged `ref.anchor` (#262); a pandoc-crossref prefix (`fig:map`) may be * left off the identifier it names. */ export declare function resolveRefSpans(spans: InlineSpan[], resourceNumbering: ResourceNumberingMap, resourceTypes: ResourceType[], resources: Resource[], refStyle?: { bold: boolean; italic: boolean; labelNumberGap?: string; anchors?: AnchorRefContext; /** `crossRefs.breakLabels` (#702): a label of words with a space * between two of them is marked `ref.breakable`, and a line may break * there (`refLabelPieces`). */ breakLabels?: boolean; }): InlineSpan[]; /** The inline formulas of a caption, a note or a cell (#541), rendered at * the snippet's size as the body's are at the body's: one em of the * formula is `fontPx` × `math.fontSizeScale`, and a formula taller than * the line is scaled down to it. With maths off the TeX prints as * written, between its dollars. */ export declare function snippetMathSpans(spans: InlineSpan[], fontPx: number, lineHeightPx: number, math: ResolvedMathConfig | undefined): InlineSpan[]; /** * A caption or note line justified in its segments (#1099): its word spaces * take the line's slack, each as wide as the others, so every renderer, * which paints a caption line as measured, sets it flush on both sides. The * rule is the one body text is painted with (`renderLine` in the canvas * backend): the last line of a paragraph, a tabbed line and a line the * measurer set ragged keep their natural spaces unless they run past the * measure; a hung mark stays out of the count and a Han–Latin space * (`autospace`) keeps the width the CJK composer gave it. A composed CJK * line already spread to its measure has no slack left and comes back as * it was. Highlight bands are placed from the segments once the document is * laid out, so they follow the wider spaces. Never changes `line` in place * (a cached measurement may share it). */ export declare function justifySnippetLine(line: VDTLine, maxWidthPx: number): VDTLine; /** The four faces a caption or note run is set in (normal, bold, italic, * bold italic). */ type SnippetFonts = readonly [string, string, string, string]; /** * Column x-edges (length = columnCount + 1) for a table model laid out at * `columnWidth`. `TableModel.columnWidths` are relative weights, normalised so * they always fill the width exactly; a missing array, a wrong length, or any * non-positive / non-finite weight falls back to an equal split. */ export declare function computeColumnEdges(model: TableModel, columnWidth: number): number[]; /** Number of leading header rows of a table: `headerRowCount` when the * model declares it, else the leading run of rows whose visible cells are * all header cells. Never the whole table — a continuation needs at least * one body row to carry. */ export declare function tableHeaderRowCount(model: TableModel): number; /** The model rows a slice lays out, in order: the header rows (repeated on a * continuation) followed by the slice's own rows. */ export declare function tableSliceRows(model: TableModel, slice: TableSliceSpec): number[]; /** * Where a slice starting at `startRow` can end so that its rows (plus the * repeated header rows of a continuation) stay within `bodyBudget` px: the * furthest row edge no rowspan straddles, backed off a group-header row so * the heading opens the rows it heads on the next page. Returns the * exclusive end row — `startRow` when not even one row fits, the row count * when the whole rest fits. */ export declare function planTableSlice(metrics: TableRowMetrics, startRow: number, bodyBudget: number): number; /** Fewest body rows the closing slice of a split table carries: a last * page holding a row or two under a repeated header reads as stranded. */ export declare const MIN_TAIL_ROWS = 3; /** * The slice of a table, from `startRow` on, that a room fits: the rows * {@link planTableSlice} allows within `budget` px of body, the closing * slice given at least {@link MIN_TAIL_ROWS} rows (handed back from this * one at a breakable edge, not under a group head, when it can spare * them), then checked by `fits` — the caption may wrap differently with * its suffix, and the closing slice carries the note instead of the * marker — backing off a row at a time over breakable edges while it * fails. `floor` is the exclusive end row of the smallest slice worth * setting. Below it, or when even that slice fails `fits`, the result is * `null`, unless `force`: then the smallest slice is returned (it * overflows, as a dominating figure would). `split` makes a slice that * stops short of the last row go on (`continues`, the marker); without it * the rows left over are dropped (`overflow: 'clip'`). Shared by floated * tables (`splitTableFloat`, a band's room) and inline ones (#634, the * room left in a column). */ export declare function chooseTableSlice(metrics: TableRowMetrics, startRow: number, budget: number, opts: { floor: number; force: boolean; split: boolean; fits: (slice: TableSliceSpec) => boolean; }): TableSliceSpec | null; /** * Measure a resource block (image / svg / table) plus its caption into a * placement-ready {@link ResolvedResourceBlock}. Returns the block and its * combined height (figure body + gap + caption) — the value placement uses to * decide whether the group fits the remaining column space. */ /** What {@link layoutCaptionText} sets: a caption's text and where it * comes from. */ export interface CaptionTextInput { /** Where the caption is measured through (see `ResourceLayoutInput`). */ cache?: MeasurementCache; /** The description as written (inline Markdown); may be empty. */ text: string; /** The resource type whose prefix opens the caption and whose own * caption style is laid over the document's. */ resourceType: ResourceType | undefined; /** The rendered number (`''` for none). */ number: string; resolved: ResolvedConfig; /** Width of the caption band (px), the bar padding included. */ width: number; resourceNumbering: ResourceNumberingMap; resourceTypes: ResourceType[]; resources: Resource[]; /** The formatted citations of the caption (#529). */ citations?: CaptionCitations['caption']; /** Set in italics after the description (a continued table's "(cont.)"). */ suffix?: string; } /** A caption laid out at the block origin (y = 0): its lines, fonts and * colours, and the height of its band (lines plus the bar padding on * both sides; 0 without a caption). */ export interface CaptionTextLayout { style: ResolvedCaptionStyleConfig; lines: VDTLine[]; fonts: { fontString: string; boldFontString: string; italicFontString: string; boldItalicFontString: string; }; fontPx: number; lineHeightPx: number; gapPx: number; paddingPx: number; bandHeight: number; linkColor: string; } /** * A resource caption — "Figure 1.7. Description" — in the caption style * (`captionStyle`, with the resource type's own laid over it): the label * and number in the label's weight and slant, the description in its own, * inline `:ref`s, citations, maths, swatches and chips resolved. Shared by * resource blocks and comic strips (#590). Measured in the writing mode in * force (a resource block sets it horizontally). */ export declare function layoutCaptionText(input: CaptionTextInput): CaptionTextLayout; /** What {@link layoutLabelLines} sets a figure label with (#860). */ export interface LabelLinesInput { /** The label's text as the parser read it (inline Markdown). */ spans: InlineSpan[]; /** Normal, bold, italic and bold italic faces. */ fonts: SnippetFonts; fontPx: number; lineHeightPx: number; /** The measure the text wraps at. */ maxWidthPx: number; letterSpacingPx?: number; direction?: 'ltr' | 'rtl'; resolved: ResolvedConfig; resourceNumbering?: ResourceNumberingMap; resourceTypes?: ResourceType[]; resources?: Resource[]; cache?: MeasurementCache; } /** * A figure label's text as lines (#860), set as a caption is: references, * formulas, swatches, chips and highlight marks resolved, forced breaks * (`\\`) kept, every line flush left at its natural width from y = 0. */ export declare function layoutLabelLines(input: LabelLinesInput): VDTLine[]; export declare function layoutResourceBlock(input: ResourceLayoutInput): ResourceBlockLayout; /** A resource block laid out (see {@link layoutResourceBlock}). */ export interface ResourceBlockLayout { block: ResolvedResourceBlock; totalHeight: number; /** Height of the caption + note band set beside the body * (`captionAside`); absent otherwise. */ asideHeight?: number; /** Row metrics of a table laid out in full (no `slice`), for * {@link planTableSlice}. */ tableRows?: TableRowMetrics; } /** Shift a resolved resource block's geometry right by `dx` (an inline * resource narrower than its column, set per `placement.align`). */ export declare function shiftResourceBlockX(rb: ResolvedResourceBlock, dx: number): void; export {}; //# sourceMappingURL=resourceLayout.d.ts.map