From 70dcf11b1ef7046498b3b81e96375489814c355f Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Sat, 6 Jun 2026 22:07:42 -0500 Subject: [PATCH 1/3] Render @section blocks in function and @rdname Rd output @section was parsed into tags$sections but only emitted by the package Rd generator. For ordinary exported functions and @rdname groups the section was silently dropped, so a documented invariant (e.g. corteza's subagent_spawn Permissions section) never reached the generated Rd. Add a render_sections() helper and call it from generate_rd() and generate_rd_grouped() (after \details, like roxygen2), gathering sections from every member of an @rdname group. Refactor generate_package_rd() to use the same helper. Fixes #10. --- R/rd.R | 37 +++++++++++++++++++++++++------ inst/tinytest/test_rd.R | 44 +++++++++++++++++++++++++++++++++++++ inst/tinytest/test_rdname.R | 32 +++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/R/rd.R b/R/rd.R index 8329784..5100705 100644 --- a/R/rd.R +++ b/R/rd.R @@ -1,3 +1,22 @@ +#' Render User-Defined @section Blocks to Rd +#' +#' Emits one `\\section{title}{content}` per parsed `@section`. Content is +#' passed through verbatim as Rd (tinyrox does no markdown parsing), matching +#' how the title-only macros elsewhere treat hand-written Rd markup. +#' +#' @param sections List of `list(title=, content=)` from parse_tags(). +#' @return Character vector of Rd lines (empty when there are no sections). +#' @keywords internal +render_sections <- function(sections) { + lines <- character() + for (sec in sections) { + lines <- c(lines, paste0("\\section{", escape_rd(sec$title), "}{")) + lines <- c(lines, sec$content) + lines <- c(lines, "}") + } + lines +} + #' Generate Rd File Content #' #' @param tags Parsed tags from parse_tags(). @@ -97,6 +116,9 @@ generate_rd <- function(tags, formals = NULL, source_file = NULL, lines <- c(lines, "}") } + # User-defined @section blocks (after details, like roxygen2) + lines <- c(lines, render_sections(tags$sections)) + # References if (!is.null(tags$references)) { lines <- c(lines, "\\references{") @@ -625,6 +647,13 @@ generate_rd_grouped <- function(topic, entries, all_tags, lines <- c(lines, "}") } + # User-defined @section blocks - concatenated from all blocks + all_sections <- list() + for (entry in entries) { + all_sections <- c(all_sections, entry$tags$sections) + } + lines <- c(lines, render_sections(all_sections)) + # \references{} - from primary if (!is.null(primary$tags$references)) { lines <- c(lines, "\\references{") @@ -881,13 +910,7 @@ generate_package_rd <- function(tags, pkg_name, source_file) { } # User-defined sections (@section Title: ...) - if (!is.null(tags$sections)) { - for (sec in tags$sections) { - lines <- c(lines, paste0("\\section{", escape_rd(sec$title), "}{")) - lines <- c(lines, sec$content) # Content may have Rd markup, don't escape - lines <- c(lines, "}") - } - } + lines <- c(lines, render_sections(tags$sections)) # Auto-generated function index lines <- c(lines, paste0("\\section{Package Content}{\\packageIndices{", diff --git a/inst/tinytest/test_rd.R b/inst/tinytest/test_rd.R index 435a7a3..9659565 100644 --- a/inst/tinytest/test_rd.R +++ b/inst/tinytest/test_rd.R @@ -52,6 +52,50 @@ rd_alias <- tinyrox:::generate_rd(tags_alias, list(names = c("x", "y"), usage = expect_true(grepl("\\\\alias\\{plus\\}", rd_alias)) expect_true(grepl("\\\\alias\\{sum2\\}", rd_alias)) +# Test @section blocks are rendered (#10) - parse -> generate, the real path +sec_block <- c( + "Example function", + "", + "Main description.", + "", + "@section Permissions:", + "This text should appear in generated Rd.", + "@export" +) +sec_tags <- tinyrox:::parse_tags(sec_block, "f") +expect_equal(length(sec_tags$sections), 1L) +rd_sec <- tinyrox:::generate_rd(sec_tags, list(names = character(), usage = "")) +expect_true(grepl("\\\\section\\{Permissions\\}\\{", rd_sec)) +expect_true(grepl("This text should appear in generated Rd", rd_sec)) + +# Multi-word section title +sec_block2 <- c( + "Title", "", "Desc.", + "@section Special Permissions:", + "Body text.", + "@export" +) +sec_tags2 <- tinyrox:::parse_tags(sec_block2, "g") +rd_sec2 <- tinyrox:::generate_rd(sec_tags2, list(names = character(), usage = "")) +expect_true(grepl("\\\\section\\{Special Permissions\\}\\{", rd_sec2)) + +# Multiple @section blocks on one function, in order +sec_block3 <- c( + "Title", "", "Desc.", + "@section First:", "One.", + "@section Second:", "Two.", + "@export" +) +sec_tags3 <- tinyrox:::parse_tags(sec_block3, "h") +expect_equal(length(sec_tags3$sections), 2L) +rd_sec3 <- tinyrox:::generate_rd(sec_tags3, list(names = character(), usage = "")) +expect_true(grepl("\\\\section\\{First\\}\\{", rd_sec3)) +expect_true(grepl("\\\\section\\{Second\\}\\{", rd_sec3)) +expect_true(regexpr("First", rd_sec3) < regexpr("Second", rd_sec3)) + +# No @section -> no \section{} block leaks in +expect_false(grepl("\\\\section\\{", rd)) + # Test with keywords tags_kw <- tags tags_kw$keywords <- c("internal", "math") diff --git a/inst/tinytest/test_rdname.R b/inst/tinytest/test_rdname.R index 86e54c2..10aea27 100644 --- a/inst/tinytest/test_rdname.R +++ b/inst/tinytest/test_rdname.R @@ -236,6 +236,38 @@ expect_true(grepl("func_a\\(1\\)", rd_c)) expect_true(grepl("func_b\\(2\\)", rd_c)) +# --- Test: @section blocks from all members of an @rdname group (#10) --- + +blocks_sec <- list( + list( + lines = c("Topic", "", "Desc.", "@section Alpha:", "Alpha body.", + "@rdname secgroup", "@export"), + object = "sec_a", + type = "function", + formals = list(names = "x", usage = "x"), + file = "test.R", + line = 1 + ), + list( + lines = c("@section Beta:", "Beta body.", "@rdname secgroup", "@export"), + object = "sec_b", + type = "function", + formals = list(names = "y", usage = "y"), + file = "test.R", + line = 10 + ) +) +tags_sa <- tinyrox:::parse_tags(blocks_sec[[1]]$lines, "sec_a") +tags_sb <- tinyrox:::parse_tags(blocks_sec[[2]]$lines, "sec_b") +entries_s <- list( + list(tags = tags_sa, block = blocks_sec[[1]]), + list(tags = tags_sb, block = blocks_sec[[2]]) +) +rd_s <- tinyrox:::generate_rd_grouped("secgroup", entries_s, list()) +expect_true(grepl("\\\\section\\{Alpha\\}\\{", rd_s)) +expect_true(grepl("\\\\section\\{Beta\\}\\{", rd_s)) + + # --- Test 7: @rdname params documented on a sibling block (issue #12) --- # A function whose formals are documented on the primary block (not its own) # must NOT be flagged as having undocumented parameters: blocks sharing an From eb52449c54f3b7996f86b3340851e293bf14c85d Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Sat, 6 Jun 2026 22:08:13 -0500 Subject: [PATCH 2/3] document: add render_sections.Rd --- man/render_sections.Rd | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 man/render_sections.Rd diff --git a/man/render_sections.Rd b/man/render_sections.Rd new file mode 100644 index 0000000..4ec1b33 --- /dev/null +++ b/man/render_sections.Rd @@ -0,0 +1,19 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{render_sections} +\alias{render_sections} +\title{Render User-Defined @section Blocks to Rd} +\usage{ +render_sections(sections) +} +\arguments{ +\item{sections}{List of `list(title=, content=)` from parse_tags().} +} +\value{ +Character vector of Rd lines (empty when there are no sections). +} +\description{ +Emits one `\\section{title}{content}` per parsed `@section`. Content is +passed through verbatim as Rd (tinyrox does no markdown parsing), matching +how the title-only macros elsewhere treat hand-written Rd markup. +} +\keyword{internal} From eea4adf00b35d890bd2accd63ba0738a3e61942b Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Sat, 6 Jun 2026 22:08:41 -0500 Subject: [PATCH 3/3] Bump version to 0.3.3.3 --- DESCRIPTION | 2 +- NEWS.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 5b9076c..73c4df2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: tinyrox Title: Minimal R Documentation Generator -Version: 0.3.3.2 +Version: 0.3.3.3 Authors@R: person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"), comment = c(ORCID = "0009-0005-4248-604X")) diff --git a/NEWS.md b/NEWS.md index 8d017e6..4b627f8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# tinyrox 0.3.3.3 + +* Render `@section` blocks in the Rd for ordinary functions and `@rdname` groups. They were parsed but only emitted for package-level docs, so a function's `@section` was silently dropped (#10). + # tinyrox 0.3.3.2 * Fix false "undocumented parameters" warning for functions documented via a sibling block in an `@rdname` group; the check is now group-wide. Also gate the warning on `cran_check` rather than `silent` (#12).