From 93273932ae9071ff46b124d9c812d9fdc6a31bc9 Mon Sep 17 00:00:00 2001 From: Yihui Xie Date: Thu, 12 Dec 2024 13:44:41 -0600 Subject: [PATCH] close #54: add support for running examples in pkg_manual() --- DESCRIPTION | 2 +- NEWS.md | 2 ++ R/fuse.R | 2 +- R/mark.R | 5 +-- R/package.R | 67 +++++++++++++++++++++++++++++++++++--- inst/resources/default.css | 18 +++++----- inst/resources/snap.css | 4 --- man/pkg_desc.Rd | 9 +++-- site/manual.Rmd | 4 +-- 9 files changed, 87 insertions(+), 26 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 6a22753..2abcd35 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: litedown Type: Package Title: A Lightweight Version of R Markdown -Version: 0.4.9 +Version: 0.4.10 Authors@R: c( person("Yihui", "Xie", role = c("aut", "cre"), email = "xie@yihui.name", comment = c(ORCID = "0000-0003-0645-5666", URL = "https://yihui.org")), person() diff --git a/NEWS.md b/NEWS.md index 5ed347b..632294c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,6 +16,8 @@ - Provided templates and a Github action `yihui/litedown/site` to build package websites. See https://yihui.org/litedown/#sec:pkg-site for details. +- Added an argument `examples` to `pkg_manual()` to run examples and show their output (thanks, @TimTaylor, #54). + - Fixed a bug that cross-references to other chapters of a book could not be resolved when previewing a single chapter. - Fixed a bug that the file navigation by line numbers on code blocks stopped working in `litedown::roam()` due to yihui/lite.js@5e06d19. diff --git a/R/fuse.R b/R/fuse.R index 007a130..688e9bf 100644 --- a/R/fuse.R +++ b/R/fuse.R @@ -329,7 +329,7 @@ run_range = function(x, extend = NULL) { i1 = i1[c(TRUE, k)]; i2 = i2[c(k, TRUE)] } } - rbind(i1, i2) + matrix(as.integer(c(i1, i2)), nrow = 2, byrow = TRUE) } # convert knitr's inline `r code` to litedown's `{r} code` diff --git a/R/mark.R b/R/mark.R index 7f7fbdf..6b4e043 100644 --- a/R/mark.R +++ b/R/mark.R @@ -288,8 +288,9 @@ mark = function(input, output = NULL, text = NULL, options = NULL, meta = list() # build table of contents ret = add_toc(ret, options) # add js/css for math - if (!has_math) has_math = length(ret) && - grepl('$$

', ret, fixed = TRUE) # math may be from pkg_manual()'s HTML + if (!has_math) has_math = length(ret) && ( + grepl('$$

', ret, fixed = TRUE) || grepl('\\)', ret, fixed = TRUE) + ) # math may be from pkg_manual()'s HTML is_katex = TRUE if (has_math && length(js_math <- js_options(options[['js_math']], 'katex'))) { is_katex = js_math$package == 'katex' diff --git a/R/package.R b/R/package.R index 2530355..d4e9200 100644 --- a/R/package.R +++ b/R/package.R @@ -115,10 +115,11 @@ vig_filter = function(ifile, encoding) { #' `pkg_desc()` returns an HTML table containing the package metadata. #' @export #' @examples +#' \dontrun{ #' litedown::pkg_desc() #' litedown::pkg_news() #' litedown::pkg_citation() -#' litedown::pkg_manual() +#' } pkg_desc = function(name = detect_pkg()) { fields = c( 'Title', 'Version', 'Description', 'Depends', 'Imports', 'Suggests', @@ -271,11 +272,15 @@ tweak_citation = function(x) { #' @param overview Whether to include the package overview page, i.e., the #' `{name}-package.Rd` page. +#' @param examples A list of arguments to be passed to [xfun::record()] to run +#' examples each help page, e.g., `list(dev = 'svg', dev.args = list(height = +#' 6))`. If not a list (e.g., `FALSE`), examples will not be run. #' @return `pkg_manual()` returns all manual pages of the package in HTML. #' @rdname pkg_desc #' @export pkg_manual = function( - name = detect_pkg(), toc = TRUE, number_sections = TRUE, overview = TRUE + name = detect_pkg(), toc = TRUE, number_sections = TRUE, overview = TRUE, + examples = list() ) { links = tools::findHTMLlinks('') # resolve internal links (will assign IDs of the form sec:man-ID to all h2) @@ -306,10 +311,17 @@ pkg_manual = function( stop(e) }, finally = close(con)) # extract body, which may end at (R 4.4.x) or (R 4.3.x) - txt = gsub('.*?(].*)(|\\s*).*', '\\1', one_string(txt)) + txt = gsub('(?s).*?(?=|\\s*).*', '', txt) # free math from txt = gsub(r2, '

$$\\1$$

', txt) - txt = gsub(r1, '\\\\(\\1\\\\)', txt) + txt = gsub(r1, '\\\\(\\1\\\\)', txt) + # run examples + if (is.list(examples)) { + xfun::pkg_attach(name) + default = list(print = NA, dev.path = 'manual/', dev.args = list(width = 9, height = 7)) + txt = run_examples(txt, merge_list(default, examples), sans_ext(i)) + } # remove existing ID and class for (a in c('id', 'class')) txt = gsub(sprintf('(]*?) %s="[^"]+"', a), '\\1', txt) if (cl != '') txt = sub('', '', res, fixed = TRUE) + style = gen_tag(jsd_resolve(jsdelivr('css/manual.min.css'))) + new_asis(c(style, toc, res)) +} - new_asis(c(toc, res)) +run_examples = function(html, config, path) { + config$dev.path = path = paste0(config$dev.path, path) + on.exit(xfun::del_empty_dir(dirname(path)), add = TRUE) + r = '(?s).*?
]*>(?s)(.+?)
' + match_replace(html, paste0('(?<=

Examples

)', r), function(x) { + code = gsub(r, '\\1', x, perl = TRUE) + code = restore_html(str_trim(code)) + nr1 = 'if (FALSE) { ## Not run' + nr2 = '} ## Not run' + code = gsub('\n?## Not run:\\s*?\n', paste0('\n', nr1, '\n'), code) + code = gsub('\n+## End[(]Not run[)]\n*', paste0('\n', nr2, '\n'), code) + res = do.call(xfun::record, merge_list(config, list(code = code, envir = globalenv()))) + idx = seq_along(res); cls = class(res) + for (i in idx) { + ri = res[[i]]; ci = class(ri) + # disable asis output since it may contain raw HTML + if ('record_asis' %in% ci) class(res[[i]]) = 'record_output' + # split the dontrun block + if ('record_source' %in% ci && !any(is.na(nr <- match(c(nr1, nr2), ri)))) { + i1 = nr[1]; i2 = nr[2] + new_block = function(i, ...) { + b = trim_blank(one_string(ri[i])) + if (xfun::is_blank(b)) b = character() + list(structure(b, class = c(ci, ...))) + } + if (i1 > 1) { + res = c(res, new_block((i1 + 1):(i2 - 1), 'fade')) + idx = c(idx, i) + } + n = length(ri) + if (i2 < n) { + res = c(res, new_block((i2 + 1):n)) + idx = c(idx, i) + } + res[i] = if (i1 > 1) new_block(1:(i1 - 1)) else + new_block((i1 + 1):(i2 - 1), 'fade') + } + } + res = res[order(idx)] + class(res) = cls + res = one_string(c('', format(res, 'markdown'))) + res + }) } detect_pkg = function(error = TRUE) { diff --git a/inst/resources/default.css b/inst/resources/default.css index 72f6527..29e30f4 100644 --- a/inst/resources/default.css +++ b/inst/resources/default.css @@ -16,9 +16,11 @@ pre code { display: block; padding: 1em; overflow-x: auto; } code { font-family: 'DejaVu Sans Mono', 'Droid Sans Mono', 'Lucida Console', Consolas, Monaco, monospace; } :not(pre) > code, code[class], .box > .caption { background-color: #f8f8f8; } pre > code:is(:not([class]), .language-plain, .language-none), .box { background-color: inherit; border: 1px solid #eee; } -pre > .message { border-color: #9eeaf9; } -pre > .warning { background: #fff3cd; border-color: #fff3cd; } -pre > .error { background: #f8d7da; border-color: #f8d7da; } +pre > code { + &.message { border-color: #9eeaf9; } + &.warning { background: #fff3cd; border-color: #fff3cd; } + &.error { background: #f8d7da; border-color: #f8d7da; } +} .fenced-chunk { border-left: 1px solid #666; } .code-fence { opacity: .4; @@ -36,10 +38,6 @@ table { th, td { padding: 5px; font-variant-numeric: tabular-nums; } thead, tfoot, tr:nth-child(even) { background: whitesmoke; } } -.table-full { - width: 100%; - td { vertical-align: baseline; } -} blockquote { color: #666; margin: 0; @@ -81,11 +79,15 @@ section.footnotes { margin-top: 2em; &::before { content: ""; display: block; max-width: 20em; } } +.fade { + background: repeating-linear-gradient(135deg, white, white 30px, #ddd 32px, #ddd 32px); + opacity: 0.6; +} @media print { body { max-width: 100%; } tr, img { page-break-inside: avoid; } } @media only screen and (min-width: 992px) { - pre:has(.line-numbers):not(:hover) { white-space: pre; } + body:not(.pagesjs) pre:has(.line-numbers):not(:hover) { white-space: pre; } } diff --git a/inst/resources/snap.css b/inst/resources/snap.css index 18a6bab..20c206a 100644 --- a/inst/resources/snap.css +++ b/inst/resources/snap.css @@ -35,10 +35,6 @@ a { color: #eb4a47; } background-color: #eee; filter: invert(1); } -.fade { - background: repeating-linear-gradient(135deg, white, white 30px, #ddd 32px, #ddd 32px); - opacity: 0.6; -} .center { text-align: center; } .slide-container h2 .section-number { display: inline-block; diff --git a/man/pkg_desc.Rd b/man/pkg_desc.Rd index fba5c9c..1e04a92 100644 --- a/man/pkg_desc.Rd +++ b/man/pkg_desc.Rd @@ -33,7 +33,8 @@ pkg_manual( name = detect_pkg(), toc = TRUE, number_sections = TRUE, - overview = TRUE + overview = TRUE, + examples = list() ) } \arguments{ @@ -69,6 +70,9 @@ will be filled out with the file paths via \code{sprintf()}), e.g., \item{overview}{Whether to include the package overview page, i.e., the \code{{name}-package.Rd} page.} + +\item{examples}{A list of arguments to be passed to \code{\link[xfun:record]{xfun::record()}} to run +examples each help page, e.g., \code{list(dev = 'svg', dev.args = list(height = 6))}. If not a list (e.g., \code{FALSE}), examples will not be run.} } \value{ A character vector (HTML or Markdown) that will be printed as is @@ -92,8 +96,9 @@ put together as the full package documentation like a \pkg{pkgdown} website. These functions can be called inside any R Markdown document. } \examples{ +\dontrun{ litedown::pkg_desc() litedown::pkg_news() litedown::pkg_citation() -litedown::pkg_manual() +} } diff --git a/site/manual.Rmd b/site/manual.Rmd index 0d0adb6..c786ca9 100644 --- a/site/manual.Rmd +++ b/site/manual.Rmd @@ -9,6 +9,4 @@ h2 { } ``` -```{r, echo = FALSE} -litedown::pkg_manual() -``` +`{r} litedown::pkg_manual()`