3.4 KiB
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
placeNodedispatches on the discriminatedDocumentNodeunion and returns either a content-stream snippet or the"page-break"sentinel that signals the orchestrator to flush + restart.- Tables emit a
continuationdescriptor ({ table, rowStart }) when they partially fit, so the orchestrator can resume on the next page without re-measuring the full table. renderTableSliceswallowsPDF.LAYOUT_OVERFLOWfromTableElement.renderPageand surfaces it as arowEnd === rowStartno-progress result.renderSlotflows header / footer nodes top-to-bottom inside the reserved slot rectangle.pickFontResourcere-uses the writer's idempotent font registry so oneF<id>survives the whole render.
Usage: Basic
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
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<id>".- 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.