Adding a table-object adapter
Source:vignettes/articles/extending-adapters.Rmd
extending-adapters.Rmdrtfreporter can read tables produced by other packages — gt,
gtsummary, rtables/tern, flextable, and huxtable — and turn them into
RTF while preserving their metadata (column labels, alignment, spanning
headers, titles, footnotes, per-cell styles). This article shows how to
teach it about a new table-object type, using the
existing gt_adapter.R, rtables_adapter.R,
flextable_adapter.R and huxtable_adapter.R as
templates.
Read Architecture & internals first for the big picture.
The contract
An adapter has one job: convert your object into the common
kwargs list that as_rtftables()
consumes. Every field except data is optional.
list(
data = <data.frame>, # REQUIRED: the rendered body (character cells are fine)
col_header = <chr vector | stacked spanning rows>,
col_spec = list(list(col = 1L, align = "left"), ...),
cell_styles = <list, one element per data row, or NULL>,
titles_block = <chr vector | NULL>, # becomes the page title
footnotes_block = <chr vector | NULL> # becomes the page footnote
)as_rtftables() does everything else (pagination,
per-page cell_styles slicing, replicating shared metadata,
attaching titles/footnotes). You do not build the
rtftable yourself.
Step 1 — a detector
Add a cheap, dependency-light predicate. Keep it side-effect free.
# R/mypkg_adapter.R
.is_mypkg_tbl <- function(x) inherits(x, "mypkg_table")
# (for an S4 class use: isS4(x) && methods::is(x, "MyClass"))Step 2 — token resolution
Tokens let users opt in/out of metadata via read =.
Mirror the gt / rtables resolvers.
.MYPKG_TOKENS_ALL <- c("col_header", "alignment", "titles", "footnotes")
.resolve_mypkg_tokens <- function(read) {
if (is.null(read) || isFALSE(read)) return(character(0))
if (isTRUE(read)) return(.MYPKG_TOKENS_ALL)
if (!is.character(read))
stop("`read` must be FALSE/TRUE or a character vector of tokens.",
call. = FALSE)
bad <- setdiff(read, .MYPKG_TOKENS_ALL)
if (length(bad))
stop("Unknown token(s): ", paste(bad, collapse = ", "), call. = FALSE)
read
}Step 3 — the extractor
Return the kwargs list. Always require the heavy package lazily.
.mypkg_to_rtftable_kwargs <- function(x, tokens = .MYPKG_TOKENS_ALL) {
if (!requireNamespace("mypkg", quietly = TRUE))
stop("Reading a mypkg table requires the `mypkg` package.", call. = FALSE)
out <- list()
out$data <- mypkg::as_body_data_frame(x) # however your package exposes the body
if ("col_header" %in% tokens)
out$col_header <- mypkg::column_labels(x)
if ("alignment" %in% tokens)
out$col_spec <- lapply(seq_len(ncol(out$data)), function(j)
list(col = j, align = mypkg::col_align(x, j)))
if ("titles" %in% tokens)
out$titles_block <- mypkg::title_lines(x)
if ("footnotes" %in% tokens)
out$footnotes_block <- mypkg::footnote_lines(x)
out
}Tips drawn from the existing adapters:
-
Spanning headers are a multi-row
col_header: a list whose upper elements arelist(col_cell(pos = c(from, to), label = ...), ...)and whose last element is the leaf-label character vector. See.extract_spanners()(gt) and.rtables_header_segments()(rtables). -
Per-cell styles go in
cell_styles: a list with one element per data row, each eitherNULLorlist(bold=, italic=, underline=, indent_twips=)where each is a vector of lengthncolwithNAmeaning “inherit fromcol_spec”. -
Footnote marks already in the text. If your source
renders marks inline (gt uses
<sup>N</sup>, rtables uses{N}), convert them to rtfreporter’s^{N}superscript markup withoutgsubbackreferences (some locale-broken R builds drop\\1) — useregmatches()as in.convert_footnote_marks()/.convert_rtables_marks().
Step 4 — wire it into as_rtftables()
Three small edits in R/as_rtftables.R:
# (a) the list-input guard must NOT treat your object as a list of items
if (is.list(x) && !is.data.frame(x) && !isS4(x) &&
!.is_gt_tbl(x) && !.is_gtsummary_tbl(x) &&
!.is_rtables_tbl(x) && !.is_mypkg_tbl(x)) { ... }
# (b) the resolve-body-+-metadata dispatch
} else if (.is_mypkg_tbl(x)) {
tokens <- .resolve_mypkg_tokens(read)
kw <- .mypkg_to_rtftable_kwargs(x, tokens = tokens)
body <- kw$data
cell_styles <- kw$cell_styles
titles_block <- kw$titles_block
footnotes_block <- kw$footnotes_block
}Optionally relax the guard in as_rtftable() (the
singular wrapper) so it accepts your type too.
Step 5 — declare the dependency
Add mypkg to Suggests: in
DESCRIPTION. Never add it to Imports: — the
zero-hard-dependency rule is deliberate. The lazy
requireNamespace() in Step 3 produces a friendly error when
it is missing.
Step 6 — tests and docs
- Add
tests/testthat/test-mypkg-adapter.R, wrapping every test inskip_if_not_installed("mypkg"). - Document the new
readtokens (extend?as_rtftables). - Add a
NEWS.mdentry; for a major/minor change updateCHANGELOG.md. - Run
devtools::document(),devtools::test(),devtools::check().
Supplying a method from your own package
The steps above are for a package that does not know
rtfreporter: gt, rtables and the rest never import it, so rtfreporter
carries their adapters. If you are writing a package that
depends on rtfreporter, do not add a branch here.
as_rtftables() is an S3 generic, so register a method for
your class instead, and rtfreporter never has to name it:
Show code
# In your package's R/ (roxygen), with rtfreporter in Imports:
#' @importFrom rtfreporter as_rtftables
#' @export
as_rtftables.my_report <- function(x, ...) {
df <- build_the_table(x) # your own work
rtfreporter::as_rtftables(df, ...) # a list of rtftable pages
}This writes importFrom(rtfreporter, as_rtftables) and
S3method(as_rtftables, my_report) to your NAMESPACE. The
method must return what the default returns: a list of
rtftable pages. rtf_tables(doc, x) then
accepts your object directly, converting it up front the way it converts
a gt_tbl. The dependency points the right way: your package
depends on rtfreporter, and rtfreporter does not depend on yours.
A worked reference
The rtables adapter (R/rtables_adapter.R) is a compact,
complete example that exercises every part of the contract — multi-level
spanning headers, per-row indentation as cell_styles,
inline {N} -> ^{N} mark conversion, and
title/footer extraction — all from a single
formatters::matrix_form() call. It is a good model to
copy.