From f442634ee84d7eb3d364f4d067d90023e0ff2329 Mon Sep 17 00:00:00 2001 From: Yihui Xie Date: Tue, 1 Oct 2024 01:41:42 -0500 Subject: [PATCH] close #24: add preliminary support for building the full package documentation as a single-file book also close #22 --- DESCRIPTION | 2 +- NAMESPACE | 4 + NEWS.md | 2 + R/package.R | 183 +++++++++++++++++++++++++++++++++++++ R/preview.R | 1 + docs/06-site.Rmd | 38 ++++++++ inst/resources/default.css | 4 + man/pkg_desc.Rd | 53 +++++++++++ 8 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 man/pkg_desc.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 9065c6f..6369609 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: litedown Type: Package Title: A Lightweight Version of R Markdown -Version: 0.2.3 +Version: 0.2.4 Authors@R: c( person("Yihui", "Xie", role = c("aut", "cre"), email = "xie@yihui.name", comment = c(ORCID = "0000-0003-0645-5666")), person() diff --git a/NAMESPACE b/NAMESPACE index 7fe3d02..28407f1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -16,6 +16,10 @@ export(html_format) export(latex_format) export(mark) export(markdown_options) +export(pkg_citation) +export(pkg_desc) +export(pkg_manual) +export(pkg_news) export(reactor) export(roam) export(sieve) diff --git a/NEWS.md b/NEWS.md index 636888d..f620a8c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # CHANGES IN litedown VERSION 0.3 +- Added helper functions `pkg_desc()`, `pkg_news()`, `pkg_citation()`, and `pkg_manual()` to get various package information for building the full package documentation as a single-file book (thanks, @jangorecki @llrs #24, @TimTaylor #22). + - Added back/forward/refresh/print buttons to the toolbar in the `litedown::roam()` preview interface. - Set `options(bitmapType = 'cairo')` in `fuse()` if `capabilities('cairo')` is TRUE, which will generate smaller bitmap plot files (e.g., `.png`) than using `quartz` or `Xlib`, and is also a safer option for `fuse()` to be executed in parallel (rstudio/rmarkdown#2561). diff --git a/R/package.R b/R/package.R index d3547ae..8f66812 100644 --- a/R/package.R +++ b/R/package.R @@ -100,3 +100,186 @@ vig_filter = function(ifile, encoding) { }) structure(split_lines(unlist(res)), control = '-H -t') } + +#' Get the package description, news, citation, and manual pages +#' +#' Helper functions to retrieve various types of package information that can be +#' put together as the full package documentation like a \pkg{pkgdown} website. +#' These functions can be called inside any R Markdown document. +#' @param name The package name (by default, it is automatically detected from +#' the `DESCRIPTION` file if it exists in the current working directory or +#' upper-level directories). +#' @return A character vector (HTML or Markdown) that will be printed as is +#' inside a code chunk of an R Markdown document. +#' +#' `pkg_desc()` returns an HTML table containing the package metadata. +#' @export +#' @examples +#' litedown::pkg_desc() +#' litedown::pkg_news() +#' litedown::pkg_citation() +#' litedown::pkg_manual() +pkg_desc = function(name = detect_pkg()) { + d = packageDescription(name, fields = c( + 'Title', 'Version', 'Description', 'Depends', 'Imports', 'Suggests', + 'License', 'URL', 'BugReports', 'VignetteBuilder', 'Authors@R', 'Author' + )) + # remove single quotes on words (which are unnecessary IMO) + for (i in c('Title', 'Description')) d[[i]] = sans_sq(d[[i]]) + # format authors + if (is.na(d[['Author']])) d$Author = one_string(by = ',', format( + eval(xfun::parse_only(d[['Authors@R']])), include = c('given', 'family', 'role') + )) + d[['Authors@R']] = NULL + # convert URLs to , and escape HTML in other fields + for (i in names(d)) d[[i]] = if (!is.na(d[[i]])) { + if (i %in% c('URL', 'BugReports')) { + sans_p(commonmark::markdown_html(d[[i]], extensions = 'autolink')) + } else xfun:::escape_html(d[[i]]) + } + d = unlist(d) + res = paste0( + '\n', paste0( + '', paste0('\n'), + paste0('\n'), '\n', collapse = '\n' + ), '\n
', names(d), '', d, '
' + ) + new_asis(res) +} + +#' @param path Path to the `NEWS.md` file. If empty, [news()] will be called to +#' retrieve the news entries. +#' @param recent The number of recent versions to show. By default, only the +#' latest version's news entries are retrieved. To show the full news, set +#' `recent = 0`. +#' @return `pkg_news()` returns the news entries. +#' @rdname pkg_desc +#' @export +pkg_news = function(name = detect_pkg(), path = detect_news(name), recent = 1, ...) { + if (path == '') { + db = news(package = name, ...) + if (recent > 0) db = head(db, recent) + res = NULL + for (v in unique(db$Version)) { + df = db[db$Version == v, ] + res = c( + res, paste('## Changes in version', v, '{-}'), '', + if (all(df$Category == '')) paste0(df$HTML, '\n') else paste0( + '### ', df$Category, '{-}\n\n', df$HTML, '\n\n' + ), '' + ) + } + } else { + res = read_utf8(path) + if (recent > 0 && length(h <- grep('^# ', res)) >= 2) + res = res[h[1]:(h[1 + recent] - 1)] + # lower heading levels: # -> ##, ## -> ###, etc, and unnumber them + res = sub('^(## .+)', '#\\1 {-}', res) + res = sub('^(# .+)', '#\\1 {-}', res) + } + new_asis(res) +} + +#' @return `pkg_citation()` returns the package citation in both the plain-text +#' and BibTeX formats. +#' @rdname pkg_desc +#' @export +pkg_citation = function(name = detect_pkg()) { + res = uapply(citation(name), function(x) { + x = tweak_citation(x) + unname(c(format(x, bibtex = FALSE), fenced_block(toBibtex(x), 'latex'))) + }) + new_asis(res) +} + +# dirty hack to add year if missing, and remove header +tweak_citation = function(x) { + cls = class(x) + x = unclass(x) + attr(x[[1]], 'header') = attr(x, 'package') = NULL + if (is.null(x[[1]]$year)) x[[1]]$year = format(Sys.Date(), '%Y') + class(x) = cls + x +} + +#' @return `pkg_manual()` returns all manual pages of the package in HTML. +#' @rdname pkg_desc +#' @export +pkg_manual = function(name = detect_pkg()) { + # all help pages on one HTML page + res = '' + con = textConnection('res', 'w', local = TRUE, encoding = 'UTF-8') + tools::pkg2HTML(name, hooks = list(pkg_href = function(pkg) { + path = if (pkg %in% xfun::base_pkgs()) 'r/%s/' else 'cran/%s/man/' + sprintf(paste0('https://rdrr.io/', path), pkg) + }), out = con, include_description = FALSE) + close(con) + res = gsub(" (id|class)='([^']+)'", ' \\1="\\2"', res) # ' -> " + + # extract topic names and put them in the beginning (like a TOC) + env = asNamespace(name) + r1 = '^(

]+>)(.*?

)' + r2 = '(?<=%s
', a, fn) + }) + fn = sub('.*(.+).*', '\\1', toc) + toc = toc[order(fn)]; fn = fn[order(fn)] + g = toupper(substr(fn, 1, 1)) + g[!g %in% LETTERS] = 'misc' + toc = split(toc, g) # group by first character + toc = unlist(mapply(function(x, g) { + c('

', sprintf('-- %s --', g), x, '

') + }, toc, names(toc))) + + res = one_string(res) + res = gsub('