This post was featured on the R Weekly highlights podcast hosted by Eric Nantz and Mike Thomas.
You might know that it’s possible to extend roxygen2 to do all sorts of cool things including but not limited to:
- documenting your internal functions for developers only (that’s devtag by my cynkra colleague Antoine Fabri),
- recording your following statistical software standards (that’s srr by my rOpenSci colleague Mark Padgham),
- writing tests from within R scripts (that’s roxytest by Mikkel Meyer Andersen).
I did something much more basic but still challenging to me: creating a custom tag that adds a new section to a manual page. In this post I’ll summarize how to go about that.
Context: need for a custom tag in the igraph R package
The igraph R package outsources a lot of its work to the igraph C library. Sometimes, the R package docs therefore have to try and catch up with the C library docs. One idea we’ve had when thinking about the problem is to link to relevant C docs from each R manual page: if a function called igraph::empty_graph()
uses igraph_empty
under the hood, then its manual page should link to https://igraph.org/c/doc/igraph-Basic.html#igraph_empty. It means the users would be able to easily browse the C docs, and as a side-effect they’d be aware of what part of the C library supports what functionality in the R package.
A workflow to make that happen is the following:
- have a sitemap of the C docs at hand (we parse the index page of the C docs);
- add
@cdocs
tags in the source of the R package, for instance@cdocs igraph_empty
near the function definition (adding them semi-automatically will be a topic for another day 😅); - have some roxygen2 machinery parsing the
@cdocs
into links in the manual page when we calldocument()
.
This blog post is about the roxygen2 machinery as I had to piece together several things I saw online.
Minimal example
Create our minimal R package
The hgb package has one script whose docs use a custom @custom
tag:
#' A function messaging a thing
#'
#' @custom Hello I am here
#'
#' @return Nothing, print thing
#' @export
#'
#' @examples
#' thing()
thing <- function() {
message("thing")
}
Create a package holding the custom tag
In a script I have the following lines that mostly come from an roxygen2 vignette, in which the creation of a custom tag is well documented (its usage less so 😅).
There’s a method roxy_tag_parse.roxy_tag_custom()
telling roxygen2 how to parse the tag, a method roxy_tag_rd.roxy_tag_custom
telling roxygen2 what to do with the value: a special “custom” section. Then there’s a method format.rd_section_custom()
telling roxygen2 how to build the “custom” section. Here it’s just pasting. In my real use case I defined a helper function to identify the C docs link corresponding to a given tag value.
#' @importFrom roxygen2 roxy_tag_parse
#' @importFrom roxygen2 roxy_tag_rd
NULL
#' @export
# same as https://roxygen2.r-lib.org/articles/extending.html
roxy_tag_parse.roxy_tag_custom <- function(x) {
roxygen2::tag_markdown(x)
}
#' @export
# same as https://roxygen2.r-lib.org/articles/extending.html
roxy_tag_rd.roxy_tag_custom <- function(x, base_path, env) {
roxygen2::rd_section("custom", x$val)
}
#' @export
# same as https://roxygen2.r-lib.org/articles/extending.html
format.rd_section_custom <- function(x, ...) {
paste0(
"\\section{Custom section}{\n",
"\\itemize{\n",
paste0(" \\item ", x$value, "\n", collapse = ""),
"}\n",
"}\n"
)
}
Create a roclet for sharing the custom tag
Even if all we do is creating a custom roxygen2 tag, we need to define a roclet as it’ll be the vessel by which another package can use the custom tag. You can have a custom tag defined inside a package without a roclet if you use the custom tag within the same package. But to cross package boundaries you need a roclet.
Further in the same script I have those lines:
#' @export
# https://github.com/shahronak47/informationtag
custom_roclet <- function() {
roxygen2::roclet("custom")
}
#' @importFrom roxygen2 block_get_tags roclet_process
#' @method roclet_process roclet_custom
#' @export
# https://github.com/shahronak47/informationtag
roclet_process.roclet_custom <- function(x, blocks, env, base_path) {
x
}
#' @export
#' @importFrom roxygen2 block_get_tags roclet_output
roclet_output.roclet_custom <- function(x, results, base_path, ...) {
x
}
You’ll notice we defined something called “custom_roclet” but now the methods are for “roclet_custom” (inverted word order). This confused me at first. Furthermore, I had no idea what the process and output methods were supposed to do, so I used the demo by Ronak Shah. Lastly, the roxygen2 tags used in that demo didn’t work for me so I stole the ones used by Antoine Fabri in devtag.
Build the package holding the custom tag
By that I mean running document()
and installing the myroxy package on my machine to try it.
Register the roclet in our minimal package
I added the following lines to the DESCRIPTION
of the hgb package to indicate to roxygen2 that there’s another package to use when documenting my package:
Roxygen: list(markdown = TRUE, roclets = c("collate", "rd", "namespace",
"myroxy::custom_roclet"), packages = "myroxy")
I also added these lines so that fellow developers might know where to get myroxy from1:
Config/Needs/build: roxygen2, devtools, maelle/myroxy
Try it
If all goes well, running document()
in the hgb package works without any error. The manual page for ?thing
has the custom section.
Do I need an external package?
The roxygen2 extensions I mentioned in the introduction (devtag, srr, roxytag) are standalone packages that you can use when building your own package, because their use case is fairly general. Here, I am developing a custom tag for igraph only. So in theory, the infrastructure could live inside the R package itself.
I actually started by implementing the custom tag within the igraph package but I was not a fan of the clutter it created in data-raw
, R
, the NAMESPACE
, etc. It was great to start this way as it was much easier (no roclet!), but I ended up factoring out the roxygen2 code in a distinct GitHub-only package called igraph.r2cdocs
.
Sources
I am very thankful to the authors of roxygen2 obviously, and to the authors of the following resources:
- roxygen2 vignette “Extending roxygen2” that says of itself that it’s “very rough”. For me the missing part was understanding what goes in what package (the package where you define the tags, the package where you use the tags); and I’d have obviously liked some explanations on roclets that are just there for a basic custom tag, as opposed to printing content / saving information in other files. I want docs just for me. 😉
- Stack Overflow post
- Video by Ronak Shah that I didn’t watch and related repositories that I did stare at a lot: informationtag and infotagdemo. This is how I understood what the “process” and “output” methods of my roclet had to look like.
- devtag source which is where I got the necessary roxygen2 tags for the “process” and “output” methods of my roclet (the ones from the aforementioned demo didn’t work for me).
Conclusion
In this post I explained the creation and usage of a custom roxygen2 tag, with a minimal example. If you’re curious about the igraph use case, find the demo PR to igraph and the igraph.r2cdocs package. Have you ever extended roxygen2? What were the difficulties in your experience?
-
This is a custom…
DESCRIPTION
field. ↩︎