| christophe dervieux | 92fa469 | 2021-09-21 16:15:17 +0200 | [diff] [blame] | 1 | # function to lookup reveal resource |
| 2 | reveal_resources <- function(...) { |
| 3 | system.file("rmarkdown/templates/revealjs_presentation/resources", |
| 4 | ..., |
| Marc Kupietz | e195cea | 2026-08-17 13:17:33 +0200 | [diff] [blame] | 5 | package = "revealjs.ids") |
| christophe dervieux | 24418a8 | 2021-09-21 16:17:18 +0200 | [diff] [blame] | 6 | } |
| 7 | |
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 8 | |
| 9 | revealjs_lib_path <- function(...) { |
| Marc Kupietz | e195cea | 2026-08-17 13:17:33 +0200 | [diff] [blame] | 10 | pkg <- system.file(package = "revealjs.ids") |
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 11 | lib_folder <- list.files(pkg, pattern = "reveal.js-")[1] |
| Marc Kupietz | e195cea | 2026-08-17 13:17:33 +0200 | [diff] [blame] | 12 | system.file(lib_folder, ..., package = "revealjs.ids") |
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 13 | } |
| 14 | |
| 15 | revealjs_version <- function() { |
| 16 | as.numeric_version(gsub(".*reveal\\.js-(.*)$", "\\1", revealjs_lib_path())) |
| 17 | } |
| 18 | |
| christophe dervieux | 24418a8 | 2021-09-21 16:17:18 +0200 | [diff] [blame] | 19 | # Convert boolean from R to JS boolean |
| Christophe Dervieux | 2f01dc9 | 2021-09-22 09:37:18 +0200 | [diff] [blame] | 20 | jsbool <- function(value) ifelse(value, "true", "false") |
| 21 | |
| 22 | # transfrom reveal option as pandoc variable |
| 23 | process_reveal_option <- function(option, value) { |
| 24 | if (is.logical(value)) { |
| 25 | value <- jsbool(value) |
| 26 | } else if (is.character(value)) { |
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 27 | # Special handling for some vector options |
| 28 | if ( |
| 29 | # chalkboard plugin options |
| 30 | # e.g: color: [ 'rgba(0,0,255,1)', 'rgba(255,255,255,0.5)' ] |
| 31 | grepl("chalkboard-(background|draw)", option) |
| 32 | # e.g autoAnimateStyles: ['opacity','color'] |
| 33 | || grepl("autoAnimateStyles", option) |
| 34 | ) { |
| 35 | if (length(value) > 1 || !grepl("^\\[.*\\]$", value)) { |
| 36 | value <- sprintf("[%s]", paste(paste0("'", value, "'"), collapse = ", ")) |
| 37 | } |
| Christophe Dervieux | 2f01dc9 | 2021-09-22 09:37:18 +0200 | [diff] [blame] | 38 | } |
| 39 | # Add quotes around some config that can be several type |
| 40 | # like number or percent unit or slideNumber = true or slideNumber = 'c/t' |
| 41 | if ( |
| 42 | option %in% c("slideNumber") || |
| 43 | (option %in% c("width", "height") && grepl("%$", value))) { |
| 44 | value <- paste0("'", value, "'") |
| 45 | } |
| 46 | } |
| 47 | pandoc_variable_arg(option, value) |
| Christophe Dervieux | aa008e4 | 2021-09-23 16:52:37 +0200 | [diff] [blame] | 48 | } |
| Marc Kupietz | 59e95a6 | 2026-08-29 10:19:05 +0300 | [diff] [blame] | 49 | |
| 50 | # Resolve a logo image (yaml entries `partner_logo` / `funder_logo`) to a URL |
| 51 | # usable inside CSS: http(s) and data URLs pass through unchanged, local |
| 52 | # files are inlined as base64 data URIs. Inlining is required: CSS background |
| 53 | # URLs would resolve relative to the theme stylesheet, and document-relative |
| 54 | # files would be left behind when the rendered html is deployed elsewhere. |
| 55 | logo_uri <- function(path) { |
| 56 | if (grepl("^(https?|data):", path)) { |
| 57 | return(path) |
| 58 | } |
| 59 | if (!file.exists(path)) { |
| 60 | stop("Logo file not found: ", path, call. = FALSE) |
| 61 | } |
| 62 | ext <- tolower(tools::file_ext(path)) |
| 63 | mime <- switch(ext, |
| 64 | svg = "image/svg+xml", png = "image/png", jpg = , jpeg = "image/jpeg", |
| 65 | gif = "image/gif", webp = "image/webp", |
| 66 | stop("Unsupported logo format '", ext, "' (use svg, png, jpg, gif or webp)", |
| 67 | call. = FALSE |
| 68 | ) |
| 69 | ) |
| 70 | if (!requireNamespace("base64enc", quietly = TRUE)) { |
| 71 | stop("Package \"base64enc\" needed to embed local logo files. Please install it.", |
| 72 | call. = FALSE |
| 73 | ) |
| 74 | } |
| 75 | sprintf("data:%s;base64,%s", mime, base64enc::base64encode(path)) |
| 76 | } |
| 77 | |
| Marc Kupietz | b2cc523 | 2026-08-29 16:21:52 +0300 | [diff] [blame^] | 78 | # qrcode::generate_svg() paints the code -- quiet zone included -- into a |
| 79 | # fixed 300x300 canvas and emits no viewBox, so the svg cannot be resized by |
| 80 | # CSS: given a smaller box it keeps drawing at full size and only its top |
| 81 | # left corner remains visible. Give it a viewBox cropped to the dark modules |
| 82 | # instead. The code then scales to whatever box it is placed in, and its |
| 83 | # quiet zone becomes the surrounding layout's business (on the title slide |
| 84 | # the white plate behind the code supplies it), which keeps the placement |
| 85 | # independent of how many modules the encoded url happens to need. |
| 86 | qr_svg_fit <- function(svg) { |
| 87 | rects <- regmatches(svg, gregexpr("<rect[^>]*fill=\"black\"[^>]*>", svg))[[1]] |
| 88 | attr_num <- function(name) { |
| 89 | suppressWarnings(as.numeric( |
| 90 | sub(sprintf(".*[ ]%s=\"([0-9.]+)(px)?\".*", name), "\\1", rects) |
| 91 | )) |
| 92 | } |
| 93 | x <- attr_num("x") |
| 94 | y <- attr_num("y") |
| 95 | w <- attr_num("width") |
| 96 | h <- attr_num("height") |
| 97 | if (!length(rects) || anyNA(c(x, y, w, h))) { |
| 98 | return(svg) |
| 99 | } |
| 100 | left <- min(x) |
| 101 | top <- min(y) |
| 102 | side <- max(max(x + w) - left, max(y + h) - top) |
| 103 | vb <- sprintf( |
| 104 | 'viewBox="%s %s %s %s" width="%s" height="%s"', |
| 105 | format(left), format(top), format(side), format(side), |
| 106 | format(side), format(side) |
| 107 | ) |
| 108 | root <- regmatches(svg, regexpr("<svg[^>]*>", svg)) |
| 109 | fitted <- sub( |
| 110 | "<svg", paste("<svg", vb), |
| 111 | gsub("[ ](width|height)=\"[0-9.]+\"", "", root), |
| 112 | fixed = TRUE |
| 113 | ) |
| 114 | # the xml prolog would end up as a stray comment once inlined into html |
| 115 | sub("^<[?]xml[^>]*[?]>", "", sub(root, fitted, svg, fixed = TRUE)) |
| 116 | } |
| 117 | |
| Marc Kupietz | 59e95a6 | 2026-08-29 10:19:05 +0300 | [diff] [blame] | 118 | #' HTML code for a linked QR code |
| 119 | #' |
| 120 | #' Generates a QR code (inline SVG) linking to the given URL and returns the |
| 121 | #' corresponding HTML code, so a QR code can be placed anywhere in a |
| 122 | #' presentation with inline R code. |
| 123 | #' |
| 124 | #' The `website` YAML entry of [revealjs_presentation()] uses this to |
| 125 | #' place a linked QR code in the top left of the title slide. |
| 126 | #' |
| 127 | #' @param url Target URL for the QR code |
| 128 | #' @param logo Optional path to a logo image to place in the center of the QR |
| 129 | #' code |
| 130 | #' @param text Optional caption displayed below the QR code (alias: |
| 131 | #' `caption`) |
| 132 | #' @param caption Alias for `text` |
| 133 | #' @return Character string with HTML code for a linked QR code |
| 134 | #' @export |
| 135 | qrlink <- function(url, logo = NULL, text = NULL, caption = text) { |
| 136 | if (!requireNamespace("qrcode", quietly = TRUE)) { |
| 137 | stop("Package \"qrcode\" needed to generate QR codes. Please install it.", |
| 138 | call. = FALSE |
| 139 | ) |
| 140 | } |
| 141 | tmp <- tempfile(fileext = ".svg") |
| 142 | qrcode <- qrcode::qr_code(url, ecl = if (is.null(logo)) "L" else "H") |
| 143 | if (!is.null(logo)) { |
| 144 | qrcode <- qrcode::add_logo(qrcode, logo, ecl = "L") |
| 145 | } |
| 146 | qrcode::generate_svg(qrcode, tmp, show = FALSE) |
| 147 | |
| 148 | cap_html <- if (!is.null(caption) && nchar(trimws(caption)) > 0) { |
| 149 | sprintf('<span class="qrcode-caption">%s</span>', caption) |
| 150 | } else { |
| 151 | "" |
| 152 | } |
| 153 | |
| Marc Kupietz | b2cc523 | 2026-08-29 16:21:52 +0300 | [diff] [blame^] | 154 | svg <- qr_svg_fit(gsub("\r?\n|\r", "", readChar(tmp, 1e7))) |
| Marc Kupietz | 59e95a6 | 2026-08-29 10:19:05 +0300 | [diff] [blame] | 155 | qr <- sprintf('<a class="qrcode" href="%s">%s</a>', url, svg) |
| 156 | if (nchar(cap_html) > 0) { |
| 157 | paste0('<div class="qrcode-container">', qr, cap_html, "</div>") |
| 158 | } else { |
| 159 | qr |
| 160 | } |
| 161 | } |