INTERNAL

Avoid R CMD check notes about undefined global objects used in magrittr pipes

cf. https://github.com/tidyverse/magrittr/issues/29#issuecomment-74313262

utils::globalVariables(names = c(".",
                                 "heading_lvl",
                                 "i_subtitle",
                                 "i_title",
                                 "is_description_heading",
                                 "is_heading_ignored",
                                 "is_help_topic",
                                 "subnode_ix"))

Constants

data_special_headings

data_special_headings <-
  tibble::tibble(type = character(),
                 heading_texts = list(),
                 meaning = character()) |>
  tibble::add_row(type = "description",
                  heading_texts = list("DESCRIPTION"),
                  meaning = paste0("Paragraph(s) below this heading, which in turn is hierarchically below a `title` or `subtitle` heading, are used as the ",
                                   "description for this very (sub)title (`desc` in the pkgdown reference index).")) |>
  tibble::add_row(type = "ignore_title",
                  heading_texts = list("EXPORTED"),
                  meaning = paste0("Will never be used as `title` or `subtitle` in the pkgdown reference index (i.e. ignored). Simply serves as a (usually ",
                                   "top-level) heading to indicate that the objects defined below it are ",
                                   "[exported](https://r-pkgs.org/dependencies-in-practice.html#exports) by the package.")) |>
  tibble::add_row(type = "ignore_content",
                  heading_texts = list(c("INTERNAL",
                                         "NOTES",
                                         "TEMPORARY",
                                         "TMP")),
                  meaning = paste0("Everything that's hierarchically below one of these headings is completely ignored for pkgdown reference index ",
                                   "generation."))

Pre-assign each heading type to a character vector variable for optimal performance.

# `envir = parent.frame(5L)` doesn't assign in the right environment for some reason, so we explicitly provide the env
pkg_env <- rlang::current_env()

data_special_headings |>
  dplyr::group_split(type) |>
  purrr::walk(\(x) assign(x = paste0("heading_texts_", x$type),
                          value = purrr::list_c(x$heading_texts,
                                                ptype = character()),
                          envir = pkg_env))
rm(pkg_env)

Functions

assemble_copyright_notice

Assemble a package's copyright notice

assemble_copyright_notice <- function(path) {

  if (fs::is_dir(path)) {
    path <- fs::path(path, "DESCRIPTION")
  }
  rlang::check_installed("desc",
                         reason = pal::reason_pkg_required())
  notice <- character()

  if (desc::desc_has_fields(keys = "Authors@R",
                            file = path)) {

    pkg <- pal::desc_get_field_safe(key = "Package",
                                    file = path)
    desc <- pal::desc_get_field_safe(key = "Title",
                                     default = "",
                                     file = path)
    authors <-
      desc::desc_get_authors(file = path) |>
      # reduce to copyright holders or otherwise authors
      pal::when(any(purrr::map_lgl(., \(x) "cph" %in% x$role)) ~ purrr::keep(., \(x) "cph" %in% x$role),
                any(purrr::map_lgl(., \(x) "aut" %in% x$role)) ~ purrr::keep(., \(x) "aut" %in% x$role),
                ~ .) |>
      format(include = c("given", "family")) |>
      pal::enum_str()

    if (length(authors) > 0L) {

      notice <- c(paste0(pkg, ": ", desc),
                  paste0("Copyright (C) ", format(Sys.Date(), "%Y"), "  ", authors))

    } else {
      cli::cli_alert_warning(paste0("Neither copyright holders nor authors found in {.field Authors@R} field in the package's {.file DESCRIPTION} file, thus ",
                                    "skipped adding copyright notice."))
    }

  } else {
    cli::cli_alert_warning(paste0("No {.field Authors@R} field present in the package's {.file DESCRIPTION} file, thus skipped adding license notice.",
                                  paste0(" Try converting the existing {.field Author} field to an {.field Authors@R} field using {.fun ",
                                         "desc::desc_coerce_authors_at_r}.")[desc::desc_has_fields("Author")]))
  }

  notice
}

assemble_license_notice

Assemble a package's license notice

assemble_license_notice <- function(path) {

  rlang::check_installed("desc",
                         reason = pal::reason_pkg_required())
  notice <- character()
  if (fs::is_dir(path)) path <- fs::path(path, "DESCRIPTION")

  if (desc::desc_has_fields(keys = "License",
                            file = path)) {

    license <- pal::desc_get_field_safe(key = "License",
                                        file = path)

    if (grepl(x = license,
              pattern = "^\\s*(AGPL ?\\(>= ?3\\)|AGPL-3\\.0-or-later)\\s*$")) {

      notice <- c(paste0("This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as ",
                         "published by the Free Software Foundation, either version 3 of the License, or any later version."),
                  paste0("This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of ",
                         "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details."),
                  paste0("You should have received a copy of the GNU Affero General Public License along with this program. If not, see",
                         " <https://www.gnu.org/licenses/>."))

    } else {
      cli::cli_alert_warning(paste0("{.field License} field in the package's {.file DESCRIPTION} file is not set to {.val AGPL (>= 3)}, thus skipped adding ",
                                    "license notice."))
    }

  } else {
    cli::cli_alert_warning("No {.field License} field present in the package's {.file DESCRIPTION} file, thus skipped adding license notice.")
  }

  notice
}

document_pkg

Builds a package's roxygen2 documentation.

document_pkg <- function(path,
                         ...) {

  cli::cli_progress_step(msg = "Building package documentation")

  devtools::document(pkg = path,
                     ...)

  cli::cli_progress_done()
}

extract_md_heading_content

Removes leading #s and trailing newlines.

extract_md_heading_content <- function(x,
                                       env) {

  x |>
    stringr::str_remove(pattern = "^#+") |>
    stringr::str_trim() |>
    purrr::map_chr(\(x) {
      if (x == "") {
        NA_character_
      } else {
        knitr::knit(text = x,
                    quiet = TRUE,
                    envir = env)
      }
    })
}

install_pkg

install_pkg <- function(path,
                        unload = FALSE,
                        quiet = FALSE,
                        ...) {

  cli::cli_progress_step(msg = "Building and installing package")
  pkg_name <- pkgload::pkg_name(path)

  # unregister pkg if it's attached
  if (pkg_name %in% loadedNamespaces()) {

    if (unload) {
      pkgload::unload(package = pkg_name,
                      quiet = quiet)
    } else {
      pkgload::unregister(package = pkg_name)
    }
  }

  devtools::install(pkg = path,
                    quiet = quiet,
                    ...)

  cli::cli_progress_done()

  invisible(path)
}

install_pkg_rstudio_api

install_pkg_rstudio_api <- function(quiet = FALSE) {

  cli::cli_alert_info("Building and installing package via RStudio API (see the {.strong Build} pane)")

  rstudioapi::executeCommand(commandId = "buildFull",
                             quiet = quiet)
}

main_rmd

#' Determine a package's main `.Rmd` source file
#'
#' Determines which R Markdown file under `Rmd/` in `path` is the package's main source file. If multiple `.Rmd` files are present, the one whose name matches
#' the package name is selected.
#'
#' @inheritParams purl_rmd
#'
#' @return A character vector, of length 1 if a main `.Rmd` is found, otherwise of length 0.
#' @keywords internal
#' @export
main_rmd <- function(path = ".") {

  rmd_file_paths <- rmd_files(path = path)
  n_rmd_file_paths <- length(rmd_file_paths)

  if (!n_rmd_file_paths) {

    cli::cli_abort("No {.file .Rmd} files found under {.arg path}.")

  } else if (n_rmd_file_paths == 1L) {

    result <- rmd_file_paths

  } else {

    result <-
      rmd_file_paths |>
      fs::path_file() |>
      fs::path_ext_remove() |>
      magrittr::is_in(pal::desc_get_field_safe(key = "Package",
                                               file = path)) |>
      which() %>%
      magrittr::extract(rmd_file_paths, .) |>
      pal::when(length(.) > 1L ~ # this is theoretically possible in case of subdirs under `Rmd/` or for case-sensitive filesystems (both `.Rmd` and `.rmd`)
                  cli::cli_abort(paste0("Multiple {.file .Rmd} files detected under {.path {fs::path(path, 'Rmd/')}} whose names match the package name: ",
                                        "{.file {.}}")),
                ~ .)
  }

  result
}

normalize_md_lf

Replaces single line feeds ("\n") with whitespaces (" "), except for a trailing line feed.

normalize_md_lf <- function(x) {

  stringr::str_replace_all(string = x,
                           pattern = "(?<!\\n)\\n(?!(\\n|$))",
                           replacement = " ")
}

process_rmd

Helper function purling a single file Rmd/*.Rmd to R/*.gen.R.

NOTES:

process_rmd <- function(path_file,
                        path_pkg,
                        copyright_notice,
                        license_notice,
                        line_width = 160L) {
  r_file_path <-
    path_file |>
    fs::path_file() |>
    fs::path_ext_set(ext = ".gen.R") %>%
    fs::path(path_pkg, "R", .)

  knitr::purl(input = path_file,
              output = r_file_path,
              quiet = TRUE,
              documentation = 0L)

  # add reminder header lines to generated R file
  result <-
    path_file %>%
    fs::path_rel(start = fs::path_dir(fs::path_dir(.))) %>%
    paste0("DO NOT EDIT THIS FILE BY HAND! Instead edit the R Markdown source file `", ., "` and run `pkgpurl::purl_rmd()`.") |>
    pal::as_comment_str("See `README.md#r-markdown-format` for more information on the literate programming approach used applying the R Markdown format.",
                        sep_paragraphs = FALSE) |>
    # add copyright and license notices if indicated
    paste0("\n",
           if (length(copyright_notice) > 0L) {
             pal::as_comment_str(copyright_notice, "",
                                 sep_paragraphs = FALSE)
           },
           if (length(license_notice) > 0L) {
             paste0(pal::as_comment_str(license_notice),
                    "\n")
           },
           brio::read_file(path = r_file_path))

  brio::write_file(text = result,
                   path = r_file_path)
}

restart_r

restart_r <- function() {

  rlang::check_installed("rstudioapi",
                         reason = pal::reason_pkg_required())

  if (rstudioapi::isAvailable()) {
    rstudioapi::restartSession()
  } else {
    cli::cli_alert_warning("Unable to restart R session because it is running outside of RStudio.")
  }
}

rmd_files

rmd_files <- function(path) {

  checkmate::assert_directory_exists(path,
                                     access = "r")
  checkmate::assert_scalar(path)

  fs::path(path, "Rmd") |>
    fs::path_abs() |>
    fs::dir_ls(recurse = TRUE,
               type = "file",
               regexp = "(?<!\\.nopurl)\\.[Rr]md$",
               perl = TRUE)
}

EXPORTED

High-level functions

create_pkg

#' Create new R Markdown package
#'
#' @description
#' Populates the directory specified via `path` with all the necessary files for a new R Markdown package.
#' 
#' The `DESCRIPTION` file is created using
#' [usethis::use_description()] and all fields except `Package`, `URL` and `BugReports` are sourced from the [`usethis.description` \R
#' option](https://usethis.r-lib.org/reference/use_description.html) if defined.
#'
#' @param name Package name.
#' @param id_netlify Netlify site identifier.
#' @param path Path to the new package directory.
#' @param incl_roxygen2_meta Whether or not to create a `man/roxygen/meta.R` file that i.a. stores the metadata for [roxygen2's `@family`
#'   tags](https://roxygen2.r-lib.org/articles/index-crossref.html#family).
#' @param incl_reexports Whether or not to create an `R/reexports.R` file prefilled with an opinionated set of magrittr and rland operator exports.
#' @param incl_sysdata_rmd Whether or not to create an `Rmd/sysdata.nopurl.Rmd` stub file.
#' @param incl_data_rmd Whether or not to create an `Rmd/data.nopurl.Rmd` stub file.
#' @param incl_asciicasts_rmd Whether or not to create an `Rmd/asciicasts.nopurl.Rmd` stub file.
#' @param incl_pkgdown_config Whether or not to create a minimal `pkgdown/_pkgdown.yml` configuration file.
#' @param incl_ripgrep_config Whether or not to create a ripgrep ignore file `.rgignore`.
#' @param incl_ack_config Whether or not to create an [`.ackrc` configuration file](https://beyondgrep.com/documentation/).
#' @param incl_air_config Whether or not to create an [`air.toml` configuration file](https://posit-dev.github.io/air/configuration.html). Note that air is not
#'   of much use yet for R Markdown packages.
#'
#' @returns `path`, invisibly.
#' @export
create_pkg <- function(name,
                       id_netlify = NULL,
                       path = ".",
                       incl_roxygen2_meta = TRUE,
                       incl_reexports = FALSE,
                       incl_sysdata_rmd = FALSE,
                       incl_data_rmd = FALSE,
                       incl_asciicasts_rmd = FALSE,
                       incl_pkgdown_config = TRUE,
                       incl_ripgrep_config = TRUE,
                       incl_ack_config = FALSE,
                       incl_air_config = FALSE) {

  checkmate::assert_string(name)
  checkmate::assert_string(id_netlify,
                           null.ok = TRUE)
  checkmate::assert_flag(incl_roxygen2_meta)
  checkmate::assert_flag(incl_reexports)
  checkmate::assert_flag(incl_sysdata_rmd)
  checkmate::assert_flag(incl_data_rmd)
  checkmate::assert_flag(incl_asciicasts_rmd)
  checkmate::assert_flag(incl_pkgdown_config)
  checkmate::assert_flag(incl_ripgrep_config)
  checkmate::assert_flag(incl_ack_config)
  checkmate::assert_flag(incl_air_config)

  tpl_pkg <- utils::packageName()

  # create dirs if necessary and set usethis proj
  if (fs::dir_exists(path)) {
    path <- fs::path(path, name)
  }
  fs::dir_create(path = path)

  # switch to target dir
  usethis::local_project(path = path,
                         force = TRUE)
  fs::dir_create("R")
  fs::dir_create("Rmd")

  # create files without any content interpolation ----
  usethis::use_template(template = "gitignore",
                        save_as = ".gitignore",
                        package = tpl_pkg)

  usethis::use_template(template = "LICENSE.md",
                        package = tpl_pkg)

  usethis::use_template(template = "PKG.Rproj",
                        save_as = fs::path(name,
                                           ext = "Rproj"),
                        package = tpl_pkg)

  usethis::use_template(template = "Rbuildignore",
                        save_as = ".Rbuildignore",
                        package = tpl_pkg)

  usethis::use_template(template = "R/PKG-package.R",
                        save_as = fs::path("R", paste0(name, "-package"),
                                           ext = "R"),
                        package = tpl_pkg)

  usethis::use_template(template = "Rmd/PKG.Rmd",
                        save_as = fs::path("Rmd", name,
                                           ext = "Rmd"),
                        package = tpl_pkg)

  if (incl_sysdata_rmd) {
    usethis::use_template(template = "Rmd/sysdata.nopurl.Rmd",
                          package = tpl_pkg)
  }

  if (incl_data_rmd) {
    usethis::use_template(template = "Rmd/data.nopurl.Rmd",
                          package = tpl_pkg)
  }

  if (incl_asciicasts_rmd) {
    usethis::use_template(template = "Rmd/asciicasts.nopurl.Rmd",
                          package = tpl_pkg)
  }

  if (incl_ack_config) {
    usethis::use_template(template = "ackrc",
                          save_as = ".ackrc",
                          package = tpl_pkg)
  }

  if (incl_air_config) {
    usethis::use_template(template = "air.toml",
                          package = tpl_pkg)
  }

  if (incl_roxygen2_meta) {
    fs::dir_create("man/roxygen")
    usethis::use_template(template = "man/roxygen/meta.R",
                          package = tpl_pkg)
  }

  if (incl_reexports) {
    usethis::use_template(template = "R/reexports.R",
                          package = tpl_pkg)
  }

  if (incl_ripgrep_config) {
    usethis::use_template(template = "rgignore",
                          save_as = ".rgignore",
                          package = tpl_pkg)
  }

  # create files customized to pkg ----
  usethis::use_template(template = "README.Rmd",
                        data = list(pkg = name,
                                    id_netlify = id_netlify),
                        package = tpl_pkg)

  if (!is.null(id_netlify)) {
    usethis::use_template(template = "netlify.toml",
                          data = list(pkg = name),
                          package = tpl_pkg)
  }

  if (incl_pkgdown_config) {
    fs::dir_create("pkgdown")
    usethis::use_template(template = "pkgdown/_pkgdown.yml",
                          data = list(pkg = name),
                          package = tpl_pkg)
  }

  usethis::use_description(fields = list(Package = name,
                                         URL = paste0("https://gitlab.com/rpkg.dev/", name),
                                         BugReports = paste0("https://gitlab.com/rpkg.dev/", name, "/-/issues")))
  invisible(path)
}

process_pkg

TODO:

NOTES:

#' Process R Markdown package from source to installation
#'
#' @description
#' Executes all steps to process an R package written in R Markdown format from source to installation in one go:
#'
#' 1. Purl all relevant `Rmd/*.Rmd` files to `R/*.gen.R` files using [purl_rmd()].
#' 2. Re-generate the [pkgdown reference index](https://pkgdown.r-lib.org/reference/build_reference.html#reference-index) based on the package's [main R
#'    Markdown file][main_rmd()] using [gen_pkgdown_ref()] (if `gen_pkgdown_ref = TRUE`).
#' 3. Re-build the package documentation using [devtools::document()] (if `document = TRUE`).
#' 4. Build and install the package (if `build_and_install = TRUE`). This is done either using
#'    [`rstudioapi::executeCommand(commandId = "buildFull")`][rstudioapi::executeCommand] (if `use_rstudio_api = TRUE`) or using [devtools::install()] (if
#'    `use_rstudio_api = FALSE`).
#' 5. Restarts the R session using [rstudioapi::restartSession()] (if either `restart_r_session = TRUE` or `use_rstudio_api = TRUE`).
#'
#' @details
#' Note that the installation via [devtools::install()] (i.e. `use_rstudio_api = FALSE`) is known to fail in certain situations (lazy-load database corruption)
#' due to [unresolved deficiencies](https://bugs.r-project.org/show_bug.cgi?id=16644) in \R's namespace unloading. If you encounter an error, simply restart the
#' \R session and try again.
#' 
#' `r pkgsnip::md_snip("rstudio_addin")`
#'
#' @inheritParams purl_rmd
#' @inheritParams devtools::document
#' @inheritParams devtools::install
#' @param document Whether or not to re-build the package documentation after purling `Rmd/*.Rmd` to `R/*.gen.R`.
#' @param build_and_install Whether or not to build and install the package after purling `Rmd/*.Rmd` to `R/*.gen.R`.
#' @param restart_r_session Whether or not to restart the R session. Highly recommended if `build_and_install = TRUE`, but only possible when R is run within
#'   RStudio. Note that the R session is always restarted if `use_rstudio_api = TRUE`.
#' @param use_rstudio_api Whether or not to rely on the RStudio API to install the built package (which always triggers an R session restart regardless of
#'   `restart_r_session`). If `NULL`, the RStudio API is automatically used if possible, i.e. RStudio is running. Note that installation without the RStudio API
#'   has known issues, see section *Details* below for further information.
#' @param quiet `r pkgsnip::param_lbl("quiet")`
#'
#' @inherit purl_rmd return
#' @family high_lvl
#' @export
process_pkg <- function(path = ".",
                        add_copyright_notice = funky::config_val("add_copyright_notice"),
                        add_license_notice = funky::config_val("add_license_notice"),
                        gen_pkgdown_ref = funky::config_val("gen_pkgdown_ref"),
                        env = parent.frame(),
                        document = TRUE,
                        build_and_install = TRUE,
                        restart_r_session = build_and_install,
                        use_rstudio_api = NULL,
                        quiet = TRUE,
                        roclets = NULL,
                        args = getOption("devtools.install.args"),
                        dependencies = NA,
                        upgrade = "never",
                        keep_source = getOption("keep.source.pkgs")) {

  checkmate::assert_flag(document)
  checkmate::assert_flag(build_and_install)
  checkmate::assert_flag(restart_r_session)
  checkmate::assert_flag(use_rstudio_api,
                         null.ok = TRUE)
  checkmate::assert_flag(quiet)
  rlang::check_installed("devtools",
                         reason = pal::reason_pkg_required())
  rlang::check_installed("pkgload",
                         reason = pal::reason_pkg_required())

  if (is.null(use_rstudio_api) && nzchar(system.file(package = "rstudioapi")) && rstudioapi::isAvailable()) {
    use_rstudio_api <- TRUE
  } else {
    use_rstudio_api <- isTRUE(use_rstudio_api)
  }

  # convert `Rmd/*.Rmd` to `R/*.gen.R`
  purl_rmd(path = path,
           add_copyright_notice = add_copyright_notice,
           add_license_notice = add_license_notice,
           gen_pkgdown_ref = gen_pkgdown_ref,
           env = env)

  # build roxygen2 documentation
  if (document) {
    document_pkg(path = path,
                 roclets = roclets,
                 quiet = quiet)
  }

  # build and install package
  if (build_and_install) {

    if (use_rstudio_api) {
      install_pkg_rstudio_api(quiet = quiet)
    } else {
      install_pkg(path = path,
                  unload = FALSE,
                  quiet = quiet,
                  reload = TRUE,
                  args = args,
                  dependencies = dependencies,
                  upgrade = upgrade,
                  keep_source = keep_source)

      if (restart_r_session) {
        restart_r()
      }
    }
  }

  invisible(path)
}

load_pkg

#' Load R Markdown package
#'
#' @description
#' Executes the steps to load an R package written in R Markdown format in one go:
#'
#' 1. Purl all relevant `Rmd/*.Rmd` files to `R/*.gen.R` files using [purl_rmd()].
#' 2. Loads the package using [devtools::load_all()].
#'
#' @inheritParams purl_rmd
#' @inheritParams devtools::load_all
#'
#' @inherit purl_rmd return
#' @family high_lvl
#' @export
load_pkg <- function(path = ".",
                     add_copyright_notice = FALSE,
                     add_license_notice = FALSE,
                     gen_pkgdown_ref = FALSE,
                     reset = TRUE,
                     recompile = FALSE,
                     export_all = TRUE,
                     helpers = TRUE,
                     quiet = FALSE,
                     ...) {

  rlang::check_installed("devtools",
                         reason = pal::reason_pkg_required())

  # convert `Rmd/*.Rmd` to `R/*.gen.R`
  purl_rmd(path = path,
           add_copyright_notice = add_copyright_notice,
           add_license_notice = add_license_notice,
           gen_pkgdown_ref = gen_pkgdown_ref)

  # load pkg
  devtools::load_all(path = path,
                     reset = reset,
                     recompile = recompile,
                     export_all = export_all,
                     helpers = helpers,
                     quiet = quiet,
                     ...)
  invisible(path)
}

purl_rmd

TODO:

NOTES:

#' Purl `Rmd/*.Rmd` to `R/*.gen.R`
#'
#' This function strives to provide a standardized way to convert all relevant `.Rmd` files in the `Rmd/` subdirectory to bare `.R` files in the `R/`
#' subdirectory using [knitr::purl()]. It is mainly intended for authoring R packages in the [R Markdown file format](https://rmarkdown.rstudio.com/).
#'
#' The generated `.R` files will be named the same as the `.Rmd` files plus the suffix `.gen` to indicate the file was auto-generated. So the file
#' `Rmd/foo.Rmd` for example will be converted to `R/foo.gen.R`.
#'
#' The R Markdown file format allows you to intermingle code with related prose in [Markdown syntax](https://bookdown.org/yihui/rmarkdown/markdown-syntax.html)
#' optimized for human readability. This facilitates (best) practices which are commonly referred to as
#' [_literate programming_](https://en.wikipedia.org/wiki/Literate_programming).
#' 
#' In practice, the main advantage of writing R code in R Markdown is that you don't have to rely on
#' [`#` comments](https://cran.r-project.org/doc/manuals/r-release/R-lang.html#Comments) to explain, annotate or otherwise elaborate on your code. It also
#' allows you to easily compile your source code to beautifully looking HTML, PDF etc. files using [rmarkdown::render()].
#'
#' `r pkgsnip::md_snip("rstudio_addin")`
#' 
#' # Files excluded from purling
#' 
#' `purl_rmd()` does not generate an `.R` file for each and every R Markdown file in the `Rmd/` subdirectory. Two types of `.Rmd` files are excluded from
#' purling:
#' 
#' 1. Files having the suffix `.nopurl` in their name, e.g. `Rmd/playground.nopurl.Rmd`.
#' 2. Hidden files [as per Unix convention](https://en.wikipedia.org/wiki/Hidden_file_and_hidden_directory#Unix_and_Unix-like_environments) whose names start
#'    with a dot, e.g. `Rmd/.playground.Rmd`.
#' 
#' The above convention allows for easy exclusion of specific `.Rmd` files from purling. A common case for this are scripts that generate [package-internal
#' data](https://r-pkgs.org/data.html#sec-data-sysdata) from raw sources. Such a script could be stored as `Rmd/data.nopurl.Rmd`, so that no corresponding file
#' under `R/*.R` is generated. For the sake of clarity, it's generally advised to prefer the `.nopurl` suffix over hiding files.
#'
#' @param path Path to the root of the package directory.
#' @param add_copyright_notice `r funky_config$description[funky_config$key == "add_copyright_notice"]` A logical scalar. Only applies if `path` [is actually an
#'   R package directory][pal::is_pkg_dir].
#' @param add_license_notice `r funky_config$description[funky_config$key == "add_license_notice"]` A logical scalar. Only applies if `path` [is actually an R
#'   package directory][pal::is_pkg_dir].
#' @param gen_pkgdown_ref `r funky_config$description[funky_config$key == "gen_pkgdown_ref"]` A logical scalar. Only applies if `path` [is actually an R package
#'   directory][pal::is_pkg_dir], [pkgdown is set up][pal::is_pkgdown_dir] and a [main R Markdown file][main_rmd()] exists.
#' @param env Environment to evaluate R Markdown inline code expressions in when generating the pkgdown reference index. Only relevant if
#'   `gen_pkgdown_ref = TRUE`.
#'
#' @return `path`, invisibly.
#' @family high_lvl
#' @export
purl_rmd <- function(path = ".",
                     add_copyright_notice = funky::config_val("add_copyright_notice"),
                     add_license_notice = funky::config_val("add_license_notice"),
                     gen_pkgdown_ref = funky::config_val("gen_pkgdown_ref"),
                     env = parent.frame()) {

  checkmate::assert_flag(add_copyright_notice)
  checkmate::assert_flag(add_license_notice)
  checkmate::assert_flag(gen_pkgdown_ref)

  rmd_files <- rmd_files(path = path)

  if (length(rmd_files) > 0L) {

    cli::cli_progress_step(msg = "Purling {.file Rmd/*.Rmd} to {.file R/*.gen.R}")

    r_dir <- fs::path(path, "R/")
    if (!fs::dir_exists(r_dir)) fs::dir_create(r_dir)
    copyright_notice <- NULL
    license_notice <- NULL

    if (pal::is_pkg_dir(path)) {

      if (add_copyright_notice) copyright_notice <- assemble_copyright_notice(path = path)
      if (add_license_notice) license_notice <- assemble_license_notice(path = path)

      if (gen_pkgdown_ref && pal::is_pkgdown_dir(path)) {

        # determine pkgdown config file path
        pkgdown_config_file <-
          c("_pkgdown.yml",
            "_pkgdown.yaml",
            "pkgdown/_pkgdown.yml",
            "pkgdown/_pkgdown.yaml",
            "inst/_pkgdown.yml",
            "inst/_pkgdown.yaml") %>%
          fs::path(path, .) |>
          fs::file_exists() %>%
          magrittr::extract(which(.)[1L]) |>
          names()

        # determine main input file for pkgdown reference generation
        main_rmd_file <- main_rmd(path = path)
        gen_pkgdown_ref <- length(main_rmd_file) > 0L

      } else {
        gen_pkgdown_ref <- FALSE
      }

    } else {

      impossible_opts <- c("add_copyright_notice"[isTRUE(add_copyright_notice)],
                           "add_license_notice"[isTRUE(add_license_notice)])
      n_impossible_opts <- length(impossible_opts)

      if (n_impossible_opts) {
        cli::cli_abort(paste0("{.arg {impossible_opts[1L]}} ",
                              "and {.arg {impossible_opts[2L]}} "[n_impossible_opts > 1L],
                              "{qty(n_impossible_opts)} {?was/were both} set to {.val TRUE}, but {.file {path}} does not appear to be an R package ",
                              "directory \u2013 thus cannot extract the necessary information from the package's {.file DESCRIPTION} file."))
      }

      add_copyright_notice <- FALSE
      add_license_notice <- FALSE
      gen_pkgdown_ref <- FALSE
    }

    rmd_files %>% purrr::walk(process_rmd,
                              path_pkg = path,
                              copyright_notice = copyright_notice,
                              license_notice = license_notice)

    if (gen_pkgdown_ref) {

      ref <-
        main_rmd_file |>
        brio::read_file() |>
        gen_pkgdown_ref(env = env)

      if (length(ref$reference) > 0L) {

        yaml::read_yaml(file = pkgdown_config_file,
                        # avoid default YAML sequence simplification, cf. https://github.com/vubiostat/r-yaml/issues/69
                        handlers = list(seq = \(x) x)) |>
          purrr::discard_at(at = "reference") |>
          c(ref) |>
          yaml::write_yaml(file = pkgdown_config_file,
                           handlers = list(logical = yaml::verbatim_logical))
      }
    }

  } else {
    cli::cli_alert_warning("{.path {path}} does not appear to be an Rmd package directory. Nothing done.")
  }

  invisible(path)
}

lint_rmd

NOTES:

#' Lint R Markdown package
#'
#' This is a convenience wrapper around [lintr::lint_dir()] which is tailored to a typical R Markdown package. To use this function, the
#' [lintr](https://github.com/jimhester/lintr/#readme) package is required.
#'
#' To avoid unnecessary noise, all the the generated `R/*.gen.R` files as well as R Markdown vignettes under `vignettes/*.Rmd` are excluded from linting.
#'
#' `r pkgsnip::md_snip("rstudio_addin")`
#'
#' @inheritParams purl_rmd
#' @inheritParams lintr::lint_dir
#' @inheritParams lintr::lint
#'
#' @inherit lintr::lint_dir return
#' @seealso [`default_linters`][default_linters] and [default_exclusions()]
#' @family high_lvl
#' @export
lint_rmd <- function(path = ".",
                     linters = default_linters,
                     cache = FALSE,
                     relative_path = TRUE,
                     exclusions = default_exclusions(excl_vignettes = TRUE),
                     pattern = "\\.[Rr]([Mm][Dd])?$",
                     parse_settings = TRUE,
                     show_progress = NULL) {

  checkmate::assert_directory_exists(path,
                                     access = "r")
  rlang::check_installed("lintr",
                         reason = pal::reason_pkg_required())

  lintr::lint_dir(path = path,
                  linters = linters,
                  cache = cache,
                  relative_path = relative_path,
                  exclusions = exclusions,
                  pattern = pattern,
                  parse_settings = parse_settings,
                  show_progress = show_progress)
}

run_nopurl_rmd

#' Run `*.nopurl.Rmd` files
#'
#' @description
#' `r lifecycle::badge("experimental")`
#'
#' Executes `.Rmd` files which are supposed to contain code not included in the [source package](https://r-pkgs.org/structure.html#sec-source-package), i.e.
#' usually outsourced to separate `.Rmd` files with the [`.nopurl` suffix in their
#' filenames](https://pkgpurl.rpkg.dev/dev/reference/purl_rmd.html#-rmd-files-excluded-from-purling). Those files are typically used to generate package
#' data.
#'
#' If an error is encountered saying `internal error -3 in R_decompress1`, restart the R session and run again.
#'
#' @inheritParams purl_rmd
#' @inheritParams devtools::document
#' @inheritParams devtools::install
#' @param path_rmd Path(s) to the `.Rmd` files to be executed. A character vector.
#' @param env Environment to evaluate the `.Rmd` files in. If `NULL`, the [global environment][globalenv] is used.
#' @param document Whether or not to re-build the package documentation after the last `.Rmd` file is executed.
#' @param build_and_install Whether or not to build and install the package after each `.Rmd` file execution.
#' @param restart_r_session Whether or not to restart the R session after the last `.Rmd` file is executed. Highly recommended if `build_and_install = TRUE`,
#'   but only possible when R is run within RStudio.
#'
#' @return `path_rmd`, invisibly.
#' @family high_lvl
#' @export
run_nopurl_rmd <- function(path = ".",
                           path_rmd = fs::dir_ls(path = fs::path(path, "Rmd"),
                                                 recurse = TRUE,
                                                 type = "file",
                                                 glob = "*.nopurl.Rmd"),
                           env = NULL,
                           document = TRUE,
                           build_and_install = TRUE,
                           restart_r_session = TRUE,
                           quiet = TRUE,
                           roclets = NULL,
                           args = getOption("devtools.install.args"),
                           dependencies = NA,
                           upgrade = "never",
                           keep_source = getOption("keep.source.pkgs")) {

  checkmate::assert_directory_exists(path,
                                     access = "r")
  checkmate::assert_scalar(path)
  checkmate::assert_file_exists(path_rmd,
                                access = "r",
                                extension = c("Rmd", "qmd"))
  checkmate::assert_environment(env,
                                null.ok = TRUE)
  checkmate::assert_flag(document)
  checkmate::assert_flag(build_and_install)
  checkmate::assert_flag(restart_r_session)

  path_tmp_purled <- purrr::map_chr(path_rmd,
                                    \(x) knitr::purl(input = x,
                                                     output = fs::file_temp(pattern = fs::path_ext_remove(fs::path_file(x)),
                                                                            ext = "R"),
                                                     quiet = quiet))
  for (i in seq_along(path_tmp_purled)) {

    cli::cli_progress_step(msg = "{.strong Executing file {.file {path_tmp_purled[i]}}}")

    # nolint start: undesirable_function_linter
    source(file = path_tmp_purled[i],
           local = ifelse(is.null(env),
                          FALSE,
                          env),
           echo = FALSE,
           encoding = "UTF-8")
    # nolint end

    cli::cli_progress_done()

    is_last_path <- i == length(path_tmp_purled)

    # build roxygen2 documentation
    if (document && is_last_path) {
      document_pkg(path = path,
                   roclets = roclets,
                   quiet = quiet)
    }

    if (build_and_install) {
      install_pkg(path = path,
                  unload = TRUE,
                  quiet = quiet,
                  reload = TRUE,
                  quick = !document || !is_last_path,
                  build = TRUE,
                  args = args,
                  dependencies = dependencies,
                  upgrade = upgrade,
                  keep_source = keep_source)
    }
  }

  fs::file_delete(path_tmp_purled)

  if (restart_r_session) {
    restart_r()
  }

  invisible(path_rmd)
}

Low-level functions

gen_pkgdown_ref

NOTES:

#' Generate pkgdown reference index
#'
#' @description
#' Generates the [pkgdown reference index](https://pkgdown.r-lib.org/reference/build_reference.html#reference-index) based on the heading hierarchy structure of
#' a package's main `.Rmd` file.
#'
#' @details
#' Basically, all elements of the pkgdown reference index except `desc` keys are derived from `rmd`'s [Markdown
#' **headings**](https://pandoc.org/MANUAL.html#headings) and their **hierarchy**. The `desc` keys, however, are assembled from the paragraph(s) below some
#' specially named headings. See below for details.
#'
#' # Special headings
#' 
#' Headings that exactly match certain strings (case-insensitive, but without any inline formatting or additional text) are treated specially. Here's an
#' overview:
#'
#' ```r
#' data_special_headings %>%
#'   dplyr::select(-type) %>%
#'   dplyr::rename(Headings = heading_texts,
#'                 Meaning = meaning) %>%
#'   pal::pipe_table()
#' ```
#'
#' # Parsing rules
#'
#' To be able to unambiguously map an `.Rmd` file's heading hierarchy to the pkgdown reference index, some parsing rules are necessary. Attention must be paid
#' to the fact that, while (R) Markdown supports up to six heading levels (corresponding to HTML's `<h1>`–`<h6>` tags), the pkgdown reference index only
#' supports up to two (`title` and `subtitle`).
#' 
#' The following rules define how the reference index is generated:
#' 
#' 1. Headings below a heading named
#'    `r data_special_headings |> dplyr::filter(type == "ignore_content") %$% heading_texts |> unlist() |> pal::enum_str(wrap = "*", sep2 = " or ")`
#'    (case-insensitive, but without any inline formatting) are simply ignored when generating the reference index.
#' 2. Every heading that is a) inline-formatted as [verbatim](https://pandoc.org/MANUAL.html#verbatim) and b) doesn't contain any whitespace characters is
#'    considered to be the name of a help topic (usually the name of a function or dataset) to be included in the reference index. This maps to the `contents`
#'    key of the reference index' YAML.
#' 3. Non-help-topic headings above help topic headings are used as reference index (sub)titles as far as hierarchical nesting allows. More precisely,
#'    non-help-topic headings *of the highest two levels* above the help topic heading are used as title and subtitle, the rest of the headings above the help
#'    topic heading is ignored – and a heading named *EXPORTED* (case-insensitive, but without any inline formatting) is always ignored regardless of its
#'    level. This maps to the `title` and `subtitle` keys of the reference index' YAML.
#' 4. Paragraph(s) below a heading named *DESCRIPTION* (case-insensitive, but without any inline formatting), that in turn is hierarchically below a title or
#'    subtitle heading, are used as the description for the respective (sub)title. This maps to the `desc` key of the reference index' YAML.
#'
#' # Parsing example
#'
#' An example might better explain how the parsing rules work than a thousand words, so here's a simplified one.
#' 
#' When fed to `gen_pkgdown_ref()`, the following R Markdown content...
#'
#' ```r
#' brio::read_lines("snippets/simplified_example.Rmd") %>%
#'   c(paste(pal::as_str(rep("`", 4L)), "rmd"), ., pal::as_str(rep("`", 4L))) %>%
#'   pal::cat_lines()
#' ```
#'
#' ...yields this pkgdown index (converted [to YAML][yaml::as.yaml()]):
#'
#' ```r
#' brio::read_file("snippets/simplified_example.Rmd") |>
#'   gen_pkgdown_ref() |>
#'   yaml::as.yaml() |>
#'   cat()
#' ```
#'
#' # Inline R code
#' 
#' R Markdown [inline code](https://rmarkdown.rstudio.com/lesson-4.html) is fully supported in headings and descriptions, except for the above mentioned special
#' headings (otherwise, they're not recognized as special headings anymore).
#'
#' @param rmd The (R) Markdown file content as a character scalar.
#' @param env Environment to evaluate R Markdown inline code expressions in.
#'
#' @return A list.
#' @family low_lvl
#' @export
#'
#' @examples
#' if (pal::is_pkg_installed("tinkr")) {
#'   yay::gh_text_file(path = "Rmd/pal.Rmd",
#'                     owner = "salim-b",
#'                     name = "pal") |>
#'     pkgpurl::gen_pkgdown_ref() |>
#'     yaml::as.yaml() |>
#'     cat()
#' }
gen_pkgdown_ref <- function(rmd,
                            env = parent.frame()) {

  checkmate::assert_string(rmd)
  checkmate::assert_environment(env)
  rlang::check_installed("xml2",
                         reason = pal::reason_pkg_required())
  rmd_xml <-
    rmd |>
    pal::md_to_xml(strip_xml_ns = FALSE) |>
    xml2::xml_contents()

  # assemble necessary Markdown heading hierarchy information (one row for each node of `rmd_xml`)
  hierarchy <-
    tibble::tibble(subnode_ix = pal::md_xml_subnode_ix(xml = rmd_xml),
                   # is _relevant_ heading
                   is_heading = lengths(subnode_ix) > 0L) |>
    tibble::rowid_to_column(var = "i") %>%
    # add variables that are relevant for heading nodes only
    dplyr::left_join(y = tibble::tibble(i = .$i[.$is_heading],
                                        heading_lvl =
                                          rmd_xml[i] |>
                                          purrr::map_chr(xml2::xml_attr,
                                                         attr = "level") |>
                                          as.integer(),
                                        is_help_topic =
                                          xml2::xml_length(rmd_xml[i]) == 1L
                                        & rmd_xml[i] |>
                                          xml2::xml_child(search = "d1:code") |>
                                          xml2::xml_text() |>
                                          stringr::str_detect(pattern = "^[^\\s`]+$"),
                                        is_description_heading =
                                          xml2::xml_length(rmd_xml[i]) == 1L
                                        & rmd_xml[i] |>
                                          xml2::xml_child(search = "d1:text") |>
                                          xml2::xml_text(trim = TRUE) |>
                                          stringr::str_detect(pattern = paste0("^(?i)", pal::fuse_regex(heading_texts_description), "$")),
                                        is_ignored =
                                          xml2::xml_length(rmd_xml[i]) == 1L
                                        & rmd_xml[i] |>
                                          xml2::xml_child(search = "d1:text") |>
                                          xml2::xml_text(trim = TRUE) |>
                                          stringr::str_detect(pattern = paste0("^(?i)", pal::fuse_regex(heading_texts_ignore_content), "$")),
                                        is_heading_ignored =
                                          xml2::xml_length(rmd_xml[i]) == 1L
                                        & rmd_xml[i] |>
                                          xml2::xml_child(search = "d1:text") |>
                                          xml2::xml_text(trim = TRUE) |>
                                          stringr::str_detect(pattern = paste0("^(?i)", pal::fuse_regex(heading_texts_ignore_title), "$"))),
                     by = "i") |>
    tidyr::replace_na(replace = list(is_help_topic = FALSE,
                                     is_description_heading = FALSE))
  ## complete `is_ignored`
  for (i in hierarchy$i[hierarchy$is_ignored
                        & !is.na(hierarchy$is_ignored)
                        & lengths(hierarchy$subnode_ix) > 0L]) {

    hierarchy$is_ignored[unlist(hierarchy$subnode_ix[i])] <- TRUE
  }
  hierarchy %<>% tidyr::replace_na(replace = list(is_ignored = FALSE))
  ## exclude ignored help topics
  hierarchy$is_help_topic[hierarchy$is_ignored] <- FALSE

  # warn if no valid help topic nodes are present
  if (any(hierarchy$is_help_topic)) {

    # initialize reference index data
    data_ref_i <- tibble::tibble(i_title = integer(),
                                 i_subtitle = integer(),
                                 content = character())

    # iterate over all help topic nodes, get their names as well as their (sub)title node indices
    for (i in hierarchy$i[hierarchy$is_help_topic]) {

      hierarchy_subset <-
        hierarchy |>
        dplyr::filter(!is_help_topic
                      & !is_heading_ignored
                      & purrr::map_lgl(hierarchy$subnode_ix,
                                       \(x) !!i %in% x))

      title_lvl <- pal::safe_min(hierarchy_subset$heading_lvl)

      data_ref_i %<>% tibble::add_row(i_title =
                                        hierarchy_subset |>
                                        dplyr::filter(heading_lvl == title_lvl) %$%
                                        ifelse(length(i) > 0L,
                                               as.integer(i),
                                               NA_integer_),
                                      i_subtitle =
                                        hierarchy_subset |>
                                        dplyr::filter(heading_lvl > title_lvl) |>
                                        dplyr::filter(heading_lvl == pal::safe_min(heading_lvl)) %$%
                                        ifelse(length(i) > 0L,
                                               as.integer(i),
                                               NA_integer_),
                                      content = xml2::xml_text(rmd_xml[i]))
    }

    # add actual (sub)titles and their descriptions
    data_ref_i %<>%
      dplyr::left_join(y = tibble::tibble(i_title =
                                            .$i_title |>
                                            unique() |>
                                            setdiff(NA_integer_),
                                          title =
                                            rmd_xml[i_title] |>
                                            purrr::map_chr(pal::xml_to_md) |>
                                            extract_md_heading_content(env = env),
                                          title_description =
                                            hierarchy$subnode_ix[i_title] |>
                                            purrr::map2_chr(.y = i_title,
                                                            .f = \(x, y) {

                                                              hierarchy[x, ] |>
                                                                dplyr::filter(is_description_heading
                                                                              & heading_lvl == hierarchy$heading_lvl[y] + 1L) %$%
                                                                subnode_ix |>
                                                                purrr::list_c(ptype = integer()) %>%
                                                                magrittr::extract(rmd_xml, .) |>
                                                                purrr::map_chr(pal::xml_to_md) |>
                                                                knitr::knit(text = _,
                                                                            quiet = TRUE,
                                                                            envir = env) |>
                                                                stringr::str_trim() |>
                                                                pal::when(length(.) > 0L ~ paste(., collapse = "\n\n"),
                                                                          ~ NA_character_)
                                                            })),
                       by = "i_title") %>%
      dplyr::left_join(y = tibble::tibble(i_subtitle =
                                            .$i_subtitle |>
                                            unique() |>
                                            setdiff(NA_integer_),
                                          subtitle =
                                            rmd_xml[i_subtitle] |>
                                            purrr::map_chr(pal::xml_to_md) |>
                                            extract_md_heading_content(env = env),
                                          subtitle_description =
                                            hierarchy$subnode_ix[i_subtitle] |>
                                            purrr::map2_chr(.y = i_subtitle,
                                                            .f = \(x, y) {

                                                              hierarchy[x, ] |>
                                                                dplyr::filter(is_description_heading
                                                                              & heading_lvl == hierarchy$heading_lvl[y] + 1L) %$%
                                                                subnode_ix |>
                                                                purrr::list_c(ptype = integer()) %>%
                                                                magrittr::extract(rmd_xml, .) |>
                                                                purrr::map_chr(pal::xml_to_md) |>
                                                                knitr::knit(text = _,
                                                                            quiet = TRUE,
                                                                            envir = env) |>
                                                                stringr::str_trim() |>
                                                                pal::when(length(.) > 0L ~ paste(., collapse = "\n\n"),
                                                                          ~ NA_character_)
                                                            })),
                       by = "i_subtitle")

    # assemble result list that can easily be converted to YAML using `yaml::as.yaml()`
    result <-
      data_ref_i |>
      dplyr::group_by(i_title) |>
      dplyr::group_map(\(data_title, key) {

        title <- data_title$title[1L]
        title_description <- data_title$title_description[1L]
        item_title <- list()

        if (!is.na(title)) {
          item_title %<>% c(list(title = title))
        }
        if (!is.na(title_description)) {
          item_title %<>% c(list(desc = title_description))
        }

        items_subtitle <-
          data_title |>
          dplyr::group_by(i_subtitle) |>
          dplyr::group_map(\(data_subtitle, key) {

            subtitle <- data_subtitle$subtitle[1L]
            subtitle_description <- data_subtitle$subtitle_description[1L]
            contents <- data_subtitle$content
            item_subtitle <- list()

            if (!is.na(subtitle)) {
              item_subtitle %<>% c(list(subtitle = subtitle))
            }
            if (!is.na(subtitle_description)) {
              item_subtitle %<>% c(list(desc = subtitle_description))
            }

            c(list(item_subtitle),
              list(list(contents = contents)))
          }) |>
          purrr::list_flatten() |>
          purrr::compact() |>
          # move subtitle-less item to the front (the `NA` group is always processed last in `group_map()`)
          pal::when(anyNA(data_title$subtitle) ~ .[c(length(.), seq_len(length(.) - 1L))],
                    ~ .)

        c(list(item_title),
          items_subtitle)
      }) |>
      purrr::list_flatten() |>
      purrr::compact() |>
      # move title-less item to the front (the `NA` group is always processed last in `group_map()`)
      pal::when(anyNA(data_ref_i$title) ~ .[c(length(.), seq_len(length(.) - 1L))],
                ~ .)
  } else {

    cli::cli_alert_warning("Unable to generate pkgdown reference index. No valid help topic headings found in main {.file .Rmd} file.")
    result <- list()
  }

  list(reference = result)
}

Miscellaneous

default_exclusions

#' pkgpurl's default lintr exclusions
#'
#' Opinionated set of files and folders to be excluded from linting, relative to the package path. To be used with [lint_rmd()], [lintr::lint_dir()] or
#' [lintr::lint_package()].
#'
#' @param excl_vignettes Whether or not to exclude all `.Rmd` files under `vignettes/`. A logical scalar.
#'
#' @return A named list of [lintr::linters].
#' @seealso [`default_linters`][default_linters] and [lint_rmd()]
#' @export
#'
#' @examples
#' pkgpurl::default_exclusions()
default_exclusions <- function(excl_vignettes = TRUE) {

  checkmate::assert_flag(excl_vignettes)

  c("docs",
    "input",
    "output",
    "pkgdown",
    "renv",
    "packrat",
    "tests",
    list.files(path = "R",
               recursive = TRUE,
               full.names = TRUE,
               pattern = "\\.gen\\.R$"),
    list.files(path = "Rmd",
               recursive = TRUE,
               full.names = TRUE,
               pattern = "\\.nopurl\\.Rmd$"),
    if (excl_vignettes) list.files(path = "vignettes",
                                   recursive = TRUE,
                                   full.names = TRUE,
                                   pattern = "\\.Rmd$"),
    "README.Rmd")
}

default_linters

#' pkgpurl's default linters
#'
#' Opinionated set of [linters][lintr::linters]. Built from [lintr::linters_with_defaults()] with lots of customizations. See the [relevant source
#' code](https://gitlab.com/rpkg.dev/pkgpurl/-/blob/master/Rmd/sysdata.nopurl.Rmd#default_linters) for details.
#'
#' @format A named list of [lintr::linters].
#' @seealso [default_exclusions()] and [lint_rmd()]
#' @export
#'
#' @examples
#' names(pkgpurl::default_linters)
"default_linters"

funky_config

#' `r pkgsnip::title_lbl("funky_config", pkg = "pkgpurl")`
#'
#' `r pkgsnip::description_lbl("funky_config", pkg = "pkgpurl")`
#'
#' @format `r pkgsnip::return_lbl("tibble_cols", cols = colnames(funky_config))`
#' @export
#'
#' @examples
#' pkgpurl::funky_config
"funky_config"


salim-b/pkgpurl documentation built on June 2, 2025, 8:26 a.m.