#' Display module documentation
#'
#' \code{box::help} displays help on a module’s objects and functions in much
#' the same way \code{\link[utils]{help}} does for package contents.
#'
#' @usage \special{box::help(topic, help_type = getOption("help_type", "text"))}
#' @param topic either the fully-qualified name of the object or function to get
#' help for, in the format \code{module$function}; or a name that was exported
#' and attached from an imported module or package.
#' @param help_type character string specifying the output format; currently,
#' only \code{'text'} is supported.
#' @return \code{box::help} is called for its side effect when called directly
#' from the command prompt.
#' @details
#' See the vignette at \code{vignette('box', 'box')} for more information about
#' displaying help for modules.
#' @export
help = function (topic, help_type = getOption('help_type', 'text')) {
topic = substitute(topic)
target = help_topic_target(topic, parent.frame())
target_mod = target[[1L]]
subject = target[[2L]]
if (! inherits(target_mod, 'box$mod')) {
throw('{topic;"} is not a valid module help topic')
}
if (subject != '.__module__.') {
obj = if (
exists(subject, target_mod, inherits = FALSE)
&& ! bindingIsActive(subject, target_mod)
) get(subject, envir = target_mod, inherits = FALSE)
if (inherits(obj, 'box$mod')) {
target_mod = obj
subject = '.__module__.'
}
}
info = attr(target_mod, 'info')
mod_name = strsplit(attr(target_mod, 'name'), ':')[[1L]][2L]
if (inherits(info, 'box$pkg_info')) {
help_call = if (subject == '.__module__.') {
bquote(help(.(as.name(mod_name))))
} else {
bquote(help(topic = .(subject), package = .(mod_name)))
}
return(call_help(help_call, parent.frame()))
}
if (! requireNamespace('roxygen2')) {
throw('displaying documentation requires {"roxygen2";\'} installed')
}
mod_ns = attr(target_mod, 'namespace')
all_docs = namespace_info(mod_ns, 'doc')
if (is.null(all_docs)) {
all_docs = parse_documentation(info, mod_ns)
namespace_info(mod_ns, 'doc') = all_docs
}
doc = all_docs[[subject]]
if (is.null(doc)) {
if (subject == '.__module__.') {
throw('no documentation available for {mod_name;"}')
} else {
throw('no documentation available for {subject;"} in module {mod_name;"}')
}
}
display_help(doc, mod_name, help_type)
}
#' Parse a module’s documentation
#'
#' @param info The module info.
#' @param mod_ns The module namespace.
#' @return \code{parse_documentation} returns a list of character strings with
#' the Rd documentation source code for each documented name in a module.
#' @keywords internal
parse_documentation = function (info, mod_ns) {
rdfiles = parse_roxygen_tags(info, mod_ns)
# Due to aliases, documentation entries may have more than one name.
aliases = map(function (rd) unique(rd$get_value('alias')), rdfiles)
names = rep(names(rdfiles), lengths(aliases))
docs = patch_mod_doc(stats::setNames(rdfiles[names], unlist(aliases)))
lapply(docs, format, wrap = FALSE)
}
#' @keywords internal
#' @rdname parse_documentation
parse_roxygen_tags = function (info, mod_ns) {
mod_path = info$source_path
blocks = roxygen2::parse_file(mod_path, mod_ns)
roxygen2::roclet_process(
roxygen2::rd_roclet(),
blocks,
mod_ns,
dirname(mod_path)
)
}
#' @param docs the list of \pkg{roxygen2} documentation objects.
#' @keywords internal
#' @rdname parse_documentation
patch_mod_doc = function (docs) {
if ('.__module__.' %in% names(docs)) {
mod_doc = docs[['.__module__.']]
mod_doc$sections$docType$value = 'package'
mod_doc$sections$usage = NULL
mod_doc$sections$format = NULL
mod_doc$sections$name$value = 'module'
}
docs
}
#' Helper functions for the help functionality
#'
#' \code{help_topic_target} parses the expression being passed to the
#' \code{help} function call to find the innermost module subset expression in
#' it.
#' \code{find_env} acts similarly to \code{\link[utils]{find}}, except that it
#' looks in the current environment’s parents rather than in the global
#' environment search list, it returns only one hit (or zero), and it returns
#' the environment rather than a character string.
#' \code{call_help} invokes a \code{help()} call expression for a package help
#' topic, finding the first \code{help} function definition, ignoring the one
#' from this package.
#'
#' @param topic the unevaluated expression passed to \code{help}.
#' @param caller the environment from which \code{help} was called.
#' @return \code{help_topic_target} returns a list of two elements containing
#' the innermost module of the \code{help} call, as well as the name of the
#' object that’s the subject of the \code{help} call. For \code{help(a$b$c$d)},
#' it returns \code{list(c, quote(d))}.
#' @name help-internal
#' @keywords internal
help_topic_target = function (topic, caller) {
inner_mod = function (mod, expr) {
name = if (is.name(expr)) {
as.character(expr)
} else if (is.call(expr) && expr[[1L]] %==% quote(`$`)) {
mod = Recall(mod, expr[[2L]])
as.character(expr[[3L]])
} else {
throw('{topic;"} is not a valid module help topic', call = call)
}
get(name, envir = mod)
}
call = sys.call(-1L)
if (is.name(topic)) {
obj = inner_mod(caller, topic)
if (inherits(obj, 'box$mod')) {
list(obj, '.__module__.')
} else {
name = as.character(topic)
list(find_env(name, caller), name)
}
} else {
list(inner_mod(caller, topic[[2L]]), as.character(topic[[3L]]))
}
}
#' @param name the name to look for.
#' @name help-internal
#' @keywords internal
find_env = function (name, caller) {
while (caller %!=% emptyenv()) {
if (exists(name, envir = caller, inherits = FALSE)) return(caller)
caller = parent.env(caller)
}
NULL
}
#' @param call the patched \code{help} call expression.
#' @name help-internal
call_help = function (call, caller) {
# Search for `help` function in caller scope. This is intended to find the
# first help call which is either `utils::help` or potentially from another
# environment, such as `devtools_shims`.
# Unfortunately during testing (and during development) the current package
# *is* attached, so we can’t just use `get` in the global/caller’s
# environment — it would recurse indefinitely to this package’s `help`
# function. To fix this, we need to manually find the first hit that isn’t
# inside this package.
candidates = utils::getAnywhere('help')
envs = map(environment, candidates$objs)
valid = candidates$visible & map_lgl(is.function, candidates$objs)
other_helps = candidates$objs[valid & ! map_lgl(identical, envs, base::topenv())]
call[[1L]] = other_helps[[1L]]
eval(call, envir = caller)
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.