From 268b95d6afc09065e75fdb9e2a828e0873c8e039 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Wed, 29 May 2024 07:41:41 -0500 Subject: [PATCH] Optionally choose math rendering Fixes #1966 --- NEWS.md | 2 + R/build-article.R | 1 + R/build-articles.R | 6 +-- R/build-footer.R | 4 +- R/build-home-authors.R | 7 +-- R/build-home-index.R | 4 +- R/build-home-md.R | 2 +- R/build-news.R | 2 +- R/build-reference-index.R | 12 ++--- R/config.R | 9 +--- R/external-deps.R | 34 ++++++++++---- R/markdown.R | 61 +++++++++++++++++--------- R/theme.R | 2 +- R/tweak-reference.R | 2 +- tests/testthat/_snaps/init.md | 2 - tests/testthat/_snaps/markdown.md | 2 +- tests/testthat/test-build-home-index.R | 6 ++- tests/testthat/test-markdown.R | 35 ++++++++------- tests/testthat/test-tweak-tabset.R | 12 +++-- tests/testthat/test-tweak-tags.R | 4 +- 20 files changed, 125 insertions(+), 84 deletions(-) diff --git a/NEWS.md b/NEWS.md index b55d43222..64051f04c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # pkgdown (development version) +* New `template.math-rendering` allows you to control how math is rendered across your site. The default uses `mathml` which is low-dependency, but has the lowest fidelity. You can also use `mathjax`, the previous default, and `katex`, a faster alternative. (#1966). +* Mathjax now uses version 3.2.2. * All external assets (JS, CSS, fonts) are now directly included in the site instead of fetched from external CDN (@salim-b, #2249) * `build_reference_index()` now displays function lifecycle badges next to the function name (#2123). The badges are extracted only from the function description. You can now also use `has_lifecycle()` to select functions by their lifecycle status. * `build_articles()` now recognises a new `external-articles` top-level field that allows you to define articles that live in other packages (#2028). diff --git a/R/build-article.R b/R/build-article.R index 2e6a98d70..09986596a 100644 --- a/R/build-article.R +++ b/R/build-article.R @@ -119,6 +119,7 @@ build_rmarkdown_format <- function(pkg, theme = NULL, template = template$path, anchor_sections = FALSE, + math_method = config_math_rendering(pkg), extra_dependencies = bs_theme_deps_suppress() ) out$knitr$opts_chunk <- fig_opts_chunk(pkg$figures, out$knitr$opts_chunk) diff --git a/R/build-articles.R b/R/build-articles.R index ce0d1f590..bff5ebbc4 100644 --- a/R/build-articles.R +++ b/R/build-articles.R @@ -298,7 +298,7 @@ data_articles <- function(pkg = ".", is_index = FALSE, call = caller_env()) { external <- config_pluck_external_articles(pkg, call = call) articles <- rbind(internal, external) - articles$description <- lapply(articles$description, markdown_text_block) + articles$description <- lapply(articles$description, markdown_text_block, pkg = pkg) # Hack data structure so we can use select_topics() articles$alias <- as.list(articles$name) @@ -375,9 +375,9 @@ data_articles_index_section <- function(section, index, articles, pkg, call = ca error_call = call ) title <- markdown_text_inline( + pkg, section$title, error_path = paste0("articles[", index, "].title"), - error_pkg = pkg, error_call = call ) @@ -400,7 +400,7 @@ data_articles_index_section <- function(section, index, articles, pkg, call = ca list( title = title, - desc = markdown_text_block(section$desc), + desc = markdown_text_block(pkg, section$desc), class = section$class, contents = purrr::transpose(contents) ) diff --git a/R/build-footer.R b/R/build-footer.R index 50894a68a..ec6f9b05c 100644 --- a/R/build-footer.R +++ b/R/build-footer.R @@ -8,8 +8,8 @@ data_footer <- function(pkg = ".", call = caller_env()) { meta_structure <- config_pluck_list(pkg, "footer.structure", call = call) structure <- modify_list(footnote_structure(), meta_structure) - left <- markdown_text_block(paste0(components[structure$left], collapse = " ")) - right <- markdown_text_block(paste0(components[structure$right], collapse = " ")) + left <- markdown_text_block(pkg, paste0(components[structure$left], collapse = " ")) + right <- markdown_text_block(pkg, paste0(components[structure$right], collapse = " ")) list(left = left, right = right) } diff --git a/R/build-home-authors.R b/R/build-home-authors.R index 6cdfe3d85..828fe028c 100644 --- a/R/build-home-authors.R +++ b/R/build-home-authors.R @@ -114,11 +114,8 @@ author_name <- function(x, authors, pkg) { author <- authors[[name]] if (!is.null(author$html)) { - name <- markdown_text_inline( - author$html, - error_path = paste0("authors.", name, ".html"), - error_pkg = pkg - ) + error_path <- paste0("authors.", name, ".html") + name <- markdown_text_inline(pkg, author$html, error_path = error_path) } if (is.null(author$href)) { diff --git a/R/build-home-index.R b/R/build-home-index.R index ae2fa32a4..6e6593454 100644 --- a/R/build-home-index.R +++ b/R/build-home-index.R @@ -13,7 +13,7 @@ build_home_index <- function(pkg = ".", quiet = TRUE) { } else { cli::cli_inform("Reading {src_path(path_rel(src_path, pkg$src_path))}") local_options_link(pkg, depth = 0L) - data$index <- markdown_body(src_path) + data$index <- markdown_body(pkg, src_path) } cur_digest <- file_digest(dst_path) @@ -124,7 +124,7 @@ data_home_sidebar <- function(pkg = ".", call = caller_env()) { needs_components <- setdiff(structure, names(default_components)) custom_yaml <- config_pluck_sidebar_components(pkg, needs_components, call = call) custom_components <- purrr::map(custom_yaml, function(x) { - sidebar_section(x$title, markdown_text_block(x$text)) + sidebar_section(x$title, markdown_text_block(pkg, x$text)) }) components <- modify_list(default_components, custom_components) diff --git a/R/build-home-md.R b/R/build-home-md.R index c139de89e..6e5f3bf16 100644 --- a/R/build-home-md.R +++ b/R/build-home-md.R @@ -37,7 +37,7 @@ package_mds <- function(path, in_dev = FALSE) { render_md <- function(pkg, filename) { cli::cli_inform("Reading {src_path(path_rel(filename, pkg$src_path))}") - body <- markdown_body(filename, strip_header = TRUE) + body <- markdown_body(pkg, filename, strip_header = TRUE) path <- path_ext_set(path_file(filename), "html") render_page(pkg, "title-body", diff --git a/R/build-news.R b/R/build-news.R index 0d08b3afe..45a3f9a92 100644 --- a/R/build-news.R +++ b/R/build-news.R @@ -147,7 +147,7 @@ build_news_multi <- function(pkg = ".") { utils::globalVariables(".") data_news <- function(pkg = list()) { - html <- markdown_body(path(pkg$src_path, "NEWS.md")) + html <- markdown_body(pkg, path(pkg$src_path, "NEWS.md")) xml <- xml2::read_html(html) downlit::downlit_html_node(xml) diff --git a/R/build-reference-index.R b/R/build-reference-index.R index d4db7ae45..642ef743b 100644 --- a/R/build-reference-index.R +++ b/R/build-reference-index.R @@ -89,13 +89,13 @@ data_reference_index_rows <- function(section, index, pkg, call = caller_env()) if (has_name(section, "title")) { rows[[1]] <- list( title = markdown_text_inline( + pkg, section$title, error_path = paste0("reference[", index, "].title"), - error_call = call, - error_pkg = pkg + error_call = call ), slug = make_slug(section$title), - desc = markdown_text_block(section$desc), + desc = markdown_text_block(pkg, section$desc), is_internal = is_internal ) } @@ -103,13 +103,13 @@ data_reference_index_rows <- function(section, index, pkg, call = caller_env()) if (has_name(section, "subtitle")) { rows[[2]] <- list( subtitle = markdown_text_inline( + pkg, section$subtitle, error_path = paste0("reference[", index, "].subtitle"), - error_call = call, - error_pkg = pkg + error_call = call ), slug = make_slug(section$subtitle), - desc = markdown_text_block(section$desc), + desc = markdown_text_block(pkg, section$desc), is_internal = is_internal ) } diff --git a/R/config.R b/R/config.R index b4d3257a7..74e6434f7 100644 --- a/R/config.R +++ b/R/config.R @@ -54,12 +54,7 @@ config_pluck_markdown_inline <- function(pkg, call = caller_env()) { text <- config_pluck_string(pkg, path, default, call = call) - markdown_text_inline( - text, - error_path = path, - error_pkg = pkg, - error_call = call - ) + markdown_text_inline(pkg, text, error_path = path, error_call = call) } config_pluck_markdown_block <- function(pkg, @@ -68,7 +63,7 @@ config_pluck_markdown_block <- function(pkg, call = caller_env()) { text <- config_pluck_string(pkg, path, default, call = call) - markdown_text_block(text) + markdown_text_block(pkg, text) } config_pluck_bool <- function(pkg, diff --git a/R/external-deps.R b/R/external-deps.R index 1348970b5..7483304a8 100644 --- a/R/external-deps.R +++ b/R/external-deps.R @@ -1,5 +1,5 @@ -external_dependencies <- function() { - list( +external_dependencies <- function(pkg, call = caller_env()) { + purrr::compact(list( fontawesome::fa_html_dependency(), cached_dependency( name = "headroom", @@ -53,21 +53,37 @@ external_dependencies <- function() { ) ) ), + math_dependency(pkg, call = call) + )) +} + +math_dependency <- function(pkg, call = caller_env()) { + math <- config_math_rendering(pkg) + if (math == "mathjax") { cached_dependency( name = "MathJax", - version = "2.7.5", + version = "3.2.2", files = list( list( - url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js", - integrity = "sha256-nvJJv9wWKEm88qvoQl9ekL2J+k/RWIsaSScxxlsrv8k=" - ), + url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-chtml.min.js", + integrity = "sha512-T8xxpazDtODy3WOP/c6hvQI2O9UPdARlDWE0CvH1Cfqc0TXZF6GZcEKL7tIR8VbfS/7s/J6C+VOqrD6hIo++vQ==" + ) + ) + ) + } else if (math == "katex") { + cached_dependency( + name = "KaTex", + version = "0.16.10", + files = list( list( - url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/config/TeX-AMS-MML_HTMLorMML.js", - integrity = "sha256-84DKXVJXs0/F8OTMzX4UR909+jtl4G7SPypPavF+GfA=" + url = "https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js", + integrity = "sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" ) ) ) - ) + } else { + NULL + } } cached_dependency <- function(name, version, files) { diff --git a/R/markdown.R b/R/markdown.R index 10266a001..72410fddf 100644 --- a/R/markdown.R +++ b/R/markdown.R @@ -1,36 +1,33 @@ -markdown_text <- function(text, ...) { +markdown_text <- function(pkg, text, ...) { if (identical(text, NA_character_) || is.null(text)) { return(NULL) } md_path <- withr::local_tempfile() write_lines(text, md_path) - markdown_path_html(md_path, ...) + markdown_path_html(pkg, md_path, ...) } -markdown_text_inline <- function(text, +markdown_text_inline <- function(pkg, + text, error_path, - error_pkg, error_call = caller_env()) { - html <- markdown_text(text) + html <- markdown_text(pkg, text) if (is.null(html)) { return() } children <- xml2::xml_children(xml2::xml_find_first(html, ".//body")) if (length(children) > 1) { - config_abort( - error_pkg, - "{.field {error_path}} must be inline markdown.", - call = error_call - ) + msg <- "{.field {error_path}} must be inline markdown." + config_abort(pkg, msg, call = error_call) } paste0(xml2::xml_contents(children), collapse = "") } -markdown_text_block <- function(text, ...) { - html <- markdown_text(text, ...) +markdown_text_block <- function(pkg, text, ...) { + html <- markdown_text(pkg, text, ...) if (is.null(html)) { return() } @@ -39,8 +36,8 @@ markdown_text_block <- function(text, ...) { paste0(as.character(children, options = character()), collapse = "") } -markdown_body <- function(path, strip_header = FALSE) { - xml <- markdown_path_html(path, strip_header = strip_header) +markdown_body <- function(pkg, path, strip_header = FALSE) { + xml <- markdown_path_html(pkg, path, strip_header = strip_header) if (is.null(xml)) { return(NULL) @@ -63,9 +60,9 @@ markdown_body <- function(path, strip_header = FALSE) { ) } -markdown_path_html <- function(path, strip_header = FALSE) { +markdown_path_html <- function(pkg, path, strip_header = FALSE) { html_path <- withr::local_tempfile() - convert_markdown_to_html(path, html_path) + convert_markdown_to_html(pkg, path, html_path) xml <- xml2::read_html(html_path, encoding = "UTF-8") if (!inherits(xml, "xml_node")) { return(NULL) @@ -81,7 +78,7 @@ markdown_path_html <- function(path, strip_header = FALSE) { structure(xml, title = title) } -markdown_to_html <- function(text, dedent = 4, bs_version = 3) { +markdown_to_html <- function(pkg, text, dedent = 4, bs_version = 3) { if (dedent) { text <- gsub(paste0("($|\n)", strrep(" ", dedent)), "\\1", text, perl = TRUE) } @@ -90,14 +87,14 @@ markdown_to_html <- function(text, dedent = 4, bs_version = 3) { html_path <- withr::local_tempfile() write_lines(text, md_path) - convert_markdown_to_html(md_path, html_path) + convert_markdown_to_html(pkg, md_path, html_path) html <- xml2::read_html(html_path, encoding = "UTF-8") tweak_page(html, "markdown", list(bs_version = bs_version)) html } -convert_markdown_to_html <- function(in_path, out_path, ...) { +convert_markdown_to_html <- function(pkg, in_path, out_path, ...) { if (rmarkdown::pandoc_available("2.0")) { from <- "markdown+gfm_auto_identifiers-citations+emoji+autolink_bare_uris" } else if (rmarkdown::pandoc_available("1.12.3")) { @@ -109,7 +106,6 @@ convert_markdown_to_html <- function(in_path, out_path, ...) { cli::cli_abort("Pandoc not available") } } - rmarkdown::pandoc_convert( input = in_path, output = out_path, @@ -121,10 +117,33 @@ convert_markdown_to_html <- function(in_path, out_path, ...) { "--indented-code-classes=R", "--section-divs", "--wrap=none", - "--mathjax", + paste0("--", config_math_rendering(pkg)), ... )) ) invisible() } + +config_math_rendering <- function(pkg, call = caller_env()) { + if (is.null(pkg)) { + # Special case for tweak_highlight_other() where it's too annoying to + # pass down the package, and it doesn't matter much anyway. + return("mathml") + } + + math <- config_pluck_string( + pkg, + "template.math-rendering", + default = "mathml", + call = call + ) + allowed <- c("mathml", "mathjax", "katex") + + if (!math %in% allowed) { + msg <- "{.field template.math-rendering} must be one of {allowed}, not {math}." + config_abort(pkg, msg, call = call) + } + + math +} diff --git a/R/theme.R b/R/theme.R index b08d8ecca..6a7dadc46 100644 --- a/R/theme.R +++ b/R/theme.R @@ -5,7 +5,7 @@ build_bslib <- function(pkg = ".", call = caller_env()) { cur_deps <- find_deps(pkg) cur_digest <- purrr::map_chr(cur_deps, file_digest) - deps <- c(bslib::bs_theme_dependencies(bs_theme), external_dependencies()) + deps <- c(bslib::bs_theme_dependencies(bs_theme), external_dependencies(pkg)) deps <- lapply(deps, htmltools::copyDependencyToDir, path(pkg$dst_path, "deps")) deps <- lapply(deps, htmltools::makeDependencyRelative, pkg$dst_path) diff --git a/R/tweak-reference.R b/R/tweak-reference.R index 803db0aa8..a284da29d 100644 --- a/R/tweak-reference.R +++ b/R/tweak-reference.R @@ -66,7 +66,7 @@ tweak_highlight_other <- function(div) { # many backticks to account for possible nested code blocks # like a Markdown code block with code chunks inside md <- paste0("``````", lang, "\n", xml2::xml_text(code), "\n``````") - html <- markdown_text(md) + html <- markdown_text(NULL, md) xml_replace_contents(code, xml2::xml_find_first(html, "body/div/pre/code")) TRUE diff --git a/tests/testthat/_snaps/init.md b/tests/testthat/_snaps/init.md index c71a7dff9..24217a8de 100644 --- a/tests/testthat/_snaps/init.md +++ b/tests/testthat/_snaps/init.md @@ -23,8 +23,6 @@ Copying /BS5/assets/link.svg to link.svg Copying /BS5/assets/pkgdown.js to pkgdown.js Copying pkgdown/extra.css to extra.css - Updating deps/MathJax-2.7.5/MathJax.js - Updating deps/MathJax-2.7.5/TeX-AMS-MML_HTMLorMML.js Updating deps/bootstrap-5.3.1/bootstrap.bundle.min.js Updating deps/bootstrap-5.3.1/bootstrap.bundle.min.js.map Updating deps/bootstrap-5.3.1/bootstrap.min.css diff --git a/tests/testthat/_snaps/markdown.md b/tests/testthat/_snaps/markdown.md index 929464e01..a0c36ec30 100644 --- a/tests/testthat/_snaps/markdown.md +++ b/tests/testthat/_snaps/markdown.md @@ -1,7 +1,7 @@ # markdown_text_inline() works with inline markdown Code - markdown_text_inline("x\n\ny", error_pkg = pkg, error_path = "title") + markdown_text_inline(pkg, "x\n\ny", error_path = "title") Condition Error: ! title must be inline markdown. diff --git a/tests/testthat/test-build-home-index.R b/tests/testthat/test-build-home-index.R index 6d0b4199e..de2d72044 100644 --- a/tests/testthat/test-build-home-index.R +++ b/tests/testthat/test-build-home-index.R @@ -18,7 +18,11 @@ test_that("title and description come from DESCRIPTION by default", { }) test_that("math is handled", { - pkg <- local_pkgdown_site(test_path("assets/home-readme-rmd"), clone = TRUE) + pkg <- local_pkgdown_site( + test_path("assets/home-readme-rmd"), + meta = list(template = list(`math-rendering` = "mathjax")), + clone = TRUE + ) write_lines(c("$1 + 1$"), path(pkg$src_path, "README.md")) suppressMessages(init_site(pkg)) diff --git a/tests/testthat/test-markdown.R b/tests/testthat/test-markdown.R index 2396557b9..3e4d8fbb2 100644 --- a/tests/testthat/test-markdown.R +++ b/tests/testthat/test-markdown.R @@ -1,50 +1,55 @@ test_that("handles empty inputs (returns NULL)", { - expect_null(markdown_text_inline("")) - expect_null(markdown_text_inline(NULL)) - expect_null(markdown_text_block(NULL)) - expect_null(markdown_text_block("")) + pkg <- local_pkgdown_site() + expect_null(markdown_text_inline(pkg, "")) + expect_null(markdown_text_inline(pkg, NULL)) + expect_null(markdown_text_block(pkg, NULL)) + expect_null(markdown_text_block(pkg, "")) path <- withr::local_tempfile() file_create(path) - expect_null(markdown_body(path)) + expect_null(markdown_body(pkg, path)) }) test_that("header attributes are parsed", { - text <- markdown_text_block("# Header {.class #id}") + pkg <- local_pkgdown_site() + text <- markdown_text_block(pkg, "# Header {.class #id}") expect_match(text, "id=\"id\"") expect_match(text, "class=\".*? class\"") }) test_that("markdown_text_inline() works with inline markdown", { - expect_equal(markdown_text_inline("**lala**"), "lala") - pkg <- local_pkgdown_site() + expect_equal(markdown_text_inline(pkg, "**lala**"), "lala") + expect_snapshot(error = TRUE, { - markdown_text_inline("x\n\ny", error_pkg = pkg, error_path = "title") + markdown_text_inline(pkg, "x\n\ny", error_path = "title") }) }) test_that("markdown_text_block() works with inline and block markdown", { skip_if_no_pandoc("2.17.1") - expect_equal(markdown_text_block("**x**"), "

x

") - expect_equal(markdown_text_block("x\n\ny"), "

x

y

") + pkg <- local_pkgdown_site() + expect_equal(markdown_text_block(pkg, "**x**"), "

x

") + expect_equal(markdown_text_block(pkg, "x\n\ny"), "

x

y

") }) test_that("markdown_body() captures title", { + pkg <- local_pkgdown_site() temp <- withr::local_tempfile() write_lines("# Title\n\nSome text", temp) - html <- markdown_body(temp) + html <- markdown_body(pkg, temp) expect_equal(attr(html, "title"), "Title") # And can optionally strip it - html <- markdown_body(temp, strip_header = TRUE) + html <- markdown_body(pkg, temp, strip_header = TRUE) expect_equal(attr(html, "title"), "Title") expect_false(grepl("Title", html)) }) test_that("markdown_text_*() handles UTF-8 correctly", { - expect_equal(markdown_text_block("\u00f8"), "

\u00f8

") - expect_equal(markdown_text_inline("\u00f8"), "\u00f8") + pkg <- local_pkgdown_site() + expect_equal(markdown_text_block(pkg, "\u00f8"), "

\u00f8

") + expect_equal(markdown_text_inline(pkg, "\u00f8"), "\u00f8") }) diff --git a/tests/testthat/test-tweak-tabset.R b/tests/testthat/test-tweak-tabset.R index 1e32e6f8b..1cd79e6a7 100644 --- a/tests/testthat/test-tweak-tabset.R +++ b/tests/testthat/test-tweak-tabset.R @@ -1,6 +1,7 @@ test_that("sections with class .tabset are converted to tabsets", { skip_on_os("windows") # some line ending problem - html <- markdown_to_html(" + pkg <- local_pkgdown_site() + html <- markdown_to_html(pkg, " # Tabset {.tabset .tabset-pills} ## Tab 1 @@ -23,7 +24,8 @@ test_that("sections with class .tabset are converted to tabsets", { test_that("can adjust active tab", { skip_on_os("windows") # some line ending problem - html <- markdown_to_html(" + pkg <- local_pkgdown_site() + html <- markdown_to_html(pkg, " ## Tabset {.tabset .tabset-pills} ### Tab 1 @@ -43,7 +45,8 @@ test_that("can adjust active tab", { }) test_that("can fade", { - html <- markdown_to_html(" + pkg <- local_pkgdown_site() + html <- markdown_to_html(pkg, " ## Tabset {.tabset .tabset-fade} ### Tab 1 @@ -63,7 +66,8 @@ test_that("can fade", { }) test_that("can accept html", { - html <- markdown_to_html(" + pkg <- local_pkgdown_site() + html <- markdown_to_html(pkg, " ## Tabset {.tabset} ### Tab 1 `with_code` {#toc-1} diff --git a/tests/testthat/test-tweak-tags.R b/tests/testthat/test-tweak-tags.R index 7101c960f..848b19da4 100644 --- a/tests/testthat/test-tweak-tags.R +++ b/tests/testthat/test-tweak-tags.R @@ -249,8 +249,8 @@ test_that("selectively remove hide- divs", { test_that("can process footnote with code", { skip_if_no_pandoc("2.17.1") - - html <- markdown_to_html(" + pkg <- local_pkgdown_site() + html <- markdown_to_html(pkg, " Hooray[^1] [^1]: Including code: