tabular 0.3.2
Minor improvements and bug fixes
- A
check_latex()unit test that verifies TinyTeX resolution off thePATHnow carriesskip_on_cran(), so it no longer errors on CRAN machines that expose a TinyTeX root without a functional TeX at it (the CRAN macOS arm64 check). Package code is unchanged.
tabular 0.3.1
CRAN release: 2026-07-20
Improvements
-
as_grid()andemit()are 11-24% faster on long tables across every backend: cell inline-markup ASTs are parsed once per distinct string, style layers extract their overrides once per layer instead of per cell, subgroup splitting / decimal alignment / pagination keys are single-pass, XML/HTML escaping is vectorised, and body rows are assembled without quadratic append.
New features
preset(font_family = )generic chains ("mono"/"serif"/"sans") gained the URW/Nimbus Core-35 clone (Nimbus Mono PS / Nimbus Roman / Nimbus Sans) between the PostScript name and the Liberation face, so the default still renders as Courier / Times / Helvetica on Linux images that ship only the ghostscript fonts (and whose Type 1 Courier the Typst compiler cannot read); all links are metric-compatible, so layout is unchanged wherever the chain resolves.emit()to.tex,.typ, and PDF now re-expresses plain group-separator blank rows as discardable space (a white tabularray hline band on LaTeX, arow-gutterentry on Typst), so a page never ends on a stray blank line above the repeated closing rule when the native page break lands at a group boundary, matching how Word renders the RTF/DOCX output; mid-page the gap keeps the blank row’s exact height, and styled or ruled tables (rowrules, side frames, striped separators) keep the real blank row.preset(decimal_metrics = "afm")now measures afont_familywith no bundled metric table (e.g. a system-installed face such as Courier 10 Pitch) through a temporary graphics device, so decimal alignment and width measurement stay exact whenever the host can resolve the font; the Times-fallback fidelity warning now fires only when that device probe also fails, and the probe can be disabled withoptions(tabular.device_metrics = FALSE).check_typst()is a new diagnostic that audits the Typst PDF toolchain: which typst binary is discoverable (standalonetypst, or the copy bundled inside Quarto), whether its version meets the 0.11 floor, and which families of the configured font chain the compiler can see; the chain is a fallback chain, so the check reports ready as soon as any family resolves and names the face PDFs render in.emit()warns after a Typst compile only when a family the user explicitly named cannot be found — missing members of the built-in cross-OS fallback chains substitute silently, matching the other backends.emit()gained a Typst backend:emit(spec, "out.typ")writes a standalone Typst document (native#tablewith repeatingtable.header/table.footer, page chrome via#set page()counters, native per-cell borders), andemit(spec, "out.pdf", format = "typst")compiles it through the standalone typst binary or Quarto’s bundled copy, so PDF output no longer requires any TeX installation. The table centres on the page, the running header and footer anchor to the body edges (the header grows upward into the top margin, the footer downward, tracking the configured margins), andpaginate()’s keep-with-next mask is enforced through unbreakable rowspans in a hidden zero-width column.emit()on a bare.pdftarget now selects its compile engine by probing the machine, LaTeX first: a usable TeX (resolved the way the compile resolves it, and not frozen on a pre-2023 TeX Live kernel) keeps the historical LaTeX path byte-stable, a discoverable typst binary takes over otherwise, and with neither engine the call aborts up front naming both remedies;format = "latex"/format = "typst"pick the engine explicitly.check_latex()now reports the TeX Live release year of the activexelatexand fails the check when it predates 2023, the first release whose LaTeX kernel (2022-11-01) can load the bundled tabularray; the report and theemit()PDF compile error both explain the update / user-space TinyTeX remedy for images frozen on an old TeX Live.check_latex()and the PDF pre-compile staging now resolve TeX the same way the compile does, preferring a TinyTeX at the standard root (installed by eithertinytex::install_tinytex()orquarto install tinytex) over thePATH, so the diagnostic can no longer describe a different TeX than the oneemit()actually runs; remediation hints name thequarto install tinytexroute, which downloads from GitHub and therefore works behind proxies that block CTAN.cols()accepts a bare string as label shorthand (soc = "SOC / PT"issoc = col_spec(label = "SOC / PT"), including glue interpolation and the deferred{.name}token) and a.hideargument that hides the named columns in one flat vector.paginate()keep-together protection on the natively-paginating backends (RTF, DOCX) now emits edge-only row glue instead of gluing a whole fitting block, so a block that fits a fresh page but not the space left on the current one no longer bumps wholesale and strands a near-empty page; widow/orphan floors count content rows only, with trailing blank spacer rows still riding with their block.paginate(keep_together = )now accepts any column ofdata(including hidden block-key columns), not onlyusage = "group"columns.
Breaking changes
col_spec()no longer hasusage,group_display, orgroup_skiparguments; row grouping is a table-level fact and is now declared once with the newgroup_rows()verb. The inert-knob warning forgroup_display/group_skipon non-group columns is gone with them.group_rows()is the new structural verb:group_rows(by, display, skip)names only the structural grouping keys outer to inner (the section headers and any hidden break keys, not the visible label column, which stays an ordinarycols()column and is auto-indented).displayis a single value applied to every key ("section","collapse", or"repeat"); a hidden break-only key is expressed withcol_spec(visible = FALSE)rather than a display mode.skipfollows thereadr::read_csv(col_names = )pattern:TRUE(the default) derives the blank spacers fromdisplayand key visibility,FALSEinserts none, and a character subset ofbybreaks on exactly those keys (e.g.skip = "param"). Nesting is just listing the header keys (by = c("param", "visit")). It replaces a prior declaration wholesale, and its keys may not overlapsubgroup(by = ).paginate()gainedrepeat_cols, replacing theusage = "id"role: the horizontal-panel stub defaults to the visiblegroup_rows()keys, and an explicitrepeat_colsvector replaces that default.
Minor improvements and bug fixes
DESCRIPTION’s SystemRequirements no longer contains the bare word “LaTeX”, which pak’s system-requirements scraper matched and answered by force-installing the
texliveOS package (a sudo failure on locked-down hosts such as Domino); the TeX distribution remains optional for PDF output.Fidelity warnings now render their feature string as inline code instead of a quoted value, so a feature that itself contains quotes (
decimal_metrics = "afm") no longer prints with escaped quotes.check_latex()now probes package availability throughkpsewhich(the resolver xelatex itself uses) instead of the tlmgr package database, fixing all-missing false negatives on apt-installed TeX Live and frozen TinyTeX images, no longer requires the tinytex R package, and reports a newbundledcolumn.emit()to DOCX now stamps the body’s left/right cell margins on section-header, blank-spacer, column-header, and subgroup-banner cells, so their text aligns with body text exactly as in RTF (section headers previously sat one cell margin further left, making nested indents read one character too wide in Word).emit()to Typst now folds 2+ space runs in body cells underpreset(whitespace = "collapse"); the no-wrap hardening previously rewrote the run to non-breaking spaces first, so the knob was a silent no-op on Typst while every other backend honoured it.emit()to Typst now stamps the header surface background and padding on the column-label cells, sopreset(colors = list(header = ...)),preset(padding = list(header = ...)), and the equivalentstyle()atcells_headers()render as they do on RTF, HTML, LaTeX, and DOCX.emit()to LaTeX/PDF and Typst now places the running header band so its bottom edge sits exactly on the top-margin line, growing upward into the margin, and keeps the body at exactly the configured top margin — matching the RTF and DOCX placement; the LaTeX head lengths moved onto thegeometryoption list (a post-load\setlength{\headsep}shifted the body off the margin instead of moving the header) and every band row now carries a\strutso the band’s bottom edge no longer depends on descenders in its content.emit()to HTML and Markdown no longer drops agroup_rows()blank spacer when a group transition coincides with a page boundary; the continuous flow keeps every spacer and only the very first row of the table suppresses one.emit()to Markdown now renders the indent of nested section headers as before the bold markers instead of spaces inside them, which CommonMark refused to parse as bold (literal**showed in the render).emit()to PDF now writes to a relativefilepath correctly; the path was previously resolved after switching into the temporary compile directory, so the output landed there and was deleted with it.emit()to RTF now resolves the row cell gap of header-band, column-label, blank-spacer, section-header, banner, and title rows frompreset(cell_padding = )like body rows do (previously hardcoded, so a non-default padding skewed header text relative to body text).figure()output no longer spills onto a blank trailing page in Word: the RTF closing paragraph after the full-page image row now carries the body font size (a bare paragraph rendered at RTF’s 12pt default, taller than the reserved row), and the page-size model now budgets thepreset(pagefoot = )slot rows (and anypageheadrows that overflow the top margin), which intrude into the body exactly like footnote lines. The same budget applies to table pagination.emit()PDF output now works on TeX installations that lacktabularray/ninecolorsand cannot runtlmgr install(locked-down Domino / Posit Workbench images): tabular ships verbatim CTAN copies of both single-file packages ininst/tex/and stages them next to the generated.texat compile time whenever the local TeX cannot resolve them.
tabular 0.2.0
CRAN release: 2026-07-06
New features
The documentation site gained an AI / Agents section linking a tabular skill (for LLM coding agents), the auto-generated
llms.txt, and a concatenatedllms-full.txt, mirroring the LLM-friendly documentation pattern.col_spec(group_display = "header_row")now collapses a single-member group to one flush-left row (the group value becomes the row label, still carrying anycells_group_headers()styling) instead of emitting a redundant header plus a lone indented child, and no longer emits an empty header for a blank orNAgroup value. Multi-member groups are unchanged.figure()renders a figure (the “F” in TFL) to every backend (RTF, LaTeX, PDF, HTML, DOCX, and Markdown), wrapping a ggplot, a recorded base-R plot, a zero-argument drawing function, or a PNG / JPG file in the same submission chrome as a table (titles, footnotes, page header and footer). The image is placed in the body content-box byhalignandvalign(both default to centred), exact on the paged backends and approximate on the continuous ones. A list input emits one figure per page, optionally driven by ametadata frame whose columns become per-page{token}values. On the continuous backends (HTML and Markdown) a multi-page figure’s shared titles and footnotes render once above the stacked plots; supplyingmetaswitches to per-page chrome.is_figure_spec()tests for the new spec, andemit()writes it to a file. A figure’s titles, footnotes, and page header / footer honourstyle()and thepreset()cosmetic knobs (fonts, colours, alignment, padding) and thepreset(spacing = ...)inter-section gaps, just like a table; styling its (absent) body, headers, or subgroup is an error. A drawing function that raises an error aborts with a clear message naming the failing page. No new package dependency: plots rasterise through basegrDevicesand ggplot2 (Suggests) only when a ggplot is passed.pivot_across()gained anauxargument to bind auxiliary comparison columns (difference, hazard ratio, p-value) from a second ARD, aligned 1:1 on therow_groupkey and appended as trailing columns.pivot_across()’scolumnargument now accepts the reserved tokens.variableand.statto make an analysis variable a column band:c(".variable", "<arm>")lays variables side by side with statistics as rows, andc(".variable", ".stat")spreads each statistic into its own column with the arm as a row stub. Per-variablestatistic/decimalsresolve inside each band.pivot_across()’sdecimalsmay now be a list keyed byrow_groupvalues, for per-group precision in one call.preset(font_family = ...)generic chains ("mono"/"sans"/"serif") now lead with the ubiquitous Microsoft Office face (Courier New / Arial / Times New Roman) and keep the metric-compatible Liberation face as the last fallback, so Word shows a font the reader actually has installed on Windows / macOS instead of a phantom “Liberation Mono” in its font menu. The faces are metric-compatible, so layout, line breaks, and decimal alignment are unchanged. A barefont_family = "Courier New"now also leads with Courier New (previously resolved led by Liberation Mono). The package bundles no fonts; an arbitrary named face ("IBM Plex Mono","Source Code Pro", etc.) is emitted verbatim for the consuming application to resolve.
Breaking changes
-
col_spec()gained a singleindentargument (an integer for a fixed level on every body row, or a column name for per-row depth); the previousindent_byargument and theusage = "indent"value were removed in favour of it. An explicitindenton agroup_display = "header_row"host now suppresses the section auto-indent, yielding a single rather than double indent. -
paginate()removed the no-oppanels = "auto";panelsis now a positive integer. -
pivot_across()no longer pre-indentsstat_labelwith two leading spaces; indentation is applied downstream by the renderer viacol_spec(usage = "group")/group_display, so aheader_rowstub no longer double-indents.
Minor improvements and bug fixes
- Every warning now carries a
tabular_warning_<kind>class (input,runtime,layout,fidelity) mirroring thetabular_error_<kind>error taxonomy, so warnings can be caught selectively; the formertabular_warn_layoutclass was renamed totabular_warning_layout. -
col_spec()now warns whengroup_displayorgroup_skipis set on a non-group column (the knobs are inert unlessusage = "group"). -
cols()/cols_apply()now restore a column’s visibility and resetgroup_displayon a later call, and merge every column attribute field-completely (previously a default value could not be merged back and some fields could be dropped). -
emit()now centres decimal-aligned columns on every backend instead of right-aligning the uniformly NBSP-padded block, so the values sit under the centred column header (cross-backend parity). -
emit()to DOCX now declares an in-class<w:altName>fallback for each font, so a reader missing the primary face substitutes a metric-compatible face in the same class (mono stays mono) instead of panose-guessing to a serif. This brings DOCX in line with the RTF (\*\falt) and PDF / LaTeX (\IfFontExistsTFcascade) backends, which already declared the same in-class fallback chain. -
emit()to HTML now applies per-cell body alignment through a specificity-bumped.tabular-table td.text-*rule, so decimal, centred and right-aligned columns actually render aligned; previously the base.tabular-table td { text-align: left }rule outranked the plain alignment class and every body cell silently fell back to left. -
emit()to HTML now aligns the running page header and footer to the table width via a centred fit-content container, instead of spanning the full document width. -
emit()to HTML and Markdown no longer repeats asubgroup()banner mid-table when a group is taller than one estimated page. The continuous backends draw one banner per subgroup value; the print-only page-break markers within a group are unchanged, and the paged backends (RTF, LaTeX, DOCX) still repeat the banner on every continuation page by design. -
emit()to PDF / LaTeX no longer renders the table wider than the printable area: tabularray adds per-column separation outside each column’swd, so the column widths are now reduced by that separation and the rendered table total matches the resolved width (and the RTF / DOCX cell widths) instead of bleeding into the right margin. (#27) -
emit()to PDF / LaTeX now renders column headers in bold, matching the DOCX, RTF and HTML backends. -
emit()to PDF / LaTeX now sets the body font size after\begin{document}(where it survives) and re-asserts it inside the running header / footer, so an 8pt table no longer renders its body or page chrome at the 10pt document-class default. (#27) -
emit()to PDF / LaTeX now tightens the gap between a running page header and the title (and body to a running footer) to one line via\headsepand\footskip, instead of the wide document-class default. -
emit()to PDF / LaTeX no longer overflows a figure onto a second page: the image is placed with flexible\vfillglue rather than a fixed-height box that reconstructed to just over the page height, so a multi-page figure (for example one Kaplan-Meier plot per treatment arm) now fits one page per plot. -
emit()to RTF and DOCX no longer emits a phantom blank page after each figure plot. The figure box reserves a line for the closing paragraph every paged backend appends after the exact-height image, RTF exits table context with\pard\parbefore the next section break, and DOCX starts each continuation page with a structural<w:pageBreakBefore/>instead of a standalone page-break paragraph that stranded a blank page. A multi-page figure driven by per-pagemetaalso sizes each page’s image box from that page’s interpolated footnotes, so a longer-footnote page no longer overflows. -
emit()aborts with a cleartabular_error_layoutmessage when a figure’s titles and footnotes alone exceed the printable height, instead of failing with an opaque graphics-device error. -
emit()to RTF now leads the body font slot with the first face of an explicitfont_familystack instead of the Linux-first default chain, and classes a mono stack\fmodern(fixed pitch) the same way DOCX classes itmodern. Afont_family = c("Courier New", "Liberation Mono", "Courier")request now renders as Courier New mono in Word instead of substituting a serif, because the named primary face leads the font table rather than being demoted to a\*\faltalternate. -
emit()to RTF now emits a section break only BETWEEN panels (n-1 breaks for n panels) rather than after every panel, so a single-panel table no longer ends with a trailing section break that Word renders as a phantom blank page. -
emit()to RTF now reserves one line per running-header row in\headery, so a multi-row page header (for example a protocol row plus an analysis-set row) no longer bleeds back into the body and tips the last table rows, or the table’s trailing paragraph, onto a phantom second page. -
emit()now renders a zero-row empty-state page as a normal table whose body is a single full-span, horizontally centred “no data” message row, where the first data row would sit: the table chrome (titles, column header) leads, the message follows in the body, and the footnote trails immediately, all flowing compactly at the top of the page with blank space below. The message is centred by each backend’s native cell alignment on every backend (RTF, LaTeX, PDF, HTML, DOCX, Markdown); it is no longer vertically centred or relocated into the page margins. A natural-height message row plus trailing footnote cannot overflow, so the recurring phantom-page bug stays fixed. Asubgroup(keep_empty = TRUE)empty crossing renders the same way, as one message row in its own panel. -
emit()now closes a zero-row empty-state table’s data region with the body bottom rule on every backend, so the “no data” page carries the same closing rule a populated table does. The rule follows therulespreset (a custombottomrulewidth / style / colour is honoured, andbottomrule = "none"drops it) instead of being absent (RTF / DOCX) or a fixed default. -
emit()to PDF / LaTeX now wraps a table footnote to the table width rather than overrunning it: the footnote minipage no longer double-counts the column separation that the column widths already fold in, so on a narrow table the footnote text stays within the table-width footnote rule instead of spilling past it (and past the printable width). -
figure()and paged-table pagination now size the body box from the number of wrapped chrome lines a long title or footnote actually occupies at the printable width, not the element count, so a wrapped footnote no longer pushes content onto a second page on DOCX (and any paged backend). Wrapping is computed by greedy word packing against the font metrics. (#26) -
paginate()now collapses a zero-row table to a single empty-state page; horizontalpanelsno longer multiply an empty table into several identical blank pages. -
pivot_across()now resolves a keyedstatistic(e.g.list(hierarchical = "{n} ({p}%)")) against each row’s real ARD context on the hierarchical path, so a list keyed byhierarchicalno longer falls through to the bare{n}default and silently drops the percent. -
pivot_across()now warns when theoveralllabel collides with an existing arm name, instead of silently merging the pooled rows into that arm’s column. -
preset()’sdecimal_metricsknob gained"afm"(now the default), making decimal alignment width-exact in proportional fonts via the bundled font metrics; Markdown output keeps character padding. -
preset()gained anempty_textknob, a house-style default for the zero-row message that a per-tabletabular(empty_text = ...)still overrides. -
preset(spacing = ...)now drives the blank lines around a subgroup banner (thesubgroupregion) and around afigure()title and footnote; the Markdown table title and footnote gaps now follow the same knob. -
preset(width_mode = "window")now sizes auto columns proportionally to their content width to fill the page, instead of giving every column an equal share. -
subgroup()gained akeep_emptyargument; withkeep_empty = TRUEa zero-N crossing is retained and rendered as an empty-state page carrying its banner instead of being dropped. -
subgroup()no longer errors on a zero-row input; the table renders the empty-state placeholder once with no subgroup banner. -
tabular()gainedempty_text, the message shown when a table has zero data rows (default “No data available to report”). Zero-row tables now render the full page chrome and the column headers with the message placed in the body, replacing the previous bare “(no rows)” marker.
tabular 0.1.0
CRAN release: 2026-06-11
First release. tabular renders pre-summarised clinical tables and listings to RTF, LaTeX, HTML, PDF, and DOCX from one immutable verb pipeline, with no external Java or SAS dependency. This initial CRAN version consolidates the entire pre-release development cycle: every feature and bug fix below was designed, implemented, and tested before this first release.
Scope. This release covers tables and listings. Figure (graph) output is not yet supported and is the focus of the next release.
Build pipeline
-
tabular()takes a pre-summarised wide data frame (one input row is one display row) plus multi-linetitlesandfootnotes. -
cols()/col_spec()set per-column usage, label, format, width, alignment (including decimal alignment via bundled font metrics), visibility, NA text, per-row indent (indent_by), and group display.cols()takes a.defaultfallbackcol_spec(), andcols_apply()applies onecol_spec()to many columns (by name or predicate), resolving a{.name}token in a label to each matched column. -
headers()builds multi-level column-header bands with passthrough leaves;sort_rows()sorts on hidden numeric keys;subgroup()partitions with a{col}banner template and optional per-pagebig_n. -
style()applies predicate-targeted cell styling through thecells_*()location helpers;preset()/set_preset()/get_preset()set cosmetic defaults (fonts, colours, rules, padding, alignment, page chrome) per table or per session, with reusable house styles viastyle_template(). -
footnote()attaches auto-numbered footnotes (letters, numbers, or symbols) to any cell, column header, or title, deduped byidand byte-identical across backends. -
paginate()does group-aware pagination with an auto-computed per-page row budget;emit()writes the chosen backend (creating parent directories withcreate_dir), andas_grid()resolves the grid without I/O.
Rendering
- Native emission to RTF, LaTeX, HTML, PDF (via LaTeX), and DOCX from a single resolved grid; output is verified by per-backend byte snapshots and a cross-backend CDISC-pilot qualification (
inst/qualification/). - Inline markup via
md()andhtml(). - Glue-style
{expr}interpolation in static labels, titles, footnotes, and header band labels, evaluated in the calling environment at build time. - Significant-whitespace preservation across all backends (
preset(whitespace = "preserve"), the default). -
check_latex()reports LaTeX-package availability for PDF output and prints the exacttlmgr_install()remedy for anything missing — including a CTAN-mirror pin (tinytex::tlmgr_repo("auto")) when an install stalls behind the redirecting default mirror (commonly on Windows);check_fonts()does the same for fonts, per backend.