This document describes rtfreporter’s internal data structures – the S3 objects the renderer works with – for people extending the package. For the public classes see Architecture & internals.
Everything is pure S3: plain
lists with aclassattribute, no external OOP framework. Public objects use copy-on-modify semantics, so the pipe API returns a new object at each step rather than mutating in place.
Two-layer model
A document is not a deep hierarchy. It is a flat sequence of pages plus a set of section overlays:
rtf_document (public, pipe-built)
└─ converted at render time to → rtfreport (internal)
├─ document : font_table, color_table, default_page, default_format
├─ pages[] : rtf_page (one content item per page)
└─ sections[] : rtf_sect (header/footer assigned to a page range)
-
Pages are flat at the document level; 1
page = 1 content (an
rtftableor anrtfplot). -
Sections are an overlay: each
rtf_sectassigns a header/footer starting at a page and applying until the next section. They do not contain content.
This is the Builder pattern
The two layers are a deliberate use of the builder pattern, not a redundant pair of document classes:
-
rtf_documentis the builder – a convenient, incrementally-grown object. The pipe verbs (rtf_tables(),rtf_section(), …) each return an updatedrtf_document, accumulating content and section definitions in the order the author writes them. -
rtfreportis the product – the normalised, render-ready form the renderer consumes (content paired with its title/footnote intortf_pages, section definitions resolved to page ranges). -
.pipe_doc_to_rtfreport()is the build step that turns one into the other, run once at render time insidegenerate_rtfreport().
Keeping the two separate means the authoring API and the rendering model can evolve independently, and each stays simple: the builder never has to be render-ready, and the product never has to support incremental edits.
The objects
rtf_document (public)
Built by the pipe API (rtf_document() +
rtf_tables() / rtf_figures() /
rtf_section() / …). Slots: document
(font/color/page/format), contents (the per-page content
list), titles, footnotes, and
sections. Immutable in use – each pipe verb returns a
copy.
rtfreport (internal)
The render-ready form produced from an rtf_document by
.pipe_doc_to_rtfreport(). It holds the resolved
document defaults, a list of rtf_page objects,
and a list of rtf_sect objects. Construction-time
validation lives in .rtfreport_validate().
rtf_page (internal, S3 tagged list)
structure(list(title, content, footnote), class = "rtf_page")
– one page:
-
title— character vector (centred lines above the content); -
content— anrtftableorrtfplot(orNULL); -
footnote— character vector rendered below the content.
rtf_sect (internal, S3 tagged list)
structure(list(header, footer, from_page), class = "rtf_sect"):
-
header/footer— a header/footer description, orNULLto inherit from the previous section; -
from_page— the first page this section applies to.
.resolve_sections() maps each from_page to
a (from_page, to_page) range at render time.
rtftable (public)
The richest object: the table data plus all formatting. Single-DF
mode stores data; multi-DF mode stores
data_list. Plus col_header /
col_header_list, col_spec (per-column: align,
bold/italic/underline, indent, text color, …),
border (an rtf_table_border),
blank_rows, width settings, row heights, cell
padding/valign, and cell_styles (per-cell overrides).
as_rtftables() attaches page-level rtf_titles
/ rtf_footnotes as attributes for rtf_tables()
to consume. print() renders a visual preview of the table
(headers, cells, rules, alignment); format() returns those
lines and summary() the compact metadata block.
Style and border objects
rtf_border (per-side line specs),
rtf_table_border (zone borders for a table), and
rtf_table_style (a reusable defaults bundle) are all plain
S3 lists. rtf_table_style uses snapshot
semantics – a table captures the style’s state at construction;
rtf_table_style_with() derives a modified copy without
mutating the original.
Rendering
generate_rtfreport() converts the
rtf_document to an rtfreport, resolves
sections, computes the colour table (from the border colours used), and
emits the RTF: a font table, colour table, page settings, then per
section a {\header} / {\footer} followed by
the page content (tables via the row renderers, figures via the picture
renderer). Page-number tokens ({AUTO_PAGE} /
{AUTO_TOTAL_PAGES} / {PAGE}) are substituted
here. Helper functions (.resolve_sections(),
.render_footnote_table(),
.compute_content_width(), …) live in
R/generate_rtfreport.R.
One rendering step deliberately works on a derived geometry
rather than the table’s own: set_decimal_split()
(R/decimal_split.R). Because RTF lets each row declare its
own cells, .decimal_split_plan() builds an expanded set of
\cellx positions – one extra edge inside each split column
– that only the data rows use. The whole column-header
block keeps rendering over the original columns, so no header or
spanning renderer knows the feature exists, and the cumulative positions
still end at the same right edge, leaving the table width, the
title/footnote band and pagination untouched. The plan is derived at
render time from tbl$decimal_split and never written back
to the object; when it is NULL every renderer takes its
original path.
Why S3 here
S3 fits this design because the objects are mostly
data: they are built once, passed down a pipe, and
rendered. Plain lists are transparent (str()),
serialisable, and dependency-free. Mutability is not needed – the pipe
API never exposes shared state – so copy-on-modify S3 is both simpler
and safer than a reference system would be.
S3 conventions & class roles
Knowing why a class exists tells you how to work with it. In rtfreporter an S3 class plays one of three roles:
-
Generic dispatch. A few classes carry
print()/plot()methods (for readable console output and base-graphics layout previews).paginate()is also an S3 generic, but it dispatches on the input type (data.frame/gt_tbl/list), not on a rtfreporter class. -
Type tag for
inherits(). Most classes exist to be recognised byinherits(x, "<class>")in ordinaryifbranches – the renderer and the pipe verbs switch on object kind this way. There is no method involved. -
Internal structural marker.
rtf_page/rtf_sectcarry a class purely for readability andstr()/ debugging output; nothing dispatches on them and the renderer reaches their fields by name.
| Class | Role |
|---|---|
rtf_document |
dispatch (print, plot) +
inherits()
|
rtftable |
dispatch (print, format,
summary, plot) + inherits()
|
rtf_border_line, rtf_border,
rtf_table_border
|
dispatch (print, plot) +
inherits()
|
rtf_table_style, rtf_col_cell,
rtf_col_header
|
dispatch (print) + inherits()
|
rtfplot |
dispatch (print) + inherits()
|
rtf_blank_rows_by_change,
rtf_blank_rows_by_rule
|
inherits() (blank-row resolver dispatch) |
rtf_toc_heading, rtf_toc_entry
|
inherits() (TOC normalisation) |
rtfreport, rtf_auto_section_item
|
inherits() (internal) |
rtf_page, rtf_sect
|
internal structural marker (no dispatch) |
Conventions to follow when extending
-
Construct with
structure(list(...), class = "<class>"), inside a public constructor or a.new_*()internal helper. -
Test types with
inherits(x, "<class>"), notclass(x) == "<class>"(robust when an object carries more than one class). -
Validate with
stop(msg, call. = FALSE)(no leading function-name noise). -
Copy-on-modify. Every modifier – the pipe verbs and
the
*_with()derivers (rtf_table_style_with()) – returns a new object; nothing is mutated in place. -
No class hierarchies. Classes are single, flat
tags. The border family is composition, not inheritance: an
rtf_table_bordercontainsrtf_borderobjects, each of which containsrtf_border_lineobjects. - Adding a class only pays off if something will dispatch on
it or
inherits()-check it – otherwise a plain list is enough.