# render-pipeline _(no submodules)_ ## Purpose Internal pipeline helpers that the top-level `renderDocument` orchestrator delegates to. Splitting placement and slot-rendering out of the orchestrator keeps each file under the 300-line hard limit and lets callers test the per-node placement protocol in isolation from the page-emit loop. The pipeline is stateless across calls — every operation receives a `PipelineState` bundle (writer, layout engine, style resolver, image registry, abort signal, page counter) and mutates only the writer and the page counter. ## Features - `placeNode` dispatches on the discriminated `DocumentNode` union and returns either a content-stream snippet or the `"page-break"` sentinel that signals the orchestrator to flush + restart. - Tables emit a `continuation` descriptor (`{ table, rowStart }`) when they partially fit, so the orchestrator can resume on the next page without re-measuring the full table. - `renderTableSlice` swallows `PDF.LAYOUT_OVERFLOW` from `TableElement.renderPage` and surfaces it as a `rowEnd === rowStart` no-progress result. - `renderSlot` flows header / footer nodes top-to-bottom inside the reserved slot rectangle. - `pickFontResource` re-uses the writer's idempotent font registry so one `F` survives the whole render. ## Usage: Basic ```ts import { placeNode, renderSlot, type PipelineState } from "@suaveplan/pdf"; const state: PipelineState = { writer, layout, styles, images, signal: undefined, pageCount: 0 }; const result = placeNode(state, node, cursor, undefined, new Map()); if (result === "page-break") { // orchestrator: flush and retry } ``` ## Usage: Advanced ```ts import { placeNode, renderTableSlice, renderSlot } from "@suaveplan/pdf"; // Slice rendering protocol — orchestrator advances rowStart between calls. let rowStart = 0; while (rowStart < tableElement.options.rows.length) { const slice = renderTableSlice(state, tableElement, rowStart, cursor, config); if (slice.rowEnd === rowStart) { // No progress — caller must flush + create a fresh cursor. break; } rowStart = slice.rowEnd; // ... write slice.stream into the per-page buffer ... } ``` ## API Reference - `placeNode(state, node, cursor, config?, pageXObjects)` → `PlacementResult`. - `renderTableSlice(state, table, rowStart, cursor, config?)` → `{ stream, rowEnd }`. - `renderSlot(state, nodes?, height?, slot, config?, pageXObjects)` → `string[]`. - `pickFontResource(state, style)` → `"F"`. - Types: `PipelineState`, `PlacementResult`. ## Implementation Notes `placeNode` always measures first, then conditionally advances the cursor. Text and divider elements pass the pre-advance `cursor.y` as the render `y` (PDF's bottom-up coordinate system means the top of the element lives at the larger `y`, and the element's `.render()` contract places baselines at `y - i·lineHeight`). Images measure once and ignore `maxWidth` because their bounds are fixed at construction. Tables short-circuit through `TableElement.render` when the whole element fits and only fall back to the slice protocol when pagination is required. The slot renderer flows nodes downward inside the reserved rectangle, which means a footer slot with two stacked text lines places the first line at the slot top and the second below — matching the visual expectation that flow content reads top-to-bottom regardless of PDF's bottom-up geometry.