diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index 3c0da1c97..d3660f3c1 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -21,6 +21,9 @@ jobs: with: use-public-rspm: true + # Needs updated pandoc for syntax highlighting tests + - uses: r-lib/actions/setup-pandoc@v1 + - uses: r-lib/actions/setup-r-dependencies@v1 with: extra-packages: covr diff --git a/NEWS.md b/NEWS.md index f84b6b58c..6e54c1481 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # pkgdown (development version) +* Code blocks in reference topics now get syntax highlighting according + to their declared language information (e.g. `yaml`), if the documentation + was built with roxygen2 7.1.2 or later (#1690, #1692). + * `build_articles()` and `build_home()` now warn if you have images that won't rendered on the website because they're in unsupported directories (#1810). Generally, it's only safe to refer to figures in `man/figures` @@ -42,7 +46,6 @@ allowing you to suppress warnings about topics that are not listed in the index (#1716). ->>>>>>> 2720abc02fbddbb761104d44d30ce7a3d0c26812 * `build_reference()` will run `pkgdown/pre-reference.R` before and `pkgdown/post-reference.R` after running examples. These allow you to do any setup or teardown operations you might need (#1602). diff --git a/R/build-reference.R b/R/build-reference.R index f3514d51d..d3bb1c48f 100644 --- a/R/build-reference.R +++ b/R/build-reference.R @@ -316,8 +316,10 @@ build_reference_topic <- function(topic, render_page( pkg, "reference-topic", data = data, - path = path("reference", topic$file_out) + path = path("reference", topic$file_out), + tweaks = list(tweak_reference_highlighting) ) + invisible() } diff --git a/R/html-build.R b/R/html-build.R index 932a7c223..5741de6c8 100644 --- a/R/html-build.R +++ b/R/html-build.R @@ -46,12 +46,4 @@ escape_html <- function(x) { x } -unescape_html <- function(x) { - x <- gsub("<", "<", x) - x <- gsub(">", ">", x) - x <- gsub("&", "&", x) - x -} - - strip_html_tags <- function(x) gsub("<.*?>", "", x) diff --git a/R/markdown.R b/R/markdown.R index 41d9dd7b9..f1ac2c724 100644 --- a/R/markdown.R +++ b/R/markdown.R @@ -1,4 +1,4 @@ -markdown_text <- function(text, pkg = pkg, ...) { +markdown_text <- function(text, pkg = list(), ...) { if (identical(text, NA_character_) || is.null(text)) { return(NULL) } diff --git a/R/rd-html.R b/R/rd-html.R index 59816b913..3b38ec029 100644 --- a/R/rd-html.R +++ b/R/rd-html.R @@ -432,15 +432,11 @@ as_html.tag_code <- function(x, ..., auto_link = TRUE) { #' @export as_html.tag_preformatted <- function(x, ...) { - text <- flatten_text(x, ...) - - # Need to unescape so that highlight_text() can tell if it's R code - # or not. It'll re-escape if needed - text <- unescape_html(text) - highlight_text(text) + # the language is stored in a prior \if{}{\out{}} block, so we delay + # highlighting until we have the complete html page + pre(flatten_text(x, ...)) } - #' @export as_html.tag_kbd <- tag_wrapper("", "") #' @export diff --git a/R/render.r b/R/render.r index edad375fe..b0e40e64a 100644 --- a/R/render.r +++ b/R/render.r @@ -19,8 +19,9 @@ #' If `""` (the default), prints to standard out. #' @param depth Depth of path relative to base directory. #' @param quiet If `quiet`, will suppress output messages +#' @param tweaks List of "tweak" functions applied to output HTML. #' @export -render_page <- function(pkg = ".", name, data, path = "", depth = NULL, quiet = FALSE) { +render_page <- function(pkg = ".", name, data, path = "", depth = NULL, quiet = FALSE, tweaks = NULL) { pkg <- as_pkgdown(pkg) if (is.null(depth)) { @@ -81,6 +82,9 @@ render_page <- function(pkg = ".", name, data, path = "", depth = NULL, quiet = if (pkg$desc$has_dep("R6")) { tweak_link_R6(html, pkg$package) } + for (tweak in tweaks) { + tweak(html) + } rendered <- as.character(html, options = character()) write_if_different(pkg, rendered, path, quiet = quiet) diff --git a/R/test.R b/R/test.R index b43982bc9..21bef42a2 100644 --- a/R/test.R +++ b/R/test.R @@ -189,7 +189,7 @@ NULL #' warning(crayon::bold("This is bold")) NULL -#' Test case: verbatim blocks +#' Test case: preformatted blocks & syntax highlighting #' #' This description block is required so that verbatim blocks are recognized #' and rendered correctly. @@ -210,6 +210,17 @@ NULL #' that #' ``` #' +#' And this block should get syntax highlighting: +#' +#' ```yaml +#' yaml: +#' this +#' +#' OR: +#' +#' yaml: +#' that +#' ``` #' @name test-verbatim #' @keywords internal #' @family tests diff --git a/R/tweak-reference.R b/R/tweak-reference.R new file mode 100644 index 000000000..8fc112575 --- /dev/null +++ b/R/tweak-reference.R @@ -0,0 +1,64 @@ +# Syntax highlight for preformatted code blocks +tweak_reference_highlighting <- function(html) { + # We only process code inside ref-section since examples and usage are + # handled elsewhere + base <- xml2::xml_find_all(html, "//div[contains(@class, 'ref-section')]") + + # There are three cases: + # 1)
with no wrapper, as created by ``` + pre_unwrapped <- xml2::xml_find_all(base, "//pre") + purrr::walk(pre_unwrapped, tweak_highlight_r) + + div <- xml2::xml_find_all(base, "//div") + div_sourceCode <- div[has_class(div, "sourceCode")] + # 2)with class sourceCode + R, as created by ```R + div_sourceCode_r <- div_sourceCode[has_class(div_sourceCode, "r")] + purrr::walk(div_sourceCode_r, tweak_highlight_r) + + # 3)with class sourceCode + another language, e.g. ```yaml + div_sourceCode_other <- div_sourceCode[!has_class(div_sourceCode, "r")] + purrr::walk(div_sourceCode_other, tweak_highlight_other) + + invisible() +} + +tweak_highlight_r <- function(block) { + code <- xml2::xml_find_first(block, ".//code") + if (is.na(code)) { + return(FALSE) + } + + text <- xml2::xml_text(code) + out <- downlit::highlight(text, classes = downlit::classes_pandoc()) + if (is.na(out) || identical(out, "")) { + return(FALSE) + } + + html <- xml2::read_html(out) + xml_replace_contents(code, xml2::xml_find_first(html, "body")) + + TRUE +} + +tweak_highlight_other <- function(div) { + code <- xml2::xml_find_first(div, ".//code") + if (is.na(code)) { + return(FALSE) + } + + lang <- sub("sourceCode ", "", xml2::xml_attr(div, "class")) + md <- paste0("```", lang, "\n", xml2::xml_text(code), "\n```") + html <- markdown_text(md) + + xml_replace_contents(code, xml2::xml_find_first(html, "body/div/pre/code")) + TRUE +} + +xml_replace_contents <- function(node, new) { + xml2::xml_remove(xml2::xml_contents(node)) + + contents <- xml2::xml_contents(new) + for (child in contents) { + xml2::xml_add_child(node, child) + } +} diff --git a/man/render_page.Rd b/man/render_page.Rd index 22b1622b1..1a6c0738c 100644 --- a/man/render_page.Rd +++ b/man/render_page.Rd @@ -1,11 +1,19 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/render.R +% Please edit documentation in R/render.r \name{render_page} \alias{render_page} \alias{data_template} \title{Render page with template} \usage{ -render_page(pkg = ".", name, data, path = "", depth = NULL, quiet = FALSE) +render_page( + pkg = ".", + name, + data, + path = "", + depth = NULL, + quiet = FALSE, + tweaks = NULL +) data_template(pkg = ".", depth = 0L) } @@ -31,6 +39,8 @@ If \code{""} (the default), prints to standard out.} \item{depth}{Depth of path relative to base directory.} \item{quiet}{If \code{quiet}, will suppress output messages} + +\item{tweaks}{List of "tweak" functions applied to output HTML.} } \description{ Each page is composed of four templates: "head", "header", "content", and diff --git a/man/test-verbatim.Rd b/man/test-verbatim.Rd index 2843bb2b6..49162652e 100644 --- a/man/test-verbatim.Rd +++ b/man/test-verbatim.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/test.R \name{test-verbatim} \alias{test-verbatim} -\title{Test case: verbatim blocks} +\title{Test case: preformatted blocks & syntax highlighting} \description{ This description block is required so that verbatim blocks are recognized and rendered correctly. @@ -19,6 +19,15 @@ OR: yaml: that } + +And this block should get syntax highlighting:\if{html}{\out{}}\preformatted{yaml: + this + +OR: + +yaml: + that +}\if{html}{\out{}} } \seealso{ Other tests: diff --git a/tests/testthat/test-rd-html.R b/tests/testthat/test-rd-html.R index e324c1faf..206eabd62 100644 --- a/tests/testthat/test-rd-html.R +++ b/tests/testthat/test-rd-html.R @@ -367,15 +367,7 @@ test_that("nested item with whitespace parsed correctly", { # Verbatim ---------------------------------------------------------------- -# test_that("parseable preformatted blocks are highlighted", { -# out <- flatten_para(rd_text("\\preformatted{1}")) -# expect_equal(out, "1
\n") -# -# out <- flatten_para(rd_text("\\preformatted{1 > 2}")) -# expect_equal(out, "1 > 2\n") -# }) - -test_that("unparseable blocks aren't double escaped", { +test_that("preformatted blocks aren't double escaped", { out <- flatten_para(rd_text("\\preformatted{\\%>\\%}")) expect_equal(out, "\n") }) @@ -390,11 +382,6 @@ test_that("spaces are preserved in preformatted blocks", { expect_equal(out, "%>%
\n") }) -test_that("parseable blocks get R syntax highlighting", { - out <- flatten_para(rd_text("\\preformatted{foo}")) - expect_equal(out, "^\n\n b\n\n c
\n") -}) - # Usage ------------------------------------------------------------------- test_that("S4 methods gets comment", { diff --git a/tests/testthat/test-tweak-reference.R b/tests/testthat/test-tweak-reference.R new file mode 100644 index 000000000..63cc0390c --- /dev/null +++ b/tests/testthat/test-tweak-reference.R @@ -0,0 +1,85 @@ +test_that("highlightsfoo
wrapped inwith language info", { + html <- xml2::read_html(' +++ ') + tweak_reference_highlighting(html) + expect_equal(xpath_attr(html, "//code/span", "class"), c("fl", "op", "fl")) + expect_equal(xpath_text(html, "//code/span"), c("1", "+", "2")) + + html <- xml2::read_html(' ++++1 + 2
++ ') + tweak_reference_highlighting(html) + # Select all leaf to work around variations in pandoc styling + expect_equal(xpath_attr(html, "//code//span[not(span)]", "class"), c("fu", "kw", "at")) + expect_equal(xpath_text(html, "//code//span[not(span)]"), c("field", ":", " value")) +}) + +test_that("highlight unwrapped+++field: value
", { + # If parseable, assume R + html <- xml2::read_html(' +++1 + 2
+ ') + tweak_reference_highlighting(html) + expect_equal(xpath_attr(html, "//code/span", "class"), c("fl", "op", "fl")) + expect_equal(xpath_text(html, "//code/span"), c("1", "+", "2")) + + # If not parseable, leave as is + html <- xml2::read_html(' +++foo(
+ ') + tweak_reference_highlighting(html) + expect_equal(xpath_length(html, "//code//span"), 0) +}) + + +# highlighting ------------------------------------------------------------ + +test_that("can highlight R code", { + html <- xml2::read_xml('') + tweak_highlight_r(html) + + expect_equal(xpath_attr(html, "//code/span", "class"), c("fl", "op", "fl")) + expect_equal(xpath_text(html, "//code/span"), c("1", "+", "2")) +}) + +test_that("fails cleanly", { + html <- xml2::read_xml('1 + 2
') + expect_equal(tweak_highlight_r(html), FALSE) + + html <- xml2::read_xml('1 +
') + expect_equal(tweak_highlight_r(html), FALSE) + + html <- xml2::read_xml('') + expect_equal(tweak_highlight_r(html), FALSE) +}) + +test_that("can highlight other languages", { + html <- xml2::read_xml('') + tweak_highlight_other(html) + + # Select all leaf to work around variations in pandoc styling + expect_equal(xpath_attr(html, "//code//span[not(span)]", "class"), c("fu", "kw", "at")) + expect_equal(xpath_text(html, "//code//span[not(span)]"), c("field", ":", " value")) +}) + +test_that("fails cleanly", { + html <- xml2::read_xml('field: value
') + tweak_highlight_other(html) + expect_equal(xpath_text(html, "//code"), "") + + html <- xml2::read_xml('') + expect_equal(tweak_highlight_other(html), FALSE) +})