Initial development version.
read_shell() parses a YAML display shell – titles, subtitles, footnotes, source line, population filter, column structure with spanning headers, row structure with per-row statistics, and pagination – into a validated shell2tlf_shell. new_shell() does the same from a list. Every structural problem is reported in one error, not one at a time. YAML’s y/n/yes/no booleans are kept as text so the statistic n and the flag value "Y" survive parsing.shell_code() emits readable, runnable {rtables} source for the display – population filter, factor set-up, column layout, one layout step per shell row, build_table() and any post-build sorting. print() renders it as a code block; writeLines() saves it as a program.build_tlf() evaluates exactly the code shell_code() prints and returns a plain {rtables} TableTree, so the reviewed code and the delivered table cannot drift apart.render_tlf() writes RTF with {r2rtf}, taking titles, footnotes, source, orientation and rows-per-page from the shell. Spanning headers are merged into single wide RTF cells; row nesting becomes a real left indent.validate_shell() checks a shell against real data and returns a tibble of problems – absent variables, statistics asked of the wrong type, empty by-groups, missing denominator datasets, levels declared but not present.inst/extdata: a demographics summary with a spanning header, and a system-organ-class / preferred-term adverse-event table with subject-level counts and frequency sorting.display: listing). One row per record, in the order listing.sort_by gives, with one column per listing.vars entry labelled by listing.labels. A listing is deliberately not de-duplicated: the emitted program has no aggregation step, so nothing can collapse a repeated key. It is built with rtables::df_to_tt() and comes back as an ordinary TableTree, so render_tlf() needed no listing branch. columns: and rows: are refused in a listing shell rather than silently ignored. Worked example: inst/extdata/ae_listing.yaml.type: shift). baseline_var cross-tabulated against var over a shared levels set that is required, not inferred, so the table stays square. The denominator is stated in the shell, never guessed: denominator: baseline_row (subjects or records in the same baseline category and the same column, each block summing to 100%) or denominator: column (the column N). The baseline_row denominator is printed on the baseline group row, so the number the percentages were divided by is on the page. The comment above denom in the generated code names the population as well. Worked example: inst/extdata/lab_shift.yaml, built on pharmaverseadam::adlb.validate_shell() counts the records whose baseline or post-baseline value falls outside the shell’s levels. They leave both the numerator and the denominator, and an unnoticed handful is how a shift table stops reconciling with its own column header. The shipped fixture has one such record, and the example shows it.as.character(); rounding and formatting of derived variables belong in the ADaM step.build_tlf() evaluates the generated program. That is the design – see the “Trust boundary” section of ?build_tlf. A shell is as trusted as an R script the same person would otherwise have written: read one you did not write before you build it.