Core Concepts
The pipeline: model -> layout -> render
TerraFluent.Html.Reporting is built as three distinct stages, each in its own namespace, connected only by simple data:
Fluent (builders) --> Model (immutable elements) --> Layout (pagination) --> Rendering (HTML)
ReportDocumentBuilder ReportDocument LayoutEngine.Paginate HtmlReportRenderer
ContentBuilder IReportElement implementations -> LayoutResult -> HTML string
TableBuilder, RowBuilder ... (pages, placements)
TerraFluent.Html.Reporting.Fluent- the builders you actually call (ReportDocumentBuilder,ContentBuilder,PageSectionBuilder,TableBuilder,RowBuilder, …). These exist purely to give you a nice chaining API; they hold mutable lists internally but produce immutable model objects.TerraFluent.Html.Reporting.Model(andModel.Elements,Model.Styling,Model.Sections) - the immutable document:ReportDocument,IReportElementimplementations (Paragraph,Table,Row, …),TextStyle,TableStyle. Nothing here knows how to paginate or render itself in isolation - each element only knows how to measure and split itself.TerraFluent.Html.Reporting.Layout-LayoutEngine.Paginatewalks the document’s content elements once, in order, deciding what goes on which page. The output, aLayoutResult, is a plain description of pages and placements - no HTML anywhere yet.TerraFluent.Html.Reporting.Rendering-HtmlReportRendererwalks aLayoutResultand asks each placed element to render itself as HTML at its resolved position.
ReportDocument.RenderHtml() (and its siblings) just calls
LayoutEngine.Paginate followed by HtmlReportRenderer.RenderDocumentTo for
you. You can call both stages yourself - e.g. to inspect LayoutResult.Warnings
before deciding whether to render at all. See
Pagination and Layout and
Rendering.
The document model
ReportDocument is the
root: page geometry (PageSize, Orientation, Margins), an optional
Header/Footer (IPageSection), the top-level ContentElements, and the
TextMeasurer to lay out with. It is immutable and can only be constructed
via ReportDocument.Create(...) followed by ReportDocumentBuilder.Build().
Every piece of content - a paragraph, a table, a row of columns - implements
IReportElement:
public interface IReportElement
{
ElementMeasurement Measure(LayoutContext context);
SplitResult Split(double availableHeightPx, LayoutContext context);
string RenderHtml(ElementPlacement placement, RenderContext context);
}
The layout engine drives every element through exactly this contract and never inspects an element’s concrete type - which is what makes it possible to add new element kinds (see Extending the Library) without touching the engine. The full Measure/Split/Render contract, including what head/tail mean and how splitting interacts with widow/orphan control, is covered in Pagination and Layout.
The rebuild-and-replace pattern
Model types like TextStyle, ReportImage, Row, and RowColumn are
immutable - every property is init-only, and each type exposes a With(...)
method that returns a new instance with the given properties overridden.
Fluent modifiers (.Bold(), .AlignCenter(), .Margin(8), …) are thin
wrappers around this: when you write
c.AddParagraph("Note").Bold().FontSize(16)
AddParagraph adds an initial Paragraph to the content list and returns a
TextElementBuilder
that remembers where in that list the paragraph lives. Each chained call
(.Bold(), then .FontSize(16)) calls Style.With(...), builds a brand new
Paragraph with the updated style, and replaces the old one at that same
index. The builder itself holds no state that outlives the chain - it’s a
short-lived handle onto a slot in a list, not a long-lived wrapper around the
element.
The same pattern repeats for every element that supports chained modifiers:
| Element | Builder/handle returned by Add* |
Backing With(...) |
|---|---|---|
Paragraph / Heading / PageNumberText |
TextElementBuilder |
TextStyle.With(...) |
ReportImage |
ImageElementBuilder |
ReportImage.With(...) |
Row |
RowHandle |
Row.With(...) |
RowColumn |
RowColumnHandle |
RowColumn.With(...) |
Elements with no modifiers to chain (HorizontalRule, Spacer, PageBreak,
RawHtml, Table, ReportList) have their Add* method return the owning
builder itself (ContentBuilder/PageSectionBuilder) for plain method
chaining instead.
One practical consequence: because each .With(...) call only overrides the
properties you pass, calling the same modifier twice in a chain just has
the later call win - there’s no accumulation (e.g. .Margin(8).Margin(4)
ends at 4, not 12).
Units: everything is a CSS pixel
Every length in the public API - page dimensions, margins, font sizes,
padding, column widths, image dimensions - is expressed in CSS pixels
(px), using the standard 96px = 1 inch conversion. There are no points,
ems, or percentages anywhere.
PageSize.FromMillimeters(...)andPageSize.FromInches(...)are convenience constructors that convert to pixels internally;PageSizeitself always storesWidthPx/HeightPx.PageSize.FromPixels(...)skips the conversion entirely.- The renderer emits these pixel values directly into inline CSS
(
left:123.45px,@page { size: 794px 1123px; }), so what the layout engine measured is emitted without a unit translation step, so page boxes and element placements do not drift between pagination and rendering.
This matters most when picking a TextMeasurer: pagination correctness
depends on the measurer’s pixel measurements agreeing with however the
browser eventually renders the same font at the same pixel size. See
Text Measurement.
Page geometry: PageSize, Margins, Orientation
PageSizeis areadonly structstoringWidthPx/HeightPx. Built-in sizes -PageSize.A4,PageSize.Letter,PageSize.Legal- are defined portrait-shaped (width < height).WithOrientation(PageOrientation.Landscape)swaps width and height;ReportDocument’s constructor calls this for you based on theorientationargument toReportDocument.Create, so you never call it directly. A size built viaPageSize.FromPixels(...)is not assumed portrait - landscape still swaps whatever width/height you gave it, it’s a plain 90-degree rotation either way.Marginsis areadonly structwith independentTop/Right/Bottom/Leftvalues.Margins.All(value)sets all four;Margins.Noneis all zero.SetMargins(...)on the document builder has both a 4-argument (top/right/bottom/left, matching CSS shorthand order) and a 1-argument (uniform) overload.- Content width is
PageSize.WidthPx - Margins.Left - Margins.Right, computed once byLayoutEngine.Paginateand threaded through every element’sMeasure/Splitcall viaLayoutContext.ContentWidthPx. - Content height per page is
PageSize.HeightPx - Margins.Top - Margins.Bottomminus the header’s and footer’s measured height (header/footer height is not configured directly - see Pagination and Layout).
If margins (plus header/footer) leave zero or negative room for content,
LayoutEngine.Paginate throws InvalidOperationException immediately rather
than producing a nonsensical layout.
Where to go next
- Content Elements for what you can actually put
inside
Header/Footer/Content. - Pagination and Layout for exactly how the engine decides where pages break.
- Extending the Library if you want to implement
IReportElementyourself.