This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
{clinify} is an R package (Atorus Research) that extends {flextable} and {officer} to simplify producing regulatory clinical tables, listings, and figures, with Word (.docx) as the first-class output. The guiding constraint (see README.md "Design Philosophy"): clinify objects inherit from flextable/officer objects and must never break native flextable/officer behavior. Lifecycle is experimental; it is on CRAN (version 0.3.0.9000 = dev).
Standard devtools-based R package (clinify.Rproj has PackageUseDevtools: Yes). Run from the package root:
devtools::load_all() # load package for interactive dev (no install)
devtools::document() # regenerate man/*.Rd + NAMESPACE from roxygen — run after changing any roxygen
devtools::test() # run all tests
devtools::test(filter = "pagination") # run one file: matches tests/testthat/test-pagination.R
devtools::check() # full R CMD check (what CI runs)
lintr::lint_package() # lint (.lintr = default linters)
devtools::build_readme() # rebuild README.md from README.Rmd (NEVER edit README.md directly)Snapshot tests dominate this suite (see below). To work with them:
testthat::snapshot_review() # review pending snapshot diffs interactively
testthat::snapshot_accept() # accept new snapshots after an intentional changeCI (.github/workflows/R-CMD-check.yaml) runs rcmdcheck on Windows/macOS/Linux × R release+devel and uploads snapshots on failure. pkgdown site + test-coverage + rhub workflows also exist.
A clintable is a flextable with an extra x$clinify_config list attached. Every user-facing clin_*() builder verb does almost nothing except mutate fields in clinify_config and return the object. No pagination, slicing, or styling is computed until a terminal action runs (print(), knit_print(), write_clindoc(), or clindoc()). Understanding any feature means tracing (1) which config field the clin_*() setter writes, and (2) where the renderer reads it. Key config fields: pagination_method ("default"/"custom"), page_by, max_rows, group_by, caption_by, group_when, key_cols, col_groups, auto_page_var, titles, footnotes, footnote_page, and the computed pagination_idx.
clintable(R/clintable.R) — inheritsflextable. Built byclintable(df, ...)oras_clintable(ft). Holdsclinify_config.clinpage(R/clinpage.R) — inheritsflextable. One rendered page = a row/column subset of a clintable. Produced byslice_clintable().clindoc(R/clindoc.R) — inheritsofficer::rdocx. The final document; can hold multiple clintables separated by page breaks. Built byclindoc(...)/as_clindoc().
Defined mainly in R/pagination.R, R/col_width.R, R/column_headers.R, R/add_titles_footnotes.R, R/group_pad.R:
clin_page_by, clin_group_by, clin_alt_pages, clin_auto_page, clin_col_widths, clin_column_headers, clin_group_pad, clin_add_titles, clin_add_footnotes, clin_add_footnote_page. Any of clin_page_by/group_by/alt_pages flips pagination_method to "custom".
prep_pagination_() is the render-time driver. It produces pagination_idx: a list of pages, each list(rows, cols, label, captions), via make_ind_list(). It combines:
- Row vectors (
page_vecs):page_by_()splits where the page variable changes (and at group starts);max_rows_()splits every N rows while respecting group boundaries. - Column vectors (
col_vecs):alt_pages_()for "alternating pages" (repeatkey_colson each page, then append onecol_groupper page) — this is how wide tables are split across pages; otherwise all columns. - Group labels and captions are carried forward across rows with
zoo::na.locf.
clin_auto_page() is a separate pagination strategy: instead of pre-slicing, it flags rows with flextable's keep_with_next() so Word itself avoids breaking groups across pages (auto_page_()).
flextable has no native way to cut an already-styled table into page chunks while preserving styles/spans/borders, so slice_clintable() reimplements it by reaching directly into flextable's internal structures ($body$dataset, complex_tabpart, fpstruct, chunkset_struct, $spans$rows, $styles$cells/pars/text). adjust_span_row() repairs header spanners that get cut at a column page break; reapply_bottom_border() pulls the table's bottom border onto each slice. This code is tightly coupled to flextable/officer internals — git history shows repeated breakage on officer/flextable upgrades (DESCRIPTION pins officer (>= 0.7.2)). Treat flextable/officer version bumps as high-risk for slicing + snapshot tests.
.onLoad populates six options that are read at render time via getOption(...):
clinify_docx_default (an officer::prop_section for page size/margins/orientation — landscape by default), and five styling functions clinify_titles_default, clinify_footnotes_default, clinify_table_default, clinify_caption_default, clinify_grouplabel_default. Organizations override defaults by assigning their own functions to these options (see vignette("defaults") and inst/defaults_template.R). clin_default_table_width() derives usable width from the docx section (page width − margins) and underpins the percent-based widths in clin_col_widths().
- HTML preview —
print.clintable/knit_print.clintable→clintable_as_html()(R/print.R). Rendersnpages (default 3) with a small JS page-switcher; titles/footnotes are rendered as separate flextables and concatenated into the body HTML. - Word —
write_clindoc()(R/write.R) →as_clindoc()/clindoc()→add_clintable_()(R/clindoc.R), using officer'sbody_append_*_contextAPI. Titles/footnotes become the section'sheader_default/footer_defaultso Word repeats them on every page; alternating pages are emitted with page breaks between slices.
- Multi-level headers via
clin_column_headers()(named character vectors, applied bottom-up) or automatically from dataframe column labels using||as the level delimiter (use_labels = TRUEdefault →headers_from_labels_()). Spanners form by merging equal-valued header cells horizontally and vertically. clin_replace_pagenums()(R/pagenums.R) swaps{PAGE}/{NUMPAGES}placeholders for real Word field objects; it is invoked inside the title/footnote default styling functions.
- Naming: exported user verbs are
clin_*; overridable default-style functions areclinify_*_default; a trailing underscore marks internal/non-exported helpers (prep_pagination_,page_by_,group_by_,auto_page_,add_clintable_,headers_from_labels_, …). - Code style: consistent with the Air formatter (
.vscode/settings.jsonenables format-on-save). Match it: 2-space indent, native pipe|>,\(x)lambdas, one argument per line in multi-line calls. Fully-qualify external calls (flextable::,officer::). - roxygen2 with markdown (
Roxygen: list(markdown = TRUE)); alwaysdevtools::document()after edits —man/andNAMESPACEare generated, never hand-edit. - Tests are snapshot-heavy (testthat edition 3). Many tests render to a
withr::local_tempfile(fileext = ".docx"), re-read it withofficer::read_docx(), andexpect_snapshot()the result; others snapshot HTML or assertprep_pagination_()index structures directly (test-pagination.Ris the largest and the best reference for the pagination contract). A snapshot diff after a deliberate change is expected — review thensnapshot_accept(). - User-facing changes go in
NEWS.md; reference the GitHub issue number.