Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# render-pipeline
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<id>` 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<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.