├── .covrignore ├── .github ├── .gitignore └── workflows │ ├── auto-pkg-maintenance.yaml │ ├── test-coverage.yaml │ ├── check-standard.yaml │ └── pkgdown.yaml ├── vignettes ├── .gitignore ├── images │ ├── epoxy-shiny │ │ ├── epoxy-app-1.png │ │ ├── shiny-app-1.png │ │ ├── epoxy-app-error.png │ │ ├── epoxy-app-input.png │ │ ├── epoxy-app-bechdel.png │ │ ├── epoxy-app-copies.png │ │ ├── epoxy-app-markdown.png │ │ ├── epoxy-mustache-empty.png │ │ ├── epoxy-mustache-list.png │ │ └── epoxy-example-markdown.png │ ├── mel-poole-gT-Sob4njj8-unsplash.jpg │ ├── hal-gatewood-tZc3vjPCk-Q-unsplash.jpg │ └── javier-quesada-ZYb_fGvNndA-unsplash.jpg ├── epoxy-script.Rmd └── epoxy.Rmd ├── LICENSE ├── cran-comments.md ├── data └── bechdel.rda ├── tests ├── testthat │ ├── rmds │ │ ├── use-file_example-no-yaml.md │ │ ├── use-file_html.md │ │ ├── use-file_latex.md │ │ ├── use-file_example-1.md │ │ ├── use-file_example-2.md │ │ ├── use-chunk_chunk.Rmd │ │ ├── use-chunk_collapse.Rmd │ │ └── use-chunk_chunk-opts.Rmd │ ├── _snaps │ │ ├── transformers.md │ │ ├── epoxy_transform_inline.md │ │ └── shiny_ui_epoxy_markdown.md │ ├── apps │ │ ├── no-shiny │ │ │ ├── app.R │ │ │ └── epoxy-no-shiny.js │ │ └── epoxy-html-list │ │ │ └── app.R │ ├── helpers.R │ ├── test-shiny_ui_epoxy_markdown.R │ ├── test-shiny_word-list.R │ ├── test-shiny_ui_epoxy_html-no-shiny.R │ ├── test-shiny_ui_epoxy_mustache.R │ ├── test-epoxy_mustache.R │ ├── test-shiny_ui_epoxy_html-list.R │ ├── test-shiny.R │ ├── test-deprecated.R │ ├── test-utils-knitr.R │ ├── test-epoxy_transform_inline.R │ ├── test-engines.R │ └── test-epoxy_use.R └── testthat.R ├── man ├── figures │ ├── logo.png │ ├── lifecycle-stable.svg │ ├── lifecycle-defunct.svg │ ├── lifecycle-archived.svg │ ├── lifecycle-maturing.svg │ ├── lifecycle-deprecated.svg │ ├── lifecycle-superseded.svg │ ├── lifecycle-experimental.svg │ └── lifecycle-questioning.svg ├── examples │ ├── epoxy_transform_one_shot.R │ ├── render_epoxy.R │ ├── epoxy_mustache.R │ ├── epoxy_transform.R │ ├── epoxy_transform_inline.R │ └── epoxy.R ├── fragments │ ├── installation.Rmd │ ├── example-acme.Rmd │ ├── first-example.Rmd │ ├── setup.Rmd │ ├── example-movie.Rmd │ ├── epoxy-lead.Rmd │ ├── example-airbnb.Rmd │ └── transformers-epoxy_transform_set.Rmd ├── engine_pick.Rd ├── epoxy-package.Rd ├── bechdel.Rd ├── epoxy_style.Rd ├── use_epoxy_knitr_engines.Rd ├── render_epoxy.Rd ├── run_epoxy_example_app.Rd ├── ui_epoxy_mustache.Rd ├── epoxy_mustache.Rd ├── epoxy_transform_html.Rd ├── epoxy_transform_one_shot.Rd └── epoxy_use.Rd ├── pkgdown ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ └── apple-touch-icon-180x180.png ├── assets │ └── doc-versions.json ├── _pkgdown.yml ├── index.Rmd └── index.md ├── .vscode └── settings.json ├── inst ├── examples │ ├── word-list │ │ ├── www │ │ │ ├── extra.js │ │ │ └── animation.css │ │ ├── server.R │ │ └── ui.R │ ├── render_epoxy │ │ └── app.R │ ├── ui_epoxy_mustache │ │ └── app.R │ ├── ui_epoxy_html │ │ └── app.R │ └── ui_epoxy_markdown │ │ └── app.R ├── srcjs │ ├── output-epoxy.css │ ├── output-epoxy-mustache.js │ └── output-epoxy.js └── lib │ └── NOTICE ├── R ├── zzz.R ├── epoxy-package.R ├── bechdel.R ├── utils.R ├── utils-knitr.R ├── epoxy_mustache.R ├── deprecated.R ├── epoxy_transform_html.R └── epoxy_use.R ├── epoxy.Rproj ├── .Rbuildignore ├── package.json ├── LICENSE.md ├── NAMESPACE ├── DESCRIPTION ├── .gitignore ├── data-raw └── bechdel.R ├── README.Rmd └── CODE_OF_CONDUCT.md /.covrignore: -------------------------------------------------------------------------------- 1 | R/zzz.R 2 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2023 2 | COPYRIGHT HOLDER: Garrick Aden-Buie 3 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## R CMD check results 2 | 3 | 0 errors | 0 warnings | 0 note 4 | -------------------------------------------------------------------------------- /data/bechdel.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/data/bechdel.rda -------------------------------------------------------------------------------- /tests/testthat/rmds/use-file_example-no-yaml.md: -------------------------------------------------------------------------------- 1 | {one}, {two}, {three}, {four} 2 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(epoxy) 3 | 4 | test_check("epoxy") 5 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/man/figures/logo.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /tests/testthat/rmds/use-file_html.md: -------------------------------------------------------------------------------- 1 | --- 2 | engine: epoxy_html 3 | --- 4 | 5 | {{text}} 6 | -------------------------------------------------------------------------------- /tests/testthat/rmds/use-file_latex.md: -------------------------------------------------------------------------------- 1 | --- 2 | engine: epoxy_latex 3 | --- 4 | 5 | \href{<< link >>}{<< text >>} 6 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /tests/testthat/rmds/use-file_example-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | .open: "[[" 3 | .close: "]]" 4 | ... 5 | 6 | [[one]] then [[two]] then [[three]] 7 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-app-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-app-1.png -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/shiny-app-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/shiny-app-1.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "todo-tree.tree.scanMode": "workspace only", 3 | "editor.insertSpaces": false, 4 | "editor.tabSize": 2 5 | } 6 | -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-app-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-app-error.png -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-app-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-app-input.png -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-app-bechdel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-app-bechdel.png -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-app-copies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-app-copies.png -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-app-markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-app-markdown.png -------------------------------------------------------------------------------- /vignettes/images/mel-poole-gT-Sob4njj8-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/mel-poole-gT-Sob4njj8-unsplash.jpg -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-mustache-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-mustache-empty.png -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-mustache-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-mustache-list.png -------------------------------------------------------------------------------- /vignettes/images/epoxy-shiny/epoxy-example-markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/epoxy-shiny/epoxy-example-markdown.png -------------------------------------------------------------------------------- /vignettes/images/hal-gatewood-tZc3vjPCk-Q-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/hal-gatewood-tZc3vjPCk-Q-unsplash.jpg -------------------------------------------------------------------------------- /vignettes/images/javier-quesada-ZYb_fGvNndA-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadenbuie/epoxy/HEAD/vignettes/images/javier-quesada-ZYb_fGvNndA-unsplash.jpg -------------------------------------------------------------------------------- /tests/testthat/rmds/use-file_example-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | .data: 3 | one: one fish 4 | two: two fish 5 | three: red fish 6 | four: blue fish 7 | ... 8 | 9 | {one}, {two}, {three}, {four} 10 | -------------------------------------------------------------------------------- /inst/examples/word-list/www/extra.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('epoxy-update', (ev) => { 2 | ev.target.classList.add('animate', 'blur') 3 | }) 4 | 5 | document 6 | .getElementById('word_list') 7 | .addEventListener('animationend', ev => ev.target.classList.remove('animate')) 8 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/transformers.md: -------------------------------------------------------------------------------- 1 | # engine_validate_alias(): errors with unknown engine names 2 | 3 | 'unknown' is not a valid engine name (language syntax). Valid choices include `md`, `markdown`, `glue`, `epoxy`, `html`, `glue_html`, `epoxy_html`, `latex`, `glue_latex`, or `epoxy_latex`. 4 | 5 | -------------------------------------------------------------------------------- /man/examples/epoxy_transform_one_shot.R: -------------------------------------------------------------------------------- 1 | abc <- c("a", "b", "c") 2 | 3 | epoxy("{abc}", .transformer = epoxy_transform_wrap("'")) 4 | 5 | epoxy("{abc}", .transformer = epoxy_transform_bold()) 6 | 7 | epoxy("{abc}", .transformer = epoxy_transform_italic()) 8 | 9 | epoxy("{abc}", .transformer = epoxy_transform_code()) 10 | 11 | epoxy("{abc}", .transformer = epoxy_transform_apply(toupper)) 12 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | .onLoad <- function(libname, pkgname, ...) { 2 | if (isNamespaceLoaded("knitr") && "knit_engines" %in% getNamespaceExports("knitr")) { 3 | use_epoxy_knitr_engines() 4 | } else { 5 | setHook(packageEvent("knitr", "onLoad"), function(...) { 6 | use_epoxy_knitr_engines() 7 | }) 8 | } 9 | 10 | if (requireNamespace("debugme", quietly = TRUE)) { 11 | debugme::debugme() 12 | } 13 | 14 | invisible() 15 | } 16 | -------------------------------------------------------------------------------- /tests/testthat/apps/no-shiny/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(epoxy) 3 | 4 | ui <- fixedPage( 5 | textInput("first", "First Name", "John"), 6 | textInput("last", "Last Name", "Doe"), 7 | ui_epoxy_html( 8 | "hello", 9 | p("Hello, {{first}} {{last}}!", "data-test-id" = "text") 10 | ), 11 | includeScript("epoxy-no-shiny.js") 12 | ) 13 | 14 | server <- function(input, output, session) { 15 | 16 | } 17 | 18 | shinyApp(ui, server) 19 | -------------------------------------------------------------------------------- /R/epoxy-package.R: -------------------------------------------------------------------------------- 1 | #' @importFrom glue glue glue_collapse glue_data 2 | #' @keywords internal 3 | #' @aliases epoxy-package NULL 4 | "_PACKAGE" 5 | 6 | # The following block is used by usethis to automatically manage 7 | # roxygen namespace tags. Modify with care! 8 | ## usethis namespace: start 9 | #' @importFrom htmltools HTML 10 | #' @importFrom lifecycle deprecated 11 | ## usethis namespace: end 12 | NULL 13 | 14 | .globals <- new.env(parent = emptyenv()) 15 | -------------------------------------------------------------------------------- /tests/testthat/rmds/use-chunk_chunk.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: markdown_document 3 | --- 4 | 5 | ```{r setup, include=FALSE} 6 | library(epoxy) 7 | 8 | the_data <- list( 9 | list(first = "one", second = "two"), 10 | list(first = "three", second = "four") 11 | ) 12 | ``` 13 | 14 | ```{epoxy chunk-template, .data = the_data[[1]]} 15 | {first} followed by {second} 16 | ``` 17 | 18 | ```{r echo=FALSE} 19 | epoxy_use_chunk(the_data[[2]], "chunk-template") 20 | ``` 21 | -------------------------------------------------------------------------------- /epoxy.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: No 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /tests/testthat/rmds/use-chunk_collapse.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: md_document 3 | --- 4 | 5 | ```{r setup, include=FALSE} 6 | library(epoxy) 7 | 8 | the_data <- list( 9 | first = c("one", "three"), 10 | second = c("two", "four") 11 | ) 12 | ``` 13 | 14 | ```{epoxy chunk-template, .data = the_data, .collapse = " == "} 15 | {first} followed by {second} 16 | ``` 17 | 18 | ```{r echo=FALSE} 19 | epoxy_use_chunk( 20 | .data = the_data, 21 | label = "chunk-template", 22 | .collapse = " || " 23 | ) 24 | ``` 25 | -------------------------------------------------------------------------------- /.github/workflows/auto-pkg-maintenance.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | # pull_request_target: 3 | # types: [opened, synchronize, labeled] 4 | pull_request: 5 | push: 6 | branches: main 7 | 8 | name: Package Maintenance 9 | 10 | jobs: 11 | auto-pkg-maintenance: 12 | uses: rstudio/education-workflows/.github/workflows/auto-pkg-maintenance.yaml@v1 13 | with: 14 | extra-packages: deps::. 15 | install-local-package: true 16 | source-repository-owner: gadenbuie 17 | style-roxygen-examples: false 18 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^epoxy\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^_dev$ 5 | ^README\.Rmd$ 6 | ^_test$ 7 | ^\.github$ 8 | ^_pkgdown\.yml$ 9 | ^data-raw$ 10 | ^pkgdown$ 11 | ^\.covrignore$ 12 | ^node_modules$ 13 | ^package\.json$ 14 | ^package-lock\.json$ 15 | ^docs$ 16 | ^vignettes/epoxy\.Rmd$ 17 | ^\.vscode$ 18 | ^CODE_OF_CONDUCT\.md$ 19 | ^cran-comments\.md$ 20 | ^vignettes/images/hal-gatewood-tZc3vjPCk-Q-unsplash\.jpg$ 21 | ^vignettes/images/javier-quesada-ZYb_fGvNndA-unsplash\.jpg$ 22 | ^vignettes/images/mel-poole-gT-Sob4njj8-unsplash\.jpg$ 23 | -------------------------------------------------------------------------------- /inst/examples/word-list/server.R: -------------------------------------------------------------------------------- 1 | function(input, output, session) { 2 | words <- shiny::reactive({ 3 | w <- c( 4 | "one", "two", "three", "four", "five", 5 | "six", "seven", "eight", "nine", "ten" 6 | ) 7 | w[seq_len(as.integer(input$number))] 8 | }) 9 | 10 | n_words <- shiny::reactive( { 11 | n_words <- length(words()) 12 | paste(n_words, ngettext(n_words, "word", "words")) 13 | }) 14 | 15 | output$word_list <- render_epoxy( 16 | verb = if (length(words()) == 1) "is" else "are", 17 | n_words = n_words(), 18 | the_words = words() 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /man/fragments/installation.Rmd: -------------------------------------------------------------------------------- 1 | You can install epoxy from CRAN: 2 | 3 | ```{r eval=FALSE} 4 | install.packages("epoxy") 5 | ``` 6 | 7 | You can install the latest development version of epoxy with [remotes] 8 | 9 | ```{r eval=FALSE} 10 | # install.packages("remotes") 11 | remotes::install_github("gadenbuie/epoxy") 12 | ``` 13 | 14 | or from [gadenbuie.r-universe.dev](https://gadenbuie.r-universe.dev). 15 | 16 | ```{r eval=FALSE} 17 | options(repos = c( 18 | gadenbuie = "https://gadenbuie.r-universe.dev/", 19 | getOption("repos") 20 | )) 21 | 22 | install.packages("epoxy") 23 | ``` 24 | 25 | 26 | [remotes]: https://remotes.r-lib.org 27 | -------------------------------------------------------------------------------- /inst/srcjs/output-epoxy.css: -------------------------------------------------------------------------------- 1 | .epoxy-html.recalculating { opacity: 1; } 2 | 3 | .epoxy-html.recalculating [data-epoxy-item], 4 | .epoxy-html.recalculating [data-epoxy-copy] { 5 | animation-name: epoxy-pulse; 6 | animation-direction: alternate; 7 | animation-iteration-count: infinite; 8 | animation-duration: 1s; 9 | animation-delay: 1s; 10 | } 11 | 12 | @keyframes epoxy-pulse { 13 | 0% { opacity: 1; } 14 | 100% { opacity: 0.3; } 15 | } 16 | 17 | .epoxy-html .epoxy-item__error { 18 | text-decoration-style: wavy; 19 | text-decoration-color: red; 20 | text-decoration-line: underline; 21 | text-decoration-thickness: from-font; 22 | } 23 | -------------------------------------------------------------------------------- /tests/testthat/apps/epoxy-html-list/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(epoxy) 3 | 4 | ui <- fixedPage( 5 | sliderInput("n", "Letters", 1, 26, 3), 6 | p( 7 | "data-test-id" = "desc", 8 | ui_epoxy_html("desc", "You've picked {{n}} {{thing}}:") 9 | ), 10 | tags$ul( 11 | "data-test-id" = "list", 12 | ui_epoxy_html("list", "{{item}}", .item_tag = "li") 13 | ) 14 | ) 15 | 16 | server <- function(input, output, session) { 17 | output$list <- render_epoxy( 18 | item = letters[1:input$n] 19 | ) 20 | 21 | output$desc <- render_epoxy( 22 | n = input$n, 23 | thing = if (input$n == 1) "letter" else "letters" 24 | ) 25 | } 26 | 27 | shinyApp(ui, server) 28 | -------------------------------------------------------------------------------- /inst/examples/render_epoxy/app.R: -------------------------------------------------------------------------------- 1 | # Generated from example in render_epoxy(): do not edit by hand 2 | library(shiny) 3 | library(epoxy) 4 | 5 | # to provide the HTML template and `render_epoxy()` to 6 | # update the current time every second. 7 | 8 | ui <- shiny::fluidPage( 9 | shiny::h2("Current Time"), 10 | ui_epoxy_html( 11 | "time", 12 | shiny::p("The current time is {{strong time}}.") 13 | ) 14 | ) 15 | 16 | server <- function(input, output, session) { 17 | current_time <- shiny::reactive({ 18 | shiny::invalidateLater(1000) 19 | strftime(Sys.time(), "%F %T") 20 | }) 21 | 22 | output$time <- render_epoxy(time = current_time()) 23 | } 24 | 25 | shiny::shinyApp(ui, server) 26 | -------------------------------------------------------------------------------- /man/examples/render_epoxy.R: -------------------------------------------------------------------------------- 1 | # This small app shows the current time using `ui_epoxy_html()` 2 | # to provide the HTML template and `render_epoxy()` to 3 | # update the current time every second. 4 | 5 | ui <- shiny::fluidPage( 6 | shiny::h2("Current Time"), 7 | ui_epoxy_html( 8 | "time", 9 | shiny::p("The current time is {{strong time}}.") 10 | ) 11 | ) 12 | 13 | server <- function(input, output, session) { 14 | current_time <- shiny::reactive({ 15 | shiny::invalidateLater(1000) 16 | strftime(Sys.time(), "%F %T") 17 | }) 18 | 19 | output$time <- render_epoxy(time = current_time()) 20 | } 21 | 22 | if (rlang::is_interactive()) { 23 | shiny::shinyApp(ui, server) 24 | } 25 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/epoxy_transform_inline.md: -------------------------------------------------------------------------------- 1 | # epoxy_transform_inline(): errors if a non-dotted argument name is provided 2 | 3 | Functions provided in `...` must be named with a leading dot (`.`). 4 | i Did you mean to use the following? 5 | * `.this_thing` instead of `this_thing` 6 | * `.that_thing` instead of `that_thing` 7 | 8 | # epoxy_transform_inline(): errors if an unnamed argument is provided 9 | 10 | Arguments to `epoxy_transform_inline()` must all be named. 11 | 12 | # epoxy_transform_inline(): returns an error if transformation fails 13 | 14 | Could not transform the text "a" using `.blam`. 15 | Caused by error in `fmts_custom[[class]]()`: 16 | ! this error is expected in the text 17 | 18 | -------------------------------------------------------------------------------- /inst/examples/word-list/ui.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(epoxy) 3 | 4 | page <- 5 | if (!requireNamespace("bslib", quietly = TRUE)) { 6 | bslib::page_fluid 7 | } else { 8 | fluidPage 9 | } 10 | 11 | page( 12 | div( 13 | class = "row", 14 | style = "margin: 1em auto; max-width: 800px;", 15 | div( 16 | class = "col-sm-6", 17 | selectInput("number", "Number of words", choices = 1:10) 18 | ), 19 | div( 20 | class = "col-sm-6 mt-xs-5", 21 | ui_epoxy_html( 22 | .id = "word_list", 23 | tags$h3("Here {{ verb }} your {{ n_words }}"), 24 | tags$ul("{{li.word the_words}}") 25 | ) 26 | ), 27 | tags$link(rel = "stylesheet", href = "animation.css"), 28 | tags$script(src = "extra.js") 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /tests/testthat/apps/no-shiny/epoxy-no-shiny.js: -------------------------------------------------------------------------------- 1 | /* global EpoxyHTML */ 2 | function sendUpdatesToEpoxy (id) { 3 | return (event, value) => { 4 | if (typeof value === 'undefined') { 5 | value = { [event.target.id]: event.target.value } 6 | } 7 | if (typeof value === 'function') { 8 | value = value(event) 9 | } 10 | 11 | const data = { [id]: value } 12 | EpoxyHTML.update_all(data, true) 13 | } 14 | } 15 | 16 | function initApp () { 17 | const initData = { 18 | hello: { 19 | first: document.getElementById('first').value, 20 | last: document.getElementById('last').value 21 | } 22 | } 23 | EpoxyHTML.update_all(initData) 24 | ;['first', 'last'].forEach(inputId => { 25 | document 26 | .getElementById(inputId) 27 | .addEventListener('input', sendUpdatesToEpoxy('hello')) 28 | }) 29 | } 30 | 31 | initApp() 32 | -------------------------------------------------------------------------------- /tests/testthat/rmds/use-chunk_chunk-opts.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: markdown_document 3 | --- 4 | 5 | ```{r setup, include=FALSE} 6 | library(epoxy) 7 | 8 | the_data <- list( 9 | list(first = "one", second = "two"), 10 | list(first = "three", second = "four"), 11 | list(first = "five", second = "six"), 12 | list(first = "seven", second = "eight"), 13 | list(first = "nine", second = "ten") 14 | ) 15 | 16 | knitr::opts_chunk$set(.data = the_data[[1]]) 17 | ``` 18 | 19 | ```{epoxy chunk-template, .data = the_data[[2]]} 20 | {first} followed by {second} 21 | ``` 22 | 23 | ```{r echo=FALSE} 24 | epoxy_use_chunk(the_data[[3]], "chunk-template") 25 | ``` 26 | 27 | ```{r echo=FALSE, .data = the_data[[3]]} 28 | epoxy_use_chunk(the_data[[4]], "chunk-template") 29 | ``` 30 | 31 | ```{r echo=FALSE, .data = the_data[[5]]} 32 | epoxy_use_chunk(label = "chunk-template") 33 | ``` 34 | 35 | `r epoxy_use_chunk(label = "chunk-template")` 36 | -------------------------------------------------------------------------------- /man/figures/lifecycle-stable.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclestablestable -------------------------------------------------------------------------------- /man/figures/lifecycle-defunct.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycledefunctdefunct -------------------------------------------------------------------------------- /man/figures/lifecycle-archived.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclearchivedarchived -------------------------------------------------------------------------------- /man/figures/lifecycle-maturing.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclematuringmaturing -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycledeprecateddeprecated -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclesupersededsuperseded -------------------------------------------------------------------------------- /pkgdown/assets/doc-versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "Latest", 4 | "version": "1.0.0", 5 | "url": "https://pkg.garrickadenbuie.com/epoxy/" 6 | }, 7 | { 8 | "text": "Development", 9 | "version": "1.0.0.9000", 10 | "url": "https://pkg.garrickadenbuie.com/epoxy/dev/", 11 | "banner": { 12 | "html": "This is the development version of epoxy. View the latest release.", 13 | "class": "alert-warning" 14 | } 15 | }, 16 | "---", 17 | "Releases", 18 | { 19 | "text": "v0.1.1", 20 | "version": "0.1.1", 21 | "url": "https://pkg.garrickadenbuie.com/epoxy/v0.1.1/", 22 | "banner": { 23 | "html": "A newer version of epoxy is available!", 24 | "class": "alert-info" 25 | } 26 | }, 27 | { 28 | "text": "v0.1.0", 29 | "version": "0.1.0", 30 | "url": "https://pkg.garrickadenbuie.com/epoxy/v0.1.0/", 31 | "banner": { 32 | "html": "A newer version of epoxy is available!", 33 | "class": "alert-info" 34 | } 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /tests/testthat/helpers.R: -------------------------------------------------------------------------------- 1 | render_rmd <- function( 2 | rmd_text, 3 | ..., 4 | output_format = rmarkdown::md_document(), 5 | envir = new.env() 6 | ) { 7 | if (identical(Sys.getenv("TESTTHAT"), "true")) { 8 | skip_if_not(rmarkdown::pandoc_available("1.12.3")) 9 | } 10 | 11 | if (length(rmd_text) == 1 && !grepl("\n", rmd_text)) { 12 | if (file.exists(rmd_text)) { 13 | rmd_text <- readLines(rmd_text) 14 | } 15 | } 16 | 17 | tmpfile <- tempfile(fileext = ".Rmd") 18 | on.exit(unlink(tmpfile)) 19 | writeLines(rmd_text, tmpfile) 20 | out <- rmarkdown::render(tmpfile, output_format = output_format, ..., envir = envir, quiet = TRUE) 21 | if (is.character(out) && file.exists(out)) { 22 | on.exit(unlink(out), add = TRUE) 23 | readLines(out) 24 | } else out 25 | } 26 | 27 | render_basic_rmd <- function(..., envir = parent.frame()) { 28 | render_rmd(c( 29 | "---", 30 | "output: md_document", 31 | "---", 32 | "", 33 | ... 34 | ), envir = envir) 35 | } 36 | -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycleexperimentalexperimental -------------------------------------------------------------------------------- /man/figures/lifecycle-questioning.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclequestioningquestioning -------------------------------------------------------------------------------- /man/fragments/example-acme.Rmd: -------------------------------------------------------------------------------- 1 | Say you're writing a report and you've calculated revenue and expenses for your company: 2 | 3 | ````{verbatim} 4 | ```{r} 5 | library(epoxy) 6 | 7 | revenue <- 1000000 8 | expenses <- 800000 9 | ``` 10 | ```` 11 | 12 | With **epoxy** you can fold these values into your prose, 13 | formatting them as you use the values. 14 | 15 | ````{verbatim} 16 | ```{epoxy} 17 | Acme Corp. had a revenue of {.dollar revenue} 18 | and expenses of {.dollar expenses}, 19 | resulting in a profit of {.dollar profit <- revenue - expenses} 20 | and a gross margin of 21 | {.pct profit / revenue} 22 | ``` 23 | ```` 24 | 25 | The rendered output has the final values, formatted just right. 26 | 27 | ```{r echo = FALSE} 28 | library(epoxy) 29 | 30 | revenue <- 1000000 31 | expenses <- 800000 32 | ``` 33 | 34 |
35 | ```{epoxy echo = FALSE} 36 | Acme Corp. had a revenue of {.dollar revenue} 37 | and expenses of {.dollar expenses}, 38 | resulting in a profit of {.dollar profit <- revenue - expenses} 39 | and a gross margin of 40 | {.pct profit / revenue} 41 | ``` 42 |
43 | -------------------------------------------------------------------------------- /vignettes/epoxy-script.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "epoxy in R scripts" 3 | output: 4 | cleanrmd::html_document_clean: 5 | theme: new.css 6 | toc: true 7 | toc_depth: 2 8 | vignette: > 9 | %\VignetteIndexEntry{epoxy in R scripts} 10 | %\VignetteEngine{knitr::rmarkdown} 11 | %\VignetteEncoding{UTF-8} 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | library(epoxy) 16 | 17 | knitr::opts_chunk$set( 18 | collapse = TRUE, 19 | comment = "#>" 20 | ) 21 | ``` 22 | 23 | 24 | epoxy isn't just for reports and Shiny apps! 25 | You can use the `epoxy()` function just like an `epoxy` knitr chunk. 26 | 27 | ```{r} 28 | movie <- list( 29 | year = 1989, 30 | title = "Back to the Future Part II", 31 | budget = 4e+07 32 | ) 33 | 34 | epoxy( 35 | "The movie {.titlecase movie$title}", 36 | "was released in {movie$year}", 37 | "and was filmed with a budget of", 38 | "{.dollar movie$budget}.", 39 | .sep = "\n" 40 | ) 41 | ``` 42 | 43 | For HTML and LaTeX contexts, check out `epoxy_html()` and `epoxy_latex()`. 44 | These work just like `epoxy()`, 45 | but use convenient defaults for HTML and LaTeX settings. 46 | -------------------------------------------------------------------------------- /man/engine_pick.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/transformers.R 3 | \name{engine_pick} 4 | \alias{engine_pick} 5 | \title{Pick an engine-specific value} 6 | \usage{ 7 | engine_pick(md, html = md, latex = md) 8 | } 9 | \arguments{ 10 | \item{md, html, latex}{The value to use in a markdown, HTML, or LaTeX context.} 11 | } 12 | \value{ 13 | The value of \code{md}, \code{html} or \code{latex} depending on the epoxy or knitr 14 | currently being evaluated. 15 | } 16 | \description{ 17 | Set different values that will be used based on the current epoxy or knitr 18 | engine (one of \code{md}, \code{html}, or \code{latex}). The engine-specific value will be 19 | used inside epoxy knitr chunks or epoxy functions matching the source syntax: 20 | \code{\link[=epoxy]{epoxy()}} (\code{md}), \code{\link[=epoxy_html]{epoxy_html()}} (\code{html}), or \code{\link[=epoxy_latex]{epoxy_latex()}} (\code{latex}). 21 | } 22 | \examples{ 23 | # Markdown and HTML are okay with bare `$` character, 24 | # but we need to escape it in LaTeX. 25 | engine_pick(md = "$", latex = "\\\\$") 26 | 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epoxy", 3 | "version": "1.0.0", 4 | "description": "web dependencies for the {epoxy} R package", 5 | "main": "index.js", 6 | "scripts": { 7 | "copy": "npm run copy:mustache && npm run copy:hint.css", 8 | "copy:mustache": "cpy 'mustache.min.js' ../../inst/lib/mustache --cwd node_modules/mustache/", 9 | "copy:hint.css": "cpy 'hint.min.css' ../../inst/lib/hint.css --cwd node_modules/hint.css/", 10 | "lint": "standard inst/srcjs/*.js", 11 | "lint:fix": "standard --fix inst/srcjs/*.js", 12 | "build": "npm run lint:fix && npm run copy" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/gadenbuie/epoxy.git" 17 | }, 18 | "author": "Garrick Aden-Buie", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/gadenbuie/epoxy/issues" 22 | }, 23 | "homepage": "https://pkg.garrickadenbuie.com/epoxy", 24 | "dependencies": { 25 | "hint.css": "^2.7.0", 26 | "mustache": "^4.2.0" 27 | }, 28 | "devDependencies": { 29 | "cpy-cli": "^4.2.0", 30 | "standard": "^17.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 Garrick Aden-Buie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /man/epoxy-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/epoxy-package.R 3 | \docType{package} 4 | \name{epoxy-package} 5 | \alias{epoxy-package} 6 | \title{epoxy: String Interpolation for Documents, Reports and Apps} 7 | \description{ 8 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 9 | 10 | Extra strength 'glue' for data-driven templates. String interpolation for 'Shiny' apps or 'R Markdown' and 'knitr'-powered 'Quarto' documents, built on the 'glue' and 'whisker' packages. 11 | } 12 | \seealso{ 13 | Useful links: 14 | \itemize{ 15 | \item \url{https://pkg.garrickadenbuie.com/epoxy/} 16 | \item \url{https://github.com/gadenbuie/epoxy} 17 | \item Report bugs at \url{https://github.com/gadenbuie/epoxy/issues} 18 | } 19 | 20 | } 21 | \author{ 22 | \strong{Maintainer}: Garrick Aden-Buie \email{garrick@adenbuie.com} (\href{https://orcid.org/0000-0002-7111-0077}{ORCID}) 23 | 24 | Other contributors: 25 | \itemize{ 26 | \item Kushagra Gour (hint.css) [contributor] 27 | \item The mustache.js community (mustache.js) [contributor] 28 | } 29 | 30 | } 31 | \keyword{internal} 32 | -------------------------------------------------------------------------------- /man/fragments/first-example.Rmd: -------------------------------------------------------------------------------- 1 | To use epoxy in your R Markdown document, 2 | create a new chunk using the engine of your choice. 3 | In that chunk, write in markdown, HTML, or LaTeX as needed, 4 | wrapping R expressions inside the delimiters for the epoxy chunk. 5 | 6 | ````{verbatim} 7 | ```{epoxy} 8 | The average speed of the cars was **{mean(cars$speed)} mph.** 9 | But on average the distance traveled was only _{mean(cars$dist)}_. 10 | ``` 11 | ```` 12 | 13 | ```{epoxy} 14 | The average speed of the cars was **{mean(cars$speed)} mph**. 15 | But on average the distance traveled was only _{mean(cars$dist)} ft_. 16 | ``` 17 | 18 | `epoxy` is built around `glue::glue()`, 19 | which evaluates the R expressions in the `{ }` 20 | and inserts the results into the string. 21 | The chunk above is equivalent to this call to `glue::glue()`: 22 | 23 | ```{r} 24 | glue::glue("The average speed of the cars was **{mean(cars$speed)} mph**. 25 | But on average the distance traveled was only _{mean(cars$dist)} ft_.") 26 | ``` 27 | 28 | One immediate advantage of using `epoxy` instead of `glue::glue()` 29 | is that RStudio's autocompletion feature works inside `epoxy` chunks! 30 | Typing `cars$` in the chunk will suggest the columns of `cars`. 31 | -------------------------------------------------------------------------------- /man/examples/epoxy_mustache.R: -------------------------------------------------------------------------------- 1 | # The canonical mustache example 2 | epoxy_mustache( 3 | "Hello {{name}}!", 4 | "You have just won {{value}} dollars!", 5 | "{{#in_ca}}", 6 | "Well, {{taxed_value}} dollars, after taxes.", 7 | "{{/in_ca}}", 8 | .data = list( 9 | name = "Chris", 10 | value = 10000, 11 | taxed_value = 10000 - (10000 * 0.4), 12 | in_ca = TRUE 13 | ) 14 | ) 15 | 16 | # Vectorized over the rows of .data 17 | epoxy_mustache( 18 | "mpg: {{ mpg }}", 19 | "hp: {{ hp }}", 20 | "wt: {{ wt }}\n", 21 | .data = mtcars[1:2, ] 22 | ) 23 | 24 | # Non-vectorized 25 | epoxy_mustache( 26 | "mpg: {{ mpg }}", 27 | "hp: {{ hp }}", 28 | "wt: {{ wt }}", 29 | .data = mtcars[1:2, ], 30 | .vectorized = FALSE 31 | ) 32 | 33 | # With mustache partials 34 | epoxy_mustache( 35 | "Hello {{name}}!", 36 | "{{> salutation }}", 37 | "You have just won {{value}} dollars!", 38 | "{{#in_ca}}", 39 | "Well, {{taxed_value}} dollars, after taxes.", 40 | "{{/in_ca}}", 41 | .partials = list( 42 | salutation = c("Hope you are well, {{name}}.") 43 | ), 44 | .sep = " ", 45 | .data = list( 46 | name = "Chris", 47 | value = 10000, 48 | taxed_value = 10000 - (10000 * 0.4), 49 | in_ca = TRUE 50 | ) 51 | ) 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/testthat/test-shiny_ui_epoxy_markdown.R: -------------------------------------------------------------------------------- 1 | skip_on_cran() 2 | skip_if_not_installed("chromote") 3 | skip_if_not_installed("shinytest2") 4 | library(shinytest2) 5 | 6 | test_that("ui_epoxy_markdown()", { 7 | 8 | app <- AppDriver$new( 9 | app_dir = system.file("examples", "ui_epoxy_markdown", package = "epoxy"), 10 | name = "ui_epoxy_markdown", 11 | height = 500, 12 | width = 900, 13 | view = interactive(), 14 | expect_values_screenshot_args = FALSE 15 | ) 16 | 17 | idx <- bechdel$imdb_id == app$get_value(input = "movie") 18 | 19 | expect_equal( 20 | app$get_text("#about_movie #title"), 21 | bechdel$title[idx] 22 | ) 23 | 24 | expect_match( 25 | app$get_text("#about_movie p:nth-of-type(2)"), 26 | tolower(bechdel$genre[idx]) 27 | ) 28 | 29 | expect_snapshot(cat(app$get_text("#about_movie"))) 30 | 31 | app$set_inputs(movie = "0462538") 32 | app$wait_for_idle() 33 | 34 | idx <- bechdel$imdb_id == app$get_value(input = "movie") 35 | 36 | expect_equal( 37 | app$get_text("#about_movie #title"), 38 | bechdel$title[idx] 39 | ) 40 | 41 | expect_match( 42 | app$get_text("#about_movie p:nth-of-type(2)"), 43 | tolower(bechdel$genre[idx]) 44 | ) 45 | 46 | expect_snapshot(cat(app$get_text("#about_movie"))) 47 | }) 48 | -------------------------------------------------------------------------------- /man/examples/epoxy_transform.R: -------------------------------------------------------------------------------- 1 | epoxy("{.strong {.and letters[1:3]}}") 2 | epoxy("{.and {.strong letters[1:3]}}") 3 | 4 | # If you used the development version of epoxy, the above is equivalent to: 5 | epoxy("{letters[1:3]&}", .transformer = epoxy_transform("bold", "collapse")) 6 | epoxy("{letters[1:3]&}", .transformer = epoxy_transform("collapse", "bold")) 7 | 8 | # In an epoxy_html chunk... 9 | epoxy_html("{{.strong {{.or letters[1:3]}} }}") 10 | 11 | # Or in an epoxy_latex chunk... 12 | epoxy_latex("<<.and <<.strong letters[1:3]>> >>") 13 | 14 | # ---- Other Transformers ---- 15 | 16 | # Format numbers with an inline transformation 17 | amount <- 123.4234234 18 | epoxy("{.number amount}") 19 | epoxy( 20 | "{.number amount}", 21 | .transformer = epoxy_transform_inline( 22 | .number = scales::label_number(accuracy = 0.01) 23 | ) 24 | ) 25 | 26 | # Apply _any_ function to all replacements 27 | epoxy( 28 | "{amount} is the same as {amount}", 29 | .transformer = epoxy_transform_apply(round, digits = 0) 30 | ) 31 | 32 | epoxy( 33 | "{amount} is the same as {amount}", 34 | .transformer = epoxy_transform( 35 | epoxy_transform_apply(~ .x * 100), 36 | epoxy_transform_apply(round, digits = 2), 37 | epoxy_transform_apply(~ paste0(.x, "%")) 38 | ) 39 | ) 40 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/shiny_ui_epoxy_markdown.md: -------------------------------------------------------------------------------- 1 | # ui_epoxy_markdown() 2 | 3 | Code 4 | cat(app$get_text("#about_movie")) 5 | Output 6 | Inception 7 | Released: 2010 8 | Rated: PG-13 9 | IMDB Rating: 8.8 10 | Inception is an action, adventure, mystery film released in 2010. It 11 | was filmed in USA, UK with a budget of $160.0M and made $292.58M at the box office. Inception recieved a Bechdel 12 | rating of 3 for the following 13 | plot: 14 | 15 | A skilled extractor is offered a chance to regain his old life as payment for a task considered to be impossible. 16 | 17 | --- 18 | 19 | Code 20 | cat(app$get_text("#about_movie")) 21 | Output 22 | The Simpsons Movie 23 | Released: 2007 24 | Rated: PG-13 25 | IMDB Rating: 7.4 26 | The Simpsons Movie is an animation, adventure, comedy film released in 2007. It 27 | was filmed in USA with a budget of $72.5M and made $183.14M at the box office. The Simpsons Movie recieved a Bechdel 28 | rating of 3 for the following 29 | plot: 30 | 31 | After Homer accidentally pollutes the town's water supply, Springfield is encased in a gigantic dome by the EPA and the Simpson family are declared fugitives. 32 | 33 | -------------------------------------------------------------------------------- /man/fragments/setup.Rmd: -------------------------------------------------------------------------------- 1 | ```{r library-epoxy} 2 | library(epoxy) 3 | ``` 4 | 5 | Loading epoxy adds four new [knitr engines][knitr-engines], or chunk types. 6 | Each type lets you intermix text with R code or data (`expr` in the table below), 7 | and each is geared toward a different output context. 8 | 9 | | Engine | Output Context | Delimiter | 10 | | :------------ | :------------------- | :-----------------------------------: | 11 | | `epoxy` | all-purpose markdown | `{expr}` | 12 | | `epoxy_html` | HTML | `{{expr}}` | 13 | | `epoxy_latex` | LaTeX | `<>` | 14 | | `whisker` | all-purpose | [mustache template language][whisker] | 15 | 16 | ⚠️ **Caution:** Previously, epoxy provided a `glue` engine, 17 | but this conflicts with a similar chunk engine by the [glue] package. 18 | You can update existing documents to use the `epoxy` engine, 19 | or you can explicitly use epoxy's `glue` chunk 20 | by including the following in your setup chunk. 21 | 22 | ```r 23 | use_epoxy_glue_engine() 24 | ``` 25 | 26 | [whisker]: https://mustache.github.io/ 27 | [knitr-engines]: https://bookdown.org/yihui/rmarkdown/language-engines.html 28 | -------------------------------------------------------------------------------- /man/fragments/example-movie.Rmd: -------------------------------------------------------------------------------- 1 | Here's an example using a small list containing data about a `movie` 2 | (expand the section below to see the full code for `movie`). 3 | We can use the inline transformer to format the replacement text 4 | as we build up a description from this data. 5 | 6 |
Movie data 7 | 8 | ```{r} 9 | movie <- list( 10 | year = 1989, 11 | title = "Back to the Future Part II", 12 | budget = 4e+07, 13 | domgross = 118450002, 14 | imdb_rating = 7.8, 15 | actors = c( 16 | "Michael J. Fox", 17 | "Christopher Lloyd", 18 | "Lea Thompson", 19 | "Thomas F. Wilson" 20 | ), 21 | runtime = 108L 22 | ) 23 | ``` 24 | 25 |
26 | 27 | ````{verbatim} 28 | ```{epoxy} 29 | The movie {.emph {.titlecase movie$title}} 30 | was released in {.strong movie$year}. 31 | It earned {.dollar movie$domgross} 32 | with a budget of {.dollar movie$budget}, 33 | and it features movie stars 34 | {.and movie$actors}. 35 | ``` 36 | ```` 37 | 38 |
39 | ```{epoxy inline-movie-first} 40 | The movie {.emph {.titlecase movie$title}} 41 | was released in {.strong movie$year}. 42 | It earned {.dollar movie$domgross} 43 | with a budget of {.dollar movie$budget}, 44 | and it features movie stars 45 | {.and movie$actors}. 46 | ``` 47 |
48 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(engine_pick) 4 | export(epoxy) 5 | export(epoxyHTML) 6 | export(epoxy_html) 7 | export(epoxy_latex) 8 | export(epoxy_mustache) 9 | export(epoxy_style) 10 | export(epoxy_style_apply) 11 | export(epoxy_style_bold) 12 | export(epoxy_style_code) 13 | export(epoxy_style_collapse) 14 | export(epoxy_style_get) 15 | export(epoxy_style_html) 16 | export(epoxy_style_inline) 17 | export(epoxy_style_italic) 18 | export(epoxy_style_set) 19 | export(epoxy_style_wrap) 20 | export(epoxy_transform) 21 | export(epoxy_transform_apply) 22 | export(epoxy_transform_bold) 23 | export(epoxy_transform_code) 24 | export(epoxy_transform_collapse) 25 | export(epoxy_transform_get) 26 | export(epoxy_transform_html) 27 | export(epoxy_transform_inline) 28 | export(epoxy_transform_italic) 29 | export(epoxy_transform_set) 30 | export(epoxy_transform_wrap) 31 | export(epoxy_use_chunk) 32 | export(epoxy_use_file) 33 | export(renderEpoxyHTML) 34 | export(render_epoxy) 35 | export(run_epoxy_example_app) 36 | export(ui_epoxy_html) 37 | export(ui_epoxy_markdown) 38 | export(ui_epoxy_mustache) 39 | export(ui_epoxy_whisker) 40 | export(use_epoxy_glue_engine) 41 | export(use_epoxy_knitr_engines) 42 | importFrom(glue,glue) 43 | importFrom(glue,glue_collapse) 44 | importFrom(glue,glue_data) 45 | importFrom(htmltools,HTML) 46 | importFrom(lifecycle,deprecated) 47 | -------------------------------------------------------------------------------- /tests/testthat/test-shiny_word-list.R: -------------------------------------------------------------------------------- 1 | skip_on_cran() 2 | skip_if_not_installed("chromote") 3 | skip_if_not_installed("shinytest2") 4 | library(shinytest2) 5 | 6 | test_that("ui_epoxy_html() with an array of values", { 7 | 8 | app <- AppDriver$new( 9 | app_dir = system.file("examples", "word-list", package = "epoxy"), 10 | name = "shiny_word-list", 11 | height = 500, 12 | width = 700, 13 | view = interactive(), 14 | expect_values_screenshot_args = FALSE 15 | ) 16 | 17 | get_output_list <- function() { 18 | li <- app$get_js( 19 | "Array.from(document.querySelectorAll('#word_list li')).map(x => x.innerText)" 20 | ) 21 | unlist(li) 22 | } 23 | 24 | get_output_list_classes <- function() { 25 | li <- app$get_js( 26 | "Array.from(document.querySelectorAll('#word_list li')).map(x => x.className)" 27 | ) 28 | trimws(gsub("animate|blur|pop", "", unlist(li))) 29 | } 30 | 31 | expect_equal(app$get_value(input = "number"), "1") 32 | expect_equal(get_output_list(), "one") 33 | expect_equal(get_output_list_classes(), "word") 34 | 35 | app$set_inputs(number = "3") 36 | expect_equal(get_output_list(), c("one", "two", "three")) 37 | expect_equal(get_output_list_classes(), rep("word", 3)) 38 | 39 | app$set_inputs(number = "5") 40 | expect_equal(get_output_list(), c("one", "two", "three", "four", "five")) 41 | expect_equal(get_output_list_classes(), rep("word", 5)) 42 | 43 | app$set_inputs(number = "1") 44 | expect_equal(get_output_list(), "one") 45 | expect_equal(get_output_list_classes(), "word") 46 | }) 47 | -------------------------------------------------------------------------------- /man/examples/epoxy_transform_inline.R: -------------------------------------------------------------------------------- 1 | revenue <- 0.2123 2 | sales <- 42000.134 3 | 4 | # ---- Basic Example with Inline Formatting -------------------------------- 5 | epoxy( 6 | '{.pct revenue} of revenue generates {.dollar sales} in profits.' 7 | ) 8 | 9 | # With standard {glue} (`epoxy_transform_inline()` is a glue transformer) 10 | glue::glue( 11 | '{.pct revenue} of revenue generates {.dollar sales} in profits.', 12 | .transformer = epoxy_transform_inline() 13 | ) 14 | 15 | # ---- Setting Inline Formatting Options ---------------------------------- 16 | # To set inline format options, provide `scales::label_*()` to the supporting 17 | # epoxy_transform_inline arguments. 18 | epoxy( 19 | '{.pct revenue} of revenue generates {.dollar sales} in profits.', 20 | .transformer = epoxy_transform_inline( 21 | .percent = scales::label_percent(accuracy = 0.1), 22 | .dollar = scales::label_dollar(accuracy = 10) 23 | ) 24 | ) 25 | 26 | glue::glue( 27 | '{.pct revenue} of revenue generates {.dollar sales} in profits.', 28 | .transformer = epoxy_transform_inline( 29 | .percent = scales::label_percent(accuracy = 0.1), 30 | .dollar = scales::label_dollar(accuracy = 10) 31 | ) 32 | ) 33 | 34 | # ---- Custom Inline Formatting ------------------------------------------ 35 | # Add your own formatting functions 36 | search <- "why are cats scared of cucumbers" 37 | 38 | epoxy_html( 39 | "https://example.com?q={{ .url_encode search }}>", 40 | .transformer = epoxy_transform_inline( 41 | .url_encode = utils::URLencode 42 | ) 43 | ) 44 | -------------------------------------------------------------------------------- /tests/testthat/test-shiny_ui_epoxy_html-no-shiny.R: -------------------------------------------------------------------------------- 1 | skip_on_cran() 2 | skip_if_not_installed("chromote") 3 | skip_if_not_installed("shinytest2") 4 | library(shinytest2) 5 | 6 | test_that("ui_epoxy_html() can be used without shiny", { 7 | app <- AppDriver$new( 8 | app_dir = test_path("apps", "no-shiny"), 9 | name = "no-shiny", 10 | height = 500, 11 | width = 700, 12 | view = interactive(), 13 | expect_values_screenshot_args = FALSE 14 | ) 15 | on.exit(app$stop()) 16 | 17 | get_test_element_text <- function(id) { 18 | app$get_js( 19 | sprintf( 20 | "document.querySelector('[data-test-id=\"%s\"]').innerText", 21 | id 22 | ) 23 | ) 24 | } 25 | 26 | chrome <- app$get_chromote_session() 27 | 28 | update_input <- function(id, text) { 29 | inputs <- structure(list(text), names = id) 30 | app$set_inputs(!!!inputs) 31 | # In real life, the user's interaction would trigger this event 32 | app$run_js(sprintf( 33 | "document.getElementById('%s').dispatchEvent(new Event('input', { bubbles: true }))", 34 | id 35 | )) 36 | } 37 | 38 | expect_epoxy_text <- function() { 39 | inputs <- app$get_values(input = c("first", "last"))$input 40 | 41 | expect_equal( 42 | get_test_element_text("text"), 43 | paste0("Hello, ", inputs$first, " ", inputs$last, "!") 44 | ) 45 | } 46 | 47 | expect_epoxy_text() 48 | 49 | update_input("first", "Jane") 50 | expect_epoxy_text() 51 | 52 | update_input("last", "Diamonds") 53 | expect_epoxy_text() 54 | 55 | update_input("first", "Lucy") 56 | expect_epoxy_text() 57 | }) 58 | -------------------------------------------------------------------------------- /tests/testthat/test-shiny_ui_epoxy_mustache.R: -------------------------------------------------------------------------------- 1 | skip_on_cran() 2 | skip_if_not_installed("chromote") 3 | skip_if_not_installed("shinytest2") 4 | library(shinytest2) 5 | 6 | test_that("ui_epoxy_mustache() with an array of values", { 7 | 8 | app <- AppDriver$new( 9 | app_dir = system.file("examples", "ui_epoxy_mustache", package = "epoxy"), 10 | name = "shiny_word-list", 11 | height = 500, 12 | width = 700, 13 | view = interactive(), 14 | expect_values_screenshot_args = FALSE 15 | ) 16 | 17 | app$wait_for_idle() 18 | 19 | expect_equal( 20 | app$get_text("#template h2"), 21 | "Hello, user!" 22 | ) 23 | expect_equal( 24 | app$get_text("#template p"), 25 | "Do you have any favorite fruits?" 26 | ) 27 | 28 | app$set_inputs(name = "Jane") 29 | expect_equal( 30 | app$get_text("#template h2"), 31 | "Hello, Jane!" 32 | ) 33 | expect_match( 34 | app$get_js("document.querySelector('#template h2').className"), 35 | "text-success" 36 | ) 37 | 38 | app$set_inputs(name = "") 39 | expect_equal( 40 | app$get_js("document.querySelector('#template h2').className"), 41 | "" 42 | ) 43 | 44 | app$set_inputs(fruits = "apple, banana, mango") 45 | expect_match( 46 | app$get_text("#template p"), 47 | "Your favorite fruits are..." 48 | ) 49 | expect_setequal( 50 | app$get_text("#template li"), 51 | c("apple", "banana", "mango") 52 | ) 53 | 54 | app$set_inputs(fruits = NULL) 55 | expect_equal( 56 | app$get_text("#template h2"), 57 | "Hello, user!" 58 | ) 59 | expect_equal( 60 | app$get_text("#template p"), 61 | "Do you have any favorite fruits?" 62 | ) 63 | }) 64 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: epoxy 2 | Title: String Interpolation for Documents, Reports and Apps 3 | Version: 1.0.0.9000 4 | Authors@R: c( 5 | person("Garrick", "Aden-Buie", , "garrick@adenbuie.com", role = c("aut", "cre"), 6 | comment = c(ORCID = "0000-0002-7111-0077")), 7 | person("Kushagra", "Gour", role = "ctb", 8 | comment = "hint.css"), 9 | person("The mustache.js community", role = "ctb", 10 | comment = "mustache.js") 11 | ) 12 | Description: Extra strength 'glue' for data-driven templates. String 13 | interpolation for 'Shiny' apps or 'R Markdown' and 'knitr'-powered 14 | 'Quarto' documents, built on the 'glue' and 'whisker' packages. 15 | License: MIT + file LICENSE 16 | URL: https://pkg.garrickadenbuie.com/epoxy/, 17 | https://github.com/gadenbuie/epoxy 18 | BugReports: https://github.com/gadenbuie/epoxy/issues 19 | Depends: 20 | R (>= 3.6.0) 21 | Imports: 22 | and, 23 | glue (>= 1.5.0), 24 | htmltools, 25 | knitr (>= 1.37), 26 | lifecycle, 27 | purrr, 28 | rlang, 29 | rmarkdown, 30 | scales (>= 1.1.0), 31 | tools, 32 | whisker 33 | Suggests: 34 | cleanrmd, 35 | commonmark, 36 | dbplyr, 37 | debugme, 38 | dplyr, 39 | pandoc, 40 | shiny, 41 | shinytest2, 42 | testthat 43 | VignetteBuilder: 44 | cleanrmd, 45 | knitr, 46 | rmarkdown 47 | Config/Needs/rcmdcheck: RSQLite, rstudio/chromote 48 | Config/Needs/website: rstudio/rmarkdown, gadenbuie/grkgdown 49 | Config/testthat/edition: 3 50 | Encoding: UTF-8 51 | LazyData: true 52 | Roxygen: list(markdown = TRUE) 53 | RoxygenNote: 7.3.1 54 | -------------------------------------------------------------------------------- /man/examples/epoxy.R: -------------------------------------------------------------------------------- 1 | movie <- bechdel[1, ] 2 | movies <- bechdel[2:4, ] 3 | 4 | # A basic example with a single row of data 5 | epoxy("{.emph movie$title} ({movie$year}) was directed by {movie$director}.") 6 | 7 | # Or vectorized over multiple rows of data 8 | epoxy("* {.emph movies$title} ({movies$year}) was directed by {movies$director}.") 9 | 10 | # You can provide the data frame to `.data` to avoid repeating `data$` 11 | epoxy("{.emph title} ({year}) was directed by {director}.", .data = movie) 12 | epoxy("* {.emph title} ({year}) was directed by {director}.", .data = movies) 13 | 14 | # Inline transformers can be nested 15 | epoxy("I'd be happy to watch {.or {.italic title}}.", .data = movies) 16 | epoxy("They were directed by {.and {.bold director}}.", .data = movies) 17 | 18 | # Learn more about inline transformers in ?epoxy_transform_inline 19 | epoxy("The budget for {.emph title} was {.dollar budget}.", .data = movie) 20 | 21 | # --------- HTML and LaTeX variants --------- 22 | # There are also HTML and LaTeX variants of epoxy. 23 | # Each uses default options that are most natural for the format. 24 | 25 | # epoxy_html() uses `{{ expr }}` for delimiters 26 | epoxy_html("I'd be happy to watch {{ title }}.", .data = movie) 27 | # It also supports an HTML transformer syntax 28 | epoxy_html("I'd be happy to watch {{em.movie-title title}}.", .data = movie) 29 | # Or use the inline transformer syntax, which uses `@` instead of `.` in HTML 30 | epoxy_html("I'd be happy to watch {{@or {{@emph title}} }}.", .data = movies) 31 | 32 | # epoxy_latex() uses `<< expr >>` for delimiters 33 | epoxy_latex("I'd be happy to watch <<.or <<.emph title >> >>.", .data = movies) 34 | -------------------------------------------------------------------------------- /tests/testthat/test-epoxy_mustache.R: -------------------------------------------------------------------------------- 1 | # test_that() 2 | 3 | describe("epoxy_mustache()", { 4 | it("throws for named inputs in ...", { 5 | expect_error(epoxy_mustache(a = 1)) 6 | }) 7 | 8 | it("concatenates multiple lines", { 9 | expect_equal( 10 | epoxy_mustache("one", "two"), 11 | "one\ntwo" 12 | ) 13 | 14 | expect_equal( 15 | epoxy_mustache(!!!c("one", "two")), 16 | "one\ntwo" 17 | ) 18 | }) 19 | 20 | it("renders a template", { 21 | expect_equal( 22 | epoxy_mustache( 23 | "Hello {{name}}!", 24 | .data = list(name = "Chris") 25 | ), 26 | "Hello Chris!" 27 | ) 28 | }) 29 | 30 | it("renders nested templates", { 31 | expect_equal( 32 | epoxy_mustache( 33 | "Hello {{#person}}{{first}} {{last}}{{/person}}!", 34 | .data = list( 35 | person = list( 36 | first = "Nate", 37 | last = "Nickerson" 38 | ) 39 | ) 40 | ), 41 | "Hello Nate Nickerson!" 42 | ) 43 | }) 44 | 45 | it("vectorizes over data frames", { 46 | expect_equal( 47 | epoxy_mustache( 48 | "mpg: {{mpg}}", 49 | .data = mtcars[1:3, ] 50 | ), 51 | paste("mpg:", mtcars$mpg[1:3]) 52 | ) 53 | }) 54 | 55 | it("collapses data frame columns if not vectorized", { 56 | expect_equal( 57 | epoxy_mustache( 58 | "mpg: {{mpg}}", 59 | .data = mtcars[1:3, ], 60 | .vectorized = FALSE 61 | ), 62 | paste("mpg:", paste(mtcars$mpg[1:3], collapse = ",")) 63 | ) 64 | }) 65 | 66 | it("vectorizes over lists", { 67 | expect_equal( 68 | epoxy_mustache( 69 | "{{thing}}", 70 | .data = list(thing = c("one", "two"), other = 3), 71 | .vectorized = TRUE 72 | ), 73 | c("one", "two") 74 | ) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | jobs: 12 | test-coverage: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-pandoc@v2 25 | with: 26 | pandoc-version: 3.1.2 27 | 28 | - uses: r-lib/actions/setup-r-dependencies@v2 29 | with: 30 | extra-packages: any::covr 31 | needs: coverage, rcmdcheck 32 | 33 | - name: Test coverage 34 | run: | 35 | covr::codecov( 36 | quiet = FALSE, 37 | clean = FALSE, 38 | install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") 39 | ) 40 | shell: Rscript {0} 41 | 42 | - name: Show testthat output 43 | if: always() 44 | run: | 45 | ## -------------------------------------------------------------------- 46 | find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true 47 | shell: bash 48 | 49 | - name: Upload test results 50 | if: failure() 51 | uses: actions/upload-artifact@v3 52 | with: 53 | name: coverage-test-failures 54 | path: ${{ runner.temp }}/package 55 | -------------------------------------------------------------------------------- /inst/examples/ui_epoxy_mustache/app.R: -------------------------------------------------------------------------------- 1 | # Generated from example in ui_epoxy_mustache(): do not edit by hand 2 | library(shiny) 3 | library(epoxy) 4 | 5 | ui <- fluidPage( 6 | fluidRow( 7 | style = "max-width: 600px; margin: 0 auto", 8 | column( 9 | width = 6, 10 | ui_epoxy_mustache( 11 | id = "template", 12 | h2(class = "{{heading_class}}", "Hello, {{name}}!"), 13 | "{{#favorites}}", 14 | p("Your favorite fruits are..."), 15 | tags$ul(HTML("{{#fruits}}
  • {{.}}
  • {{/fruits}}")), 16 | "{{/favorites}}", 17 | "{{^favorites}}

    Do you have any favorite fruits?

    {{/favorites}}" 18 | ) 19 | ), 20 | column( 21 | width = 6, 22 | h2("Inputs"), 23 | textInput("name", "Your name"), 24 | textInput("fruits", "Favorite fruits", placeholder = "apple, banana"), 25 | helpText("Enter a comma-separated list of fruits.") 26 | ) 27 | ) 28 | ) 29 | 30 | server <- function(input, output, session) { 31 | user_name <- reactive({ 32 | if (!nzchar(input$name)) return("user") 33 | input$name 34 | }) 35 | 36 | favorites <- reactive({ 37 | if (identical(input$fruits, "123456")) { 38 | # Errors are equivalent to "empty" values, 39 | # the rest of the template will still render. 40 | stop("Bad fruits, bad!") 41 | } 42 | 43 | if (!nzchar(input$fruits)) return(NULL) 44 | list(fruits = strsplit(input$fruits, "\\s*,\\s*")[[1]]) 45 | }) 46 | 47 | output$template <- render_epoxy( 48 | name = user_name(), 49 | heading_class = if (user_name() != "user") "text-success", 50 | favorites = favorites() 51 | ) 52 | } 53 | 54 | shiny::shinyApp(ui, server) 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---- Default .gitignore From grkmisc ---- 2 | .Rproj.user 3 | .Rhistory 4 | .RData 5 | .DS_Store 6 | README.html 7 | 8 | # vscode 9 | .history/ 10 | 11 | # Directories that start with _ 12 | _*/ 13 | !tests/testthat/_snaps 14 | 15 | ## https://github.com/github/gitignore/blob/master/R.gitignore 16 | # History files 17 | .Rhistory 18 | .Rapp.history 19 | 20 | # Session Data files 21 | .RData 22 | 23 | # Example code in package build process 24 | *-Ex.R 25 | 26 | # Output files from R CMD build 27 | /*.tar.gz 28 | 29 | # Output files from R CMD check 30 | /*.Rcheck/ 31 | 32 | # RStudio files 33 | .Rproj.user/ 34 | 35 | # produced vignettes 36 | vignettes/*.html 37 | vignettes/*.pdf 38 | 39 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 40 | .httr-oauth 41 | 42 | # knitr and R markdown default cache directories 43 | /*_cache/ 44 | /cache/ 45 | 46 | # Temporary files created by R markdown 47 | *.utf8.md 48 | *.knit.md 49 | 50 | # Shiny token, see https://shiny.rstudio.com/articles/shinyapps.html 51 | rsconnect/ 52 | 53 | ## https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 54 | # General 55 | .DS_Store 56 | .AppleDouble 57 | .LSOverride 58 | 59 | # Icon must end with two \r 60 | Icon 61 | 62 | 63 | # Thumbnails 64 | ._* 65 | 66 | # Files that might appear in the root of a volume 67 | .DocumentRevisions-V100 68 | .fseventsd 69 | .Spotlight-V100 70 | .TemporaryItems 71 | .Trashes 72 | .VolumeIcon.icns 73 | .com.apple.timemachine.donotpresent 74 | 75 | # Directories potentially created on remote AFP share 76 | .AppleDB 77 | .AppleDesktop 78 | Network Trash Folder 79 | Temporary Items 80 | .apdisk 81 | inst/doc 82 | docs/ 83 | node_modules 84 | pkgdown/index.html 85 | -------------------------------------------------------------------------------- /inst/examples/ui_epoxy_html/app.R: -------------------------------------------------------------------------------- 1 | # Generated from example in ui_epoxy_html(): do not edit by hand 2 | library(shiny) 3 | library(epoxy) 4 | 5 | ui <- fluidPage( 6 | h2("ui_epoxy_html demo"), 7 | ui_epoxy_html( 8 | .id = "example", 9 | .item_class = "inner", 10 | fluidRow( 11 | tags$div( 12 | class = "col-xs-4", 13 | selectInput( 14 | inputId = "thing", 15 | label = "What is this {{color}} thing?", 16 | choices = c("apple", "banana", "coconut", "dolphin") 17 | ) 18 | ), 19 | tags$div( 20 | class = "col-xs-4", 21 | selectInput( 22 | inputId = "color", 23 | label = "What color is the {{thing}}?", 24 | c("red", "blue", "black", "green", "yellow") 25 | ) 26 | ), 27 | tags$div( 28 | class = "col-xs-4", 29 | sliderInput( 30 | inputId = "height", 31 | label = "How tall is the {{color}} {{thing}}?", 32 | value = 5, 33 | min = 0, 34 | max = 10, 35 | step = 0.1, 36 | post = "ft" 37 | ) 38 | ) 39 | ), 40 | tags$p(class = "big", "The {{color}} {{thing}} is {{height}} feet tall."), 41 | # Default values for placeholders above. 42 | thing = "THING", 43 | color = "COLOR", 44 | height = "HEIGHT" 45 | ), 46 | tags$style(HTML( 47 | ".big { font-size: 1.5em; } 48 | .inner { background-color: rgba(254, 233, 105, 0.5);} 49 | .epoxy-item__placeholder { color: #999999; background-color: unset; }" 50 | )) 51 | ) 52 | 53 | server <- function(input, output, session) { 54 | output$example <- render_epoxy( 55 | thing = input$thing, 56 | color = input$color, 57 | height = input$height 58 | ) 59 | } 60 | 61 | shinyApp(ui, server) 62 | -------------------------------------------------------------------------------- /.github/workflows/check-standard.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/master/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master, "rc-*"] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macOS-latest, r: 'release'} 22 | - {os: windows-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 24 | - {os: ubuntu-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'oldrel-1'} 26 | 27 | env: 28 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 29 | R_KEEP_PKG_SOURCE: yes 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | 34 | - uses: r-lib/actions/setup-pandoc@v2 35 | with: 36 | pandoc-version: 3.1.2 37 | 38 | - uses: r-lib/actions/setup-r@v2 39 | with: 40 | r-version: ${{ matrix.config.r }} 41 | http-user-agent: ${{ matrix.config.http-user-agent }} 42 | use-public-rspm: true 43 | 44 | - uses: r-lib/actions/setup-r-dependencies@v2 45 | with: 46 | extra-packages: rcmdcheck 47 | 48 | - uses: r-lib/actions/check-r-package@v2 49 | 50 | - name: Update gadenbuie/status dashboard 51 | uses: gadenbuie/status/actions/status-update-rcmdcheck@main 52 | with: 53 | github-token-repo-scope: ${{ secrets.GADENBUIE_STATUS_TOKEN }} 54 | status-repo: gadenbuie/status 55 | 56 | -------------------------------------------------------------------------------- /R/bechdel.R: -------------------------------------------------------------------------------- 1 | #' Top 10 Highest-Rated, Bechdel-Passing Movies 2 | #' 3 | #' A small dataset for \pkg{epoxy} demonstrations with the top audience-rated 4 | #' movies that pass the [Bechdel Test](https://bechdeltest.com). 5 | #' 6 | #' @format A data frame with 10 rows and 18 variables: 7 | #' \describe{ 8 | #' \item{imdb_id}{IMDB Movie ID} 9 | #' \item{bechdel_rating}{Rating (0-3): 0 = unscored; 1 = It has to have at least two (named) women in it; 2 = Who talk to each other; 3 = About something besides a man.} 10 | #' \item{year}{Year} 11 | #' \item{title}{Title of movie} 12 | #' \item{budget}{Budget in $USD as of release year} 13 | #' \item{domgross}{Domestic gross in $USD in release year} 14 | #' \item{intgross}{International gross in $USD in release year} 15 | #' \item{plot}{Plot of the movie} 16 | #' \item{rated}{Moving rating, e.g. PG, PG-13, R, etc.} 17 | #' \item{language}{Language of the movie} 18 | #' \item{country}{Country where the movie was produced} 19 | #' \item{imdb_rating}{IMDB rating of the movie, 0-10} 20 | #' \item{director}{Director of the movie} 21 | #' \item{actors}{Major actors appearing in the movie} 22 | #' \item{genre}{Genre} 23 | #' \item{awards}{Awards won by the movie, text description} 24 | #' \item{runtime}{Movie runtime in minutes} 25 | #' \item{poster}{URL of movie poster image, sourced from [themoviedb.org](https://www.themoviedb.org). Poster images URLs ar provided from the TMDB API but \pkg{epoxy} is not endorsed or certified by TMDB.} 26 | #' } 27 | #' @source [TidyTuesday (2021-03-09)](https://github.com/rfordatascience/tidytuesday/blob/044e769/data/2021/2021-03-09/readme.md), 28 | #' [FiveThirtyEight](https://github.com/fivethirtyeight/data/tree/master/bechdel), 29 | #' [bechdeltest.com](https://bechdeltest.com/), 30 | #' [themoviedb.org](https://www.themoviedb.org) 31 | "bechdel" 32 | -------------------------------------------------------------------------------- /inst/examples/word-list/www/animation.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --animate-delay-base: 150ms; 3 | --animate-duration: 500ms; 4 | } 5 | 6 | .animate { 7 | animation-duration: var(--animate-duration); 8 | animation-delay: calc((var(--delay) - 1) * var(--animate-delay-base)); 9 | animation-name: animate-fade; 10 | animation-timing-function: cubic-bezier(.26, .53, .74, 1.48); 11 | animation-fill-mode: backwards; 12 | } 13 | 14 | .animate:nth-child(1) { --delay: 1; } 15 | .animate:nth-child(2) { --delay: 2; } 16 | .animate:nth-child(3) { --delay: 3; } 17 | .animate:nth-child(4) { --delay: 4; } 18 | .animate:nth-child(5) { --delay: 5; } 19 | .animate:nth-child(6) { --delay: 6; } 20 | .animate:nth-child(7) { --delay: 7; } 21 | .animate:nth-child(8) { --delay: 8; } 22 | .animate:nth-child(9) { --delay: 9; } 23 | .animate:nth-child(10) { --delay: 10; } 24 | .animate:nth-child(11) { --delay: 11; } 25 | .animate:nth-child(12) { --delay: 12; } 26 | .animate:nth-child(13) { --delay: 13; } 27 | .animate:nth-child(14) { --delay: 14; } 28 | .animate:nth-child(15) { --delay: 15; } 29 | .animate:nth-child(16) { --delay: 16; } 30 | .animate:nth-child(17) { --delay: 17; } 31 | .animate:nth-child(18) { --delay: 18; } 32 | .animate:nth-child(19) { --delay: 19; } 33 | .animate:nth-child(20) { --delay: 20; } 34 | 35 | /* Pop In */ 36 | .animate.pop { animation-name: animate-pop; } 37 | 38 | @keyframes animate-pop { 39 | 0% { 40 | opacity: 0; 41 | transform: scale(0.5, 0.5); 42 | } 43 | 100% { 44 | opacity: 1; 45 | transform: scale(1, 1); 46 | } 47 | } 48 | 49 | /* Blur In */ 50 | .animate.blur { 51 | animation-name: animate-blur; 52 | animation-timing-function: ease; 53 | } 54 | 55 | @keyframes animate-blur { 56 | 0% { 57 | opacity: 0; 58 | filter: blur(15px); 59 | } 60 | 100% { 61 | opacity: 1; 62 | filter: blur(0px); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /man/fragments/epoxy-lead.Rmd: -------------------------------------------------------------------------------- 1 | ```{r include = FALSE} 2 | rel_or_abs_url <- function(path) { 3 | rel_path <- knitr::opts_chunk$get("use_rel_path") 4 | if (is.null(rel_path)) rel_path <- FALSE 5 | paste0( 6 | if (rel_path) "articles/", 7 | if (!rel_path) "https://pkg.garrickadenbuie.com/epoxy/articles/", 8 | path 9 | ) 10 | } 11 | ``` 12 | 13 | [glue]: https://glue.tidyverse.org 14 | [shiny]: https://shiny.posit.co/ 15 | [rmarkdown]: https://rmarkdown.rstudio.com 16 | [quarto]: https://quarto.org 17 | [mustache]: https://mustache.github.io 18 | [epoxy-report]: `r rel_or_abs_url("epoxy-report.html")` 19 | [epoxy-script]: `r rel_or_abs_url("epoxy-script.html")` 20 | [epoxy-shiny]: `r rel_or_abs_url("epoxy-shiny.html")` 21 | 22 | ## epoxy is super glue 23 | 24 | ### [In R Markdown and Quarto reports][epoxy-report] 25 | 26 | Use `epoxy` chunks for extra-strength inline syntax. 27 | Just `library(epoxy)` 28 | in your [R Markdown][rmarkdown] or [Quarto][quarto] 29 | document to get started. 30 | All epoxy chunks make it easy to transform values in place 31 | with a `{cli}`-inspired inline syntax 32 | described in `?epoxy_transform_inline`. 33 | 34 | ### [In R scripts][epoxy-script] 35 | 36 | The same functions that power epoxy chunks are availble in three flavors: 37 | 38 | * `epoxy()` for markdown and general purpose outputs 39 | 40 | * `epoxy_html()` for HTML outputs, with added support for HTML templating 41 | (see `?epoxy_transform_html`) 42 | 43 | * `epoxy_latex()` for LaTeX reports 44 | 45 | These functions are accompanied by 46 | a robust system for chained glue-transformers 47 | powered by `epoxy_transform()`. 48 | 49 | ### [In Shiny apps][epoxy-shiny] 50 | 51 | `ui_epoxy_html()` makes it easy to update text or HTML dynamically, 52 | anywhere in your [Shiny][shiny] app's UI. 53 | For more complicated situations, 54 | `ui_epoxy_mustache()` lets you turn any Shiny UI 55 | into a template that leverages the [Mustache templating language][mustache]. 56 | -------------------------------------------------------------------------------- /pkgdown/_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: http://pkg.garrickadenbuie.com/epoxy 2 | 3 | template: 4 | package: grkgdown 5 | 6 | development: 7 | mode: auto 8 | 9 | navbar: 10 | components: 11 | github: 12 | icon: fab fa-github fa-lg 13 | href: https://github.com/gadenbuie/epoxy 14 | 15 | home: 16 | links: 17 | - text: Help and discussions 18 | href: https://github.com/gadenbuie/epoxy/discussions 19 | 20 | articles: 21 | - title: Get Started 22 | desc: ~ 23 | contents: 24 | - '`epoxy`' 25 | - '`epoxy-report`' 26 | - '`epoxy-shiny`' 27 | - '`epoxy-script`' 28 | - title: Articles 29 | desc: ~ 30 | navbar: ~ 31 | contents: 32 | - '`inline-reporting`' 33 | 34 | reference: 35 | - title: epoxy, super-glue wrappers 36 | contents: 37 | - epoxy 38 | - use_epoxy_knitr_engines 39 | - title: epoxy Transformers 40 | contents: 41 | - epoxy_transform 42 | - epoxy_transform_inline 43 | - epoxy_transform_html 44 | - epoxy_transform_one_shot 45 | - engine_pick 46 | - title: Reuse epoxy Templates 47 | contents: 48 | - epoxy_use_chunk 49 | - title: Templating for Shiny 50 | contents: 51 | - ui_epoxy_html 52 | - ui_epoxy_markdown 53 | - render_epoxy 54 | - run_epoxy_example_app 55 | - title: Mustache-style Templating 56 | description: > 57 | Sometimes you need just a little bit more templating power than `epoxy()` or 58 | `{glue}` can provide. The [mustache templating 59 | language](https://mustache.github.io/) is a simple, popular, logic-less 60 | templating language. Consider using mustache when your template uses nested 61 | data structures or conditionally included content, but doesn't require any 62 | inline formatting. 63 | contents: 64 | - epoxy_mustache 65 | - ui_epoxy_mustache 66 | - title: Example Datasets 67 | description: | 68 | Interesting datasets that can help you explore, learn, and practice using epoxy. 69 | contents: 70 | - bechdel 71 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | `%||%` <- function(x, y) { 2 | if (is.null(x)) y else x 3 | } 4 | 5 | str_extract <- function(text, pattern) { 6 | m <- regexpr(pattern, text) 7 | x <- regmatches(text, m, invert = NA) 8 | x <- vapply(x, `[`, 2L, FUN.VALUE = character(1)) 9 | x[is.na(x)] <- "" 10 | x 11 | } 12 | 13 | str_extract_all <- function(text, pattern) { 14 | m <- gregexpr(pattern, text, perl = TRUE) 15 | regmatches(text, m) 16 | } 17 | 18 | str_count <- function(text, pattern) { 19 | m <- gregexpr(pattern, text, perl = TRUE) 20 | x <- regmatches(text, m) 21 | vapply(x, length, integer(1)) 22 | } 23 | 24 | is_htmlish_output <- function(exclude = NULL) { # nocov start 25 | if (isTRUE(rmarkdown::metadata$always_allow_html)) { 26 | return(TRUE) 27 | } 28 | 29 | fmt <- knitr::opts_knit$get("rmarkdown.pandoc.to") 30 | 31 | if (is.null(fmt) || !nzchar(fmt)) { 32 | return(TRUE) 33 | } 34 | 35 | fmt <- sub("[+-].+$", "", fmt) 36 | if (grepl("^markdown", fmt)) { 37 | fmt <- "markdown" 38 | } 39 | 40 | fmt_htmlish <- c( 41 | "markdown", "gfm", 42 | "epub", "epub2", "epub3", 43 | "html", "html4", "html5", 44 | "revealjs", "s5", "slideous", "slidy" 45 | ) 46 | fmt_htmlish <- setdiff(fmt_htmlish, exclude) 47 | fmt %in% fmt_htmlish 48 | } # nocov end 49 | 50 | is_tag <- function(x) inherits(x, c("shiny.tag", "shiny.tag.list", "html")) 51 | 52 | with_options <- function(opts, expr) { 53 | old <- options(opts) 54 | on.exit(options(old)) 55 | force(expr) 56 | } 57 | 58 | escape_html <- function(x) { 59 | htmltools::htmlEscape(x, attribute = FALSE) 60 | } 61 | 62 | list_split_named <- function(l) { 63 | if (is.null(names(l))) { 64 | return(list(unnamed = l, named = NULL)) 65 | } 66 | 67 | list( 68 | unnamed = l[!nzchar(names(l))], 69 | named = l[nzchar(names(l))] 70 | ) 71 | } 72 | 73 | discard_null <- function(x) { 74 | x[!vapply(x, is.null, logical(1))] 75 | } 76 | 77 | as_glue_chr <- function(x) { 78 | class(x) <- c("glue", class(x)) 79 | x 80 | } 81 | -------------------------------------------------------------------------------- /man/bechdel.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/bechdel.R 3 | \docType{data} 4 | \name{bechdel} 5 | \alias{bechdel} 6 | \title{Top 10 Highest-Rated, Bechdel-Passing Movies} 7 | \format{ 8 | A data frame with 10 rows and 18 variables: 9 | \describe{ 10 | \item{imdb_id}{IMDB Movie ID} 11 | \item{bechdel_rating}{Rating (0-3): 0 = unscored; 1 = It has to have at least two (named) women in it; 2 = Who talk to each other; 3 = About something besides a man.} 12 | \item{year}{Year} 13 | \item{title}{Title of movie} 14 | \item{budget}{Budget in $USD as of release year} 15 | \item{domgross}{Domestic gross in $USD in release year} 16 | \item{intgross}{International gross in $USD in release year} 17 | \item{plot}{Plot of the movie} 18 | \item{rated}{Moving rating, e.g. PG, PG-13, R, etc.} 19 | \item{language}{Language of the movie} 20 | \item{country}{Country where the movie was produced} 21 | \item{imdb_rating}{IMDB rating of the movie, 0-10} 22 | \item{director}{Director of the movie} 23 | \item{actors}{Major actors appearing in the movie} 24 | \item{genre}{Genre} 25 | \item{awards}{Awards won by the movie, text description} 26 | \item{runtime}{Movie runtime in minutes} 27 | \item{poster}{URL of movie poster image, sourced from \href{https://www.themoviedb.org}{themoviedb.org}. Poster images URLs ar provided from the TMDB API but \pkg{epoxy} is not endorsed or certified by TMDB.} 28 | } 29 | } 30 | \source{ 31 | \href{https://github.com/rfordatascience/tidytuesday/blob/044e769/data/2021/2021-03-09/readme.md}{TidyTuesday (2021-03-09)}, 32 | \href{https://github.com/fivethirtyeight/data/tree/master/bechdel}{FiveThirtyEight}, 33 | \href{https://bechdeltest.com/}{bechdeltest.com}, 34 | \href{https://www.themoviedb.org}{themoviedb.org} 35 | } 36 | \usage{ 37 | bechdel 38 | } 39 | \description{ 40 | A small dataset for \pkg{epoxy} demonstrations with the top audience-rated 41 | movies that pass the \href{https://bechdeltest.com}{Bechdel Test}. 42 | } 43 | \keyword{datasets} 44 | -------------------------------------------------------------------------------- /inst/examples/ui_epoxy_markdown/app.R: -------------------------------------------------------------------------------- 1 | # Generated from example in ui_epoxy_markdown(): do not edit by hand 2 | library(shiny) 3 | library(epoxy) 4 | 5 | # Shiny epoxy template functions don't support inline transformations, 6 | # so we still have to do some prep work ourselves. 7 | bechdel <- epoxy::bechdel 8 | 9 | as_dollars <- scales::label_dollar( 10 | scale_cut = scales::cut_short_scale() 11 | ) 12 | bechdel$budget <- as_dollars(bechdel$budget) 13 | bechdel$domgross <- as_dollars(bechdel$domgross) 14 | 15 | vowels <- c("a", "e", "i", "o", "u") 16 | bechdel$genre <- paste( 17 | ifelse(substr(tolower(bechdel$genre), 1, 1) %in% vowels, "an", "a"), 18 | tolower(bechdel$genre) 19 | ) 20 | 21 | movie_ids <- rlang::set_names( 22 | bechdel$imdb_id, 23 | bechdel$title 24 | ) 25 | 26 | ui <- fixedPage( 27 | fluidRow( 28 | column( 29 | width = 3, 30 | selectInput("movie", "Movie", movie_ids), 31 | uiOutput("poster") 32 | ), 33 | column( 34 | width = 9, 35 | ui_epoxy_markdown( 36 | .id = "about_movie", 37 | " 38 | ## {{title}} 39 | 40 | **Released:** {{ year }} \\ 41 | **Rated:** {{ rated }} \\ 42 | **IMDB Rating:** {{ imdb_rating }} 43 | 44 | _{{ title }}_ is {{ genre }} film released in {{ year }}. 45 | It was filmed in {{ country }} with a budget of {{ budget }} 46 | and made {{ domgross }} at the box office. 47 | _{{ title }}_ recieved a Bechdel rating of **{{ bechdel_rating }}** 48 | for the following plot: 49 | 50 | > {{ plot }} 51 | " 52 | ) 53 | ) 54 | ) 55 | ) 56 | 57 | server <- function(input, output, session) { 58 | movie <- reactive({ 59 | bechdel[bechdel$imdb_id == input$movie, ] 60 | }) 61 | 62 | output$about_movie <- render_epoxy(.list = movie()) 63 | output$poster <- renderUI( 64 | img( 65 | src = movie()$poster, 66 | alt = paste0("Poster for ", movie()$title), 67 | style = "max-height: 400px; max-width: 100%; margin: 0 auto; display: block;" 68 | ) 69 | ) 70 | } 71 | 72 | shinyApp(ui, server) 73 | -------------------------------------------------------------------------------- /vignettes/epoxy.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "epoxy" 3 | output: 4 | cleanrmd::html_document_clean: 5 | theme: holiday 6 | toc: false 7 | --- 8 | 9 | ```{r, include = FALSE} 10 | knitr::opts_chunk$set( 11 | collapse = TRUE, 12 | comment = "#>" 13 | ) 14 | ``` 15 | 16 | ```{css echo=FALSE} 17 | .row > main { 18 | width: 100% !important; 19 | margin-left: auto; 20 | margin-right: auto; 21 | } 22 | .card h3.card-title { 23 | margin-top: 0; 24 | } 25 | ``` 26 | 27 | ## Use epoxy 28 | 29 | ```{=html} 30 |
    31 |
    32 | 33 |
    34 | 41 |
    42 | 43 |
    44 | 51 |
    52 | 53 |
    54 | 61 |
    62 |
    63 |
    64 | ``` 65 | -------------------------------------------------------------------------------- /man/fragments/example-airbnb.Rmd: -------------------------------------------------------------------------------- 1 | ```{r airbnb-data, echo=FALSE} 2 | # Airbnb stats that may not be completely accurate 3 | airbnb <- list( 4 | countries = 220, 5 | cities = 1e5, 6 | avg_stay = 4.326, 7 | avg_cost = 184.952, 8 | hosts = 4.12e6 9 | ) 10 | ``` 11 | 12 | ````markdown 13 | ```{r}`r ''` 14 | `r paste(knitr::knit_code$get("airbnb-data"), collapse = "\n")` 15 | ``` 16 | ```` 17 | 18 | With standard inline R code in R Markdown, we can write the following: 19 | 20 | ```{verbatim} 21 | * Airbnb includes listings in `r airbnb$cities` cities 22 | in `r airbnb$countries` countries 23 | from around `r airbnb$hosts` hosts. 24 | The average guest stays `r airbnb$avg_stay` nights 25 | at a rate of `r airbnb$avg_cost` per night. 26 | ``` 27 | 28 | * Airbnb includes listings in 10^5 cities 29 | in `r I(airbnb$countries)` countries 30 | from around 4.12 × 10^6 hosts. 31 | The average guest stays `r airbnb$avg_stay` nights 32 | at a rate of `r airbnb$avg_cost` per night. 33 | 34 | Using `epoxy` and the light-weight `fmt()` function from `epoxy_transform_format()`, 35 | we can improve the readability and formatting of the interwoven numbers. 36 | 37 | 38 | ````markdown 39 | ```{r my_style, echo = FALSE}`r ''` 40 | `r paste(knitr::knit_code$get("my-style"), collapse = "\n")` 41 | ``` 42 | 43 | ```{epoxy .transformer = my_style}`r ''` 44 | `r paste(knitr::knit_code$get("airbnb-epoxy"), collapse = "\n")` 45 | ``` 46 | ```` 47 | 48 | ```{r my-style, echo = FALSE} 49 | # Define number/dollar format to apply consistently 50 | my_style <- epoxy_transform_format( 51 | dollar = scales::label_dollar(accuracy = 1), 52 | number = scales::label_number( 53 | accuracy = 0.1, 54 | scale_cut = scales::cut_short_scale() 55 | ) 56 | ) 57 | ``` 58 | 59 | ```{epoxy airbnb-epoxy, .transformer = my_style} 60 | * Airbnb includes listings in {fmt(airbnb$cities, ",")} cities 61 | in {fmt(airbnb$countries, "auto")} countries 62 | from around {fmt(airbnb$hosts, "#")} hosts. 63 | The average guest stays {fmt(airbnb$avg_stay, "#")} nights 64 | at a rate of {fmt(airbnb$avg_cost, "$")} per night. 65 | ``` 66 | -------------------------------------------------------------------------------- /man/fragments/transformers-epoxy_transform_set.Rmd: -------------------------------------------------------------------------------- 1 | To change the transformer used by [epoxy()] and the HTML and LaTeX variants, use `epoxy_transform_set()`. 2 | This function takes the same values as [epoxy_transform()], but makes them the default transformer for any [epoxy()] calls that do not specify a transformer. 3 | By default, the setting is made for all engines, but you can specify a single engine with the `engine` argument. 4 | 5 | Here's a small example that applies the [bold][epoxy_transform_bold] and [collapse][epoxy_transform_collapse] transformers to all epoxy chunks: 6 | 7 | ```{r eval = FALSE} 8 | epoxy_transform_set("bold", "collapse") 9 | ``` 10 | 11 | Most often, you'll want to to update the default transformer to customize the formatting functions used by the [inline transformer][epoxy_transform_inline]. 12 | You can use `epoxy_transform_set()` to change settings of existing formatting functions or to add new one. 13 | Pass the new function to an argument with the dot-prefixed name. 14 | 15 | In the next example I'm setting the `.dollar` transformation to use "K" and "M" to abbreviate large numbers. 16 | I'm also adding my own transformation that truncates long strings to fit in 8 characters. 17 | 18 | ```{r eval = FALSE} 19 | epoxy_transform_set( 20 | .dollar = scales::label_dollar( 21 | accuracy = 0.01, 22 | scale_cut = scales::cut_short_scale() 23 | ), 24 | .trunc8 = function(x) glue::glue_collapse(x, width = 8) 25 | ) 26 | 27 | epoxy("{.dollar 12345678}") 28 | #> $12.34M 29 | epoxy("{.trunc8 12345678}") 30 | #> 12345... 31 | ``` 32 | 33 | Note that the `engine` argument can be used even with inline tranformations, e.g. to apply a change only for HTML you can use `engine = "html"`. 34 | 35 | To unset the session defaults, you have two options: 36 | 37 | 1. Unset everything by passing `NULL` to `epoxy_transform_set()`: 38 | 39 | ```{r eval = FALSE} 40 | epoxy_transform_set(NULL) 41 | ``` 42 | 43 | 1. Unset a single inline transformation by passing `rlang::zap()` to the named argument: 44 | 45 | ```{r eval = FALSE} 46 | epoxy_transform_set(.dollar = rlang::zap()) 47 | ``` 48 | 49 | Or you can provide new values to overwrite the current settings. 50 | And as before, you can unset session defaults for a specific `engine`. 51 | -------------------------------------------------------------------------------- /R/utils-knitr.R: -------------------------------------------------------------------------------- 1 | knitr_current_label <- function() { 2 | if (isTRUE(.globals$inline_chunk)) { 3 | return("___inline_chunk___") 4 | } 5 | 6 | knitr::opts_current$get("label") 7 | } 8 | 9 | knitr_chunk_get <- function(label = knitr_current_label()) { 10 | chunk <- knitr::knit_code$get(label) 11 | list( 12 | code = paste(c(chunk), collapse = "\n"), 13 | opts = knitr_chunk_specific_options(label) 14 | ) 15 | } 16 | 17 | knitr_chunk_specific_options <- function(label = knitr_current_label()) { 18 | if (identical(label, "___inline_chunk___")) { 19 | return(NULL) 20 | } 21 | 22 | chunk <- knitr::knit_code$get(label) 23 | if (is.null(chunk)) return(NULL) 24 | 25 | opts <- attr(chunk, "chunk_opts") 26 | 27 | lapply(opts, function(opt) { 28 | if (!(rlang::is_symbol(opt) || rlang::is_call(opt))) { 29 | return(opt) 30 | } 31 | rlang::eval_bare(opt, env = knitr::knit_global()) 32 | }) 33 | } 34 | 35 | # knitr doesn't have a way of detecting when code is being evaluated inside an 36 | # inline code chunk. And worse, the inline chunk "inherits" options from the 37 | # previous chunk -- or at least `opts_current` returns the previous chunk's 38 | # options. This inline chunk detector could probably be built into knitr in some 39 | # way: https://github.com/yihui/knitr/issues/1988 40 | # Prior to knitr 1.44 we could use `opts_current$set()` to set an inline chunk 41 | # option, but modifying the current chunk options will now throw an error, 42 | # see: https://github.com/yihui/knitr/issues/1798 43 | # nocov start 44 | knitr_register_detect_inline <- function() { 45 | if ("...detect_inline_chunk" %in% knitr::opts_chunk$get()) { 46 | # We've already registered the global option we hook into 47 | return() 48 | } 49 | 50 | # We key off this chunk options to always set inline chunk status 51 | knitr::opts_chunk$set(...detect_inline_chunks = TRUE) 52 | 53 | # Set `...inline_chunk` chunk option to FALSE when entering any 54 | # regular code chunk, or TRUE when exiting the chunk 55 | knitr::knit_hooks$set( 56 | ...detect_inline_chunks = knitr_hook_detect_inline_chunk 57 | ) 58 | } 59 | # nocov end 60 | 61 | knitr_hook_detect_inline_chunk <- function(before, ...) { 62 | # Set to FALSE inside a code chunk, reset to TRUE outside 63 | .globals$inline_chunk <- !before 64 | } 65 | 66 | knitr_is_inline_chunk <- function() { 67 | .globals$inline_chunk %||% 68 | is.null(knitr::opts_current$get("label")) 69 | } 70 | -------------------------------------------------------------------------------- /man/epoxy_style.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deprecated.R 3 | \name{epoxy_style} 4 | \alias{epoxy_style} 5 | \alias{epoxy_style_apply} 6 | \alias{epoxy_style_bold} 7 | \alias{epoxy_style_code} 8 | \alias{epoxy_style_collapse} 9 | \alias{epoxy_style_get} 10 | \alias{epoxy_style_html} 11 | \alias{epoxy_style_inline} 12 | \alias{epoxy_style_italic} 13 | \alias{epoxy_style_set} 14 | \alias{epoxy_style_wrap} 15 | \title{Deprecated: \code{epoxy_style()}} 16 | \usage{ 17 | epoxy_style(...) 18 | 19 | epoxy_style_apply(...) 20 | 21 | epoxy_style_bold(...) 22 | 23 | epoxy_style_code(...) 24 | 25 | epoxy_style_collapse(...) 26 | 27 | epoxy_style_get(...) 28 | 29 | epoxy_style_html(...) 30 | 31 | epoxy_style_inline(...) 32 | 33 | epoxy_style_italic(...) 34 | 35 | epoxy_style_set(...) 36 | 37 | epoxy_style_wrap(...) 38 | } 39 | \arguments{ 40 | \item{...}{Passed to the new \code{epoxy_transform} function.} 41 | } 42 | \value{ 43 | Returns the result of the new \code{epoxy_transform} function. 44 | } 45 | \description{ 46 | \code{epoxy_style()} was renamed \code{\link[=epoxy_transform]{epoxy_transform()}} in \pkg{epoxy} version 0.1.0. 47 | } 48 | \section{Functions}{ 49 | \itemize{ 50 | \item \code{epoxy_style()}: is now \code{\link[=epoxy_transform]{epoxy_transform()}}. 51 | 52 | \item \code{epoxy_style_apply()}: is now \code{\link[=epoxy_transform_apply]{epoxy_transform_apply()}}. 53 | 54 | \item \code{epoxy_style_bold()}: is now \code{\link[=epoxy_transform_bold]{epoxy_transform_bold()}}. 55 | 56 | \item \code{epoxy_style_code()}: is now \code{\link[=epoxy_transform_code]{epoxy_transform_code()}}. 57 | 58 | \item \code{epoxy_style_collapse()}: is now \code{\link[=epoxy_transform_collapse]{epoxy_transform_collapse()}}. 59 | 60 | \item \code{epoxy_style_get()}: is now \code{\link[=epoxy_transform_get]{epoxy_transform_get()}}. 61 | 62 | \item \code{epoxy_style_html()}: is now \code{\link[=epoxy_transform_html]{epoxy_transform_html()}}. 63 | 64 | \item \code{epoxy_style_inline()}: is now \code{\link[=epoxy_transform_inline]{epoxy_transform_inline()}}. 65 | 66 | \item \code{epoxy_style_italic()}: is now \code{\link[=epoxy_transform_italic]{epoxy_transform_italic()}}. 67 | 68 | \item \code{epoxy_style_set()}: is now \code{\link[=epoxy_transform_set]{epoxy_transform_set()}}. 69 | 70 | \item \code{epoxy_style_wrap()}: is now \code{\link[=epoxy_transform_wrap]{epoxy_transform_wrap()}}. 71 | 72 | }} 73 | \keyword{internal} 74 | -------------------------------------------------------------------------------- /inst/lib/NOTICE: -------------------------------------------------------------------------------- 1 | # mustache.js ---------------------------------------------------------------- # 2 | The MIT License 3 | 4 | Copyright (c) 2009 Chris Wanstrath (Ruby) 5 | Copyright (c) 2010-2014 Jan Lehnardt (JavaScript) 6 | Copyright (c) 2010-2015 The mustache.js community 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | 14 | # hint.css ------------------------------------------------------------------- # 15 | MIT License 16 | 17 | Copyright (c) 2021 Kushagra Gour 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in all 27 | copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | -------------------------------------------------------------------------------- /data-raw/bechdel.R: -------------------------------------------------------------------------------- 1 | library(dplyr) 2 | library(httr2) 3 | 4 | # ## Bechdel Test from tidytuesday 5 | # https://github.com/rfordatascience/tidytuesday/blob/master/data/2021/2021-03-09/readme.md 6 | raw_bechdel <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-03-09/raw_bechdel.csv') 7 | movies <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-03-09/movies.csv') 8 | 9 | set.seed(424042) 10 | top_movies <- 11 | raw_bechdel %>% 12 | rename(bechdel_rating = rating) %>% 13 | left_join( 14 | movies %>% select(imdb_id, imdb_rating, rated), 15 | by = "imdb_id" 16 | ) %>% 17 | filter(rated %in% c("N/A", "G", "PG", "PG-13")) %>% 18 | arrange(desc(bechdel_rating), desc(imdb_rating)) %>% 19 | filter(imdb_rating > 6, bechdel_rating == 3) %>% 20 | group_by(imdb_rating) %>% 21 | slice_sample(n = 2, replace = TRUE) %>% 22 | ungroup() %>% 23 | distinct() %>% 24 | slice_sample(n = 10) %>% 25 | arrange(desc(imdb_rating)) %>% 26 | select(imdb_id, bechdel_rating) 27 | 28 | 29 | get_tmdb_movie <- function(title, year) { 30 | res <- 31 | request("https://api.themoviedb.org/3") %>% 32 | req_url_path_append("search", "movie") %>% 33 | req_url_query(api_key = keyring::key_get("tmdb_api_key_v3")) %>% 34 | req_url_query(query = title, year = year) %>% 35 | req_perform() %>% 36 | resp_body_json() 37 | 38 | if (is.null(res$results)) { 39 | return(NA_character_) 40 | } 41 | 42 | poster_path <- res$results[[1]]$poster_path 43 | as.character(glue::glue("https://image.tmdb.org/t/p/w500{poster_path}")) 44 | } 45 | 46 | bechdel <- 47 | movies %>% 48 | select( 49 | imdb_id, 50 | year, 51 | title, 52 | budget:intgross, 53 | plot, 54 | rated, 55 | language, 56 | country, 57 | imdb_rating, 58 | director, 59 | actors, 60 | genre, 61 | awards, 62 | runtime 63 | ) %>% 64 | inner_join(top_movies, ., by = "imdb_id") %>% 65 | mutate( 66 | across(where(is.character), ~ if_else(.x == "N/A", "", .x)), 67 | across(budget:intgross, as.numeric), 68 | runtime = as.integer(sub(" min$", "", runtime)), 69 | poster = purrr::pmap_chr(., function(title, year, ...) get_tmdb_movie(title, year)) 70 | ) 71 | 72 | # bechdel %>% 73 | # select(.title = title, .poster = poster) %>% 74 | # purrr::pmap(function(.title, .poster) htmltools::withTags( 75 | # div( 76 | # style="max-width: 20vw; margin: 10px;", 77 | # h3(.title), 78 | # p(img(src = .poster, style = "max-width: 100%; max-height: 100%")) 79 | # ) 80 | # )) %>% 81 | # htmltools::div(style = "display: flex; flex-direction: row; flex-wrap: wrap") %>% 82 | # htmltools::browsable() 83 | 84 | usethis::use_data(bechdel, overwrite = TRUE) 85 | -------------------------------------------------------------------------------- /man/use_epoxy_knitr_engines.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/engines.R 3 | \name{use_epoxy_knitr_engines} 4 | \alias{use_epoxy_knitr_engines} 5 | \alias{use_epoxy_glue_engine} 6 | \title{Use the epoxy knitr engines} 7 | \usage{ 8 | use_epoxy_knitr_engines( 9 | use_glue_engine = "glue" \%in\% include, 10 | include = c("md", "html", "latex", "mustache") 11 | ) 12 | 13 | use_epoxy_glue_engine() 14 | } 15 | \arguments{ 16 | \item{use_glue_engine}{If \code{TRUE} (default \code{FALSE}), uses \pkg{epoxy}'s \code{glue} 17 | engine, most likely overwriting the \code{glue} engine provided by \pkg{glue}.} 18 | 19 | \item{include}{The epoxy knitr engines to include. Defaults to all engines 20 | except for the \code{glue} engine (which is just an alias for the \code{epoxy} 21 | engine).} 22 | } 23 | \value{ 24 | Silently sets \pkg{epoxy}'s knitr engines and invisible returns 25 | \link[knitr:knit_engines]{knitr::knit_engines} as they were prior to the function call. 26 | } 27 | \description{ 28 | Sets \pkg{epoxy}'s \pkg{knitr} engines for use by \pkg{knitr} in R Markdown 29 | and other document formats powered by \pkg{knitr}. These engines are also 30 | set up when loading \pkg{epoxy} with \code{library()}, so in general you will not 31 | need to call this function explicitly. 32 | 33 | \pkg{epoxy} provides four \pkg{knitr} engines: 34 | \itemize{ 35 | \item \code{epoxy} uses default \pkg{glue} syntax, e.g. \code{{var}} for markdown outputs 36 | \item \code{epoxy_html} uses double brace syntax, e.g. \code{{{var}}} for HTML outputs 37 | \item \code{epoxy_latex} uses double angle brackets syntax, e.g. \verb{<>} for LaTeX 38 | outputs 39 | \item \code{whisker} uses the \pkg{whisker} package which provides an R-based 40 | implementation of the \href{https://mustache.github.io/}{mustache} templating 41 | language. 42 | } 43 | 44 | For historical reasons, aliases for the HTML and LaTeX engines are also 45 | created: \code{glue_html} and \code{glue_latex}. You may opt into a third alias — 46 | \code{glue} for the \code{epoxy} engine — by calling \code{use_epoxy_glue_engine()}, but 47 | note that this will most likely overwrite the \code{glue} engine provided by the 48 | \pkg{glue} package. 49 | } 50 | \section{Functions}{ 51 | \itemize{ 52 | \item \code{use_epoxy_glue_engine()}: Use \pkg{epoxy}'s \code{epoxy} engine as 53 | the \code{glue} engine. 54 | 55 | }} 56 | \examples{ 57 | \dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 58 | use_epoxy_knitr_engines() 59 | \dontshow{\}) # examplesIf} 60 | } 61 | \seealso{ 62 | \code{\link[=epoxy]{epoxy()}}, \code{\link[=epoxy_html]{epoxy_html()}}, \code{\link[=epoxy_latex]{epoxy_latex()}}, and \code{\link[=epoxy_mustache]{epoxy_mustache()}} 63 | for the functions that power these knitr engines. 64 | } 65 | -------------------------------------------------------------------------------- /tests/testthat/test-shiny_ui_epoxy_html-list.R: -------------------------------------------------------------------------------- 1 | skip_on_cran() 2 | skip_if_not_installed("chromote") 3 | skip_if_not_installed("shinytest2") 4 | library(shinytest2) 5 | 6 | test_that("ui_epoxy_html() doesn't break block-level elements", { 7 | app <- AppDriver$new( 8 | app_dir = test_path("apps", "epoxy-html-list"), 9 | name = "epoxy-html-list", 10 | height = 500, 11 | width = 700, 12 | view = interactive(), 13 | expect_values_screenshot_args = FALSE 14 | ) 15 | on.exit(app$stop()) 16 | 17 | get_test_element_text <- function(id) { 18 | app$get_js( 19 | sprintf( 20 | "document.querySelector('[data-test-id=\"%s\"]').innerText", 21 | id 22 | ) 23 | ) 24 | } 25 | 26 | app$run_js(" 27 | document.getElementById('list').addEventListener('epoxy-updated', ev => { 28 | console.log(ev) 29 | Shiny.setInputValue('epoxy_updated_list', ev.detail); 30 | }) 31 | document.getElementById('desc').addEventListener('epoxy-updated', ev => { 32 | console.log(ev) 33 | Shiny.setInputValue('epoxy_updated_desc_' + ev.detail.key, ev.detail); 34 | }) 35 | ") 36 | 37 | expect_event <- function(input, ...) { 38 | data <- list(...) 39 | expect_equal( 40 | app$get_values(input = !!input)$input[[!!input]], 41 | !!data 42 | ) 43 | } 44 | 45 | app$set_inputs(n = 1) 46 | expect_equal( 47 | get_test_element_text("desc"), 48 | "You've picked 1 letter:" 49 | ) 50 | expect_equal( 51 | get_test_element_text("list"), 52 | "a" 53 | ) 54 | expect_event( 55 | "epoxy_updated_list", 56 | output = "list", 57 | key = "item", 58 | data = "a", 59 | outputType = "html" 60 | ) 61 | expect_event( 62 | "epoxy_updated_desc_n", 63 | output = "desc", 64 | key = "n", 65 | data = 1L, 66 | outputType = "html" 67 | ) 68 | expect_event( 69 | "epoxy_updated_desc_thing", 70 | output = "desc", 71 | key = "thing", 72 | data = "letter", 73 | outputType = "html" 74 | ) 75 | 76 | app$set_inputs(n = 4) 77 | expect_equal( 78 | get_test_element_text("desc"), 79 | "You've picked 4 letters:" 80 | ) 81 | expect_equal( 82 | get_test_element_text("list"), 83 | "a\nb\nc\nd" 84 | ) 85 | expect_event( 86 | "epoxy_updated_list", 87 | output = "list", 88 | key = "item", 89 | data = as.list(letters[1:4]), 90 | outputType = "html", 91 | # there are three copies created from the original letter template 92 | # the event has dom elements, shinytest2 only sees an empty named list 93 | copies = lapply(1:3, function(...) list(a = 1)[0]) 94 | ) 95 | expect_event( 96 | "epoxy_updated_desc_n", 97 | output = "desc", 98 | key = "n", 99 | data = 4L, 100 | outputType = "html" 101 | ) 102 | expect_event( 103 | "epoxy_updated_desc_thing", 104 | output = "desc", 105 | key = "thing", 106 | data = "letters", 107 | outputType = "html" 108 | ) 109 | }) 110 | -------------------------------------------------------------------------------- /tests/testthat/test-shiny.R: -------------------------------------------------------------------------------- 1 | describe("ui_epoxy_html()", { 2 | example <- "BAD" 3 | 4 | it ("doesn't find things in the global environment", { 5 | example <- "BADBAD" 6 | ex <- ui_epoxy_html("test", "{{example}}", example = "GOOD") 7 | expect_true(grepl("GOOD", format(ex))) 8 | expect_false(grepl("BAD", format(ex))) 9 | 10 | ex2 <- ui_epoxy_html("test", "{{example}}", example = "GOOD", .placeholder = "NEUTRAL") 11 | expect_true(grepl("GOOD", format(ex2))) 12 | expect_false(grepl("NEUTRAL", format(ex2))) 13 | expect_false(grepl("BAD", format(ex2))) 14 | 15 | ex3 <- ui_epoxy_html("test", "{{example}}", .placeholder = "NEUTRAL") 16 | expect_true(grepl("NEUTRAL", format(ex3))) 17 | expect_false(grepl("BAD", format(ex3))) 18 | }) 19 | 20 | it ("works with htmltags, too", { 21 | ex <- ui_epoxy_html("test", htmltools::tags$p("{{example}}"), example = "GOOD") 22 | expect_true(grepl("

    .+GOOD.*

    ", format(ex))) 23 | 24 | ex2 <- ui_epoxy_html("test", "{{p example}}", example = "GOOD") 25 | expect_true(grepl("

    ", format(ex2))) 26 | }) 27 | 28 | it ("works with html in placeholders", { 29 | ex <- ui_epoxy_html("test", "{{example}}", example="placeholder") 30 | expect_true(grepl("placeholder", format(ex), fixed = TRUE)) 31 | 32 | ex2 <- ui_epoxy_html("test", "{{example}}", example = htmltools::tags$strong("placeholder")) 33 | expect_true(grepl("placeholder", format(ex2), fixed = TRUE)) 34 | }) 35 | 36 | it (".item_container", { 37 | div_span <- ui_epoxy_html("test", "{{item}}") 38 | expect_s3_class(div_span, "shiny.tag") 39 | expect_equal(div_span$name, "epoxy-html") 40 | expect_true(grepl("^ 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | out.width = "100%" 12 | ) 13 | 14 | library(epoxy) 15 | ``` 16 | 17 | # epoxy 18 | 19 | 20 | CRAN status 21 | epoxy r-universe badge 22 | R-CMD-check 23 | Codecov test coverage 24 | 25 | 26 | 27 | ::: lead 28 | Extra-strength glue for scripts, reports, and apps 29 | ::: 30 | 31 | ```{r child = "../man/fragments/epoxy-lead.Rmd", use_rel_path = TRUE} 32 | ``` 33 | 34 | ## Learn more 35 | 36 | There's a whole lot more that epoxy can do! 37 | 38 | ```{=html} 39 |

    40 |
    41 | 42 |
    43 | 49 |
    50 | 51 |
    52 | 58 |
    59 | 60 |
    61 | 67 |
    68 |
    69 |
    70 | ``` 71 | 72 | 73 | ## Installation 74 | 75 | ```{r child = "../man/fragments/installation.Rmd"} 76 | ``` 77 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | 15 | library(epoxy) 16 | ``` 17 | 18 |

    19 | epoxy logo
    20 | {epoxy} 21 |

    22 |

    extra-strength glue for scripts, reports, and apps.

    23 | 24 |

    25 | 26 | CRAN status 27 | epoxy r-universe badge 28 | R-CMD-check 29 | Codecov test coverage 30 | 31 | 32 |

    33 | 34 | ```{r child = "man/fragments/epoxy-lead.Rmd"} 35 | ``` 36 | 37 | ## epoxy in R Markdown and Quarto documents 38 | 39 | In [R Markdown](https://rmarkdown.rstudio.com) and 40 | [Quarto](https://quarto.org) documents, 41 | **epoxy** gives you an `epoxy` chunk where you can write in markdown, 42 | blending prose and data using [glue]'s template syntax. 43 | 44 | ```{r child = "man/fragments/example-movie.Rmd"} 45 | ``` 46 | 47 | Learn more about `epoxy` chunks -- 48 | and its siblings `epoxy_html` and `epoxy_latex` -- 49 | in [Getting Started](https://pkg.garrickadenbuie.com/epoxy//articles/epoxy.html). 50 | Or read more about epoxy's inline formatting in `?epoxy_transform_inline`. 51 | 52 | ## Installation 53 | 54 | ```{r child = "man/fragments/installation.Rmd"} 55 | ``` 56 | 57 | ## Setup 58 | 59 | ```{r child = "man/fragments/setup.Rmd"} 60 | ``` 61 | 62 | ## Use epoxy 63 | 64 | ```{r child = "man/fragments/first-example.Rmd"} 65 | ``` 66 | 67 | ## Learn more 68 | 69 | There's a whole lot more that epoxy can do! 70 | Learn more: 71 | 72 | - [epoxy Package Documentation](https://pkg.garrickadenbuie.com/epoxy/) 73 | 74 | - [Getting Started](https://pkg.garrickadenbuie.com/epoxy//articles/epoxy.html) 75 | 76 | - [Inline Reporting with epoxy](https://pkg.garrickadenbuie.com/epoxy//articles/inline-reporting.html) 77 | 78 | ## Code of Conduct 79 | 80 | Please note that the epoxy project is released with a [Contributor Code of Conduct](http://pkg.garrickadenbuie.com/epoxy/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. 81 | 82 | -------------------------------------------------------------------------------- /inst/srcjs/output-epoxy-mustache.js: -------------------------------------------------------------------------------- 1 | /* globals Shiny,$,Mustache,CustomEvent */ 2 | class EpoxyMustache extends window.HTMLElement { 3 | static is_set_global_event_listener = false 4 | 5 | constructor () { 6 | super() 7 | 8 | if (EpoxyMustache.is_set_global_event_listener) return 9 | window.addEventListener('epoxy-message.mustache', ev => { 10 | // {example: {thing: "dolphin", color: "blue", height: 5}} 11 | EpoxyMustache.update_all(ev.detail) 12 | }) 13 | EpoxyMustache.is_set_global_event_listener = true 14 | } 15 | 16 | static update_all (data) { 17 | // { [id]: template_data } 18 | for (const [key, value] of Object.entries(data)) { 19 | const el = document.getElementById(key) 20 | if (!el) { 21 | console.warn( 22 | `[epoxy-mustache] No element with id "${key}"`, { [key]: value } 23 | ) 24 | continue 25 | } 26 | el.update(value) 27 | } 28 | } 29 | 30 | connectedCallback () { 31 | // store template in DOM element and clean up visible markup 32 | this.mustache_template = this.dataset.epoxyTemplate 33 | this.removeAttribute('data-epoxy-template') 34 | } 35 | 36 | _emit_errors (data) { 37 | const errors = data.__errors__ 38 | if (!errors) return 39 | if (errors.length === 0) return 40 | 41 | errors.forEach(key => { 42 | console.error(`[epoxy-mustache] [${this.id}]: ${data[key]}`) 43 | this.dispatchEvent( 44 | new CustomEvent('epoxy-errored', { 45 | bubbles: true, 46 | detail: { 47 | output: this.id, 48 | key, 49 | message: data[key], 50 | outputType: 'mustache' 51 | } 52 | }) 53 | ) 54 | data[key] = '' 55 | }) 56 | } 57 | 58 | _emit_updated (data) { 59 | this.dispatchEvent( 60 | new CustomEvent('epoxy-updated', { 61 | bubbles: true, 62 | detail: { output: this.id, data, outputType: 'mustache' } 63 | }) 64 | ) 65 | } 66 | 67 | update (data) { 68 | this._emit_errors(data) 69 | 70 | this.innerHTML = Mustache.render(this.mustache_template, data) 71 | this._emit_updated(data) 72 | } 73 | } 74 | 75 | window.customElements.define('epoxy-mustache', EpoxyMustache) 76 | 77 | if (window.Shiny) { 78 | const epoxyMustacheOutputBinding = new Shiny.OutputBinding() 79 | 80 | $.extend(epoxyMustacheOutputBinding, { 81 | find: function (scope) { 82 | return $(scope).find('.epoxy-mustache') 83 | }, 84 | renderValue: function (el, data) { 85 | el.update(data) 86 | }, 87 | renderError: function (el, err) { 88 | this.clearError(el) 89 | if (err.message !== '') { 90 | console.error(`[epoxy-mustache] [${el.id}] ${err.message}`) 91 | el.classList.add('epoxy-error') 92 | } 93 | }, 94 | clearError: function (el) { 95 | el.classList.remove('epoxy-error') 96 | } 97 | }) 98 | 99 | Shiny.outputBindings.register( 100 | epoxyMustacheOutputBinding, 101 | 'shiny.ui_epoxy_mustache' 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /tests/testthat/test-deprecated.R: -------------------------------------------------------------------------------- 1 | # test_that() 2 | 3 | describe("epoxy_style() functions are deprecated", { 4 | env <- rlang::env(word = "abc") 5 | 6 | it("epoxy_style()", { 7 | lifecycle::expect_deprecated( 8 | expect_equal( 9 | epoxy_style("bold")("word", env), 10 | "**abc**" 11 | ) 12 | ) 13 | }) 14 | 15 | it("epoxy_style_apply()", { 16 | lifecycle::expect_deprecated( 17 | expect_equal( 18 | epoxy_style_apply(toupper)("word", env), 19 | "ABC" 20 | ) 21 | ) 22 | }) 23 | 24 | it("epoxy_style_bold()", { 25 | lifecycle::expect_deprecated( 26 | expect_equal( 27 | epoxy_style_bold()("word", env), 28 | "**abc**" 29 | ) 30 | ) 31 | }) 32 | 33 | it("epoxy_style_code()", { 34 | lifecycle::expect_deprecated( 35 | expect_equal( 36 | epoxy_style_code()("word", env), 37 | "`abc`" 38 | ) 39 | ) 40 | }) 41 | 42 | it("epoxy_style_collapse()", { 43 | env <- rlang::env(word = c("a", "b", "c")) 44 | lifecycle::expect_deprecated( 45 | expect_equal( 46 | epoxy_style_collapse()("word*", env), 47 | "a, b, c" 48 | ) 49 | ) 50 | }) 51 | 52 | it("epoxy_style_html()", { 53 | lifecycle::expect_deprecated( 54 | expect_equal( 55 | epoxy_style_html()("strong word", env), 56 | html_chr("abc") 57 | ) 58 | ) 59 | }) 60 | 61 | it("epoxy_style_inline()", { 62 | lifecycle::expect_deprecated( 63 | expect_equal( 64 | epoxy_style_inline()(".code word", env), 65 | "`abc`" 66 | ) 67 | ) 68 | }) 69 | 70 | it("epoxy_style_italic()", { 71 | lifecycle::expect_deprecated( 72 | expect_equal( 73 | epoxy_style_italic()("word", env), 74 | "_abc_" 75 | ) 76 | ) 77 | }) 78 | 79 | it("epoxy_style_get()", { 80 | lifecycle::expect_deprecated(epoxy_style_get()) 81 | }) 82 | 83 | it("epoxy_style_set()", { 84 | lifecycle::expect_deprecated(epoxy_style_set()) 85 | }) 86 | 87 | it("epoxy_style_wrap()", { 88 | lifecycle::expect_deprecated( 89 | expect_equal( 90 | epoxy_style_wrap(before = "-")("word", env), 91 | "-abc-" 92 | ) 93 | ) 94 | }) 95 | }) 96 | 97 | describe("deprecated shiny functions", { 98 | it("epoxyHTML()", { 99 | lifecycle::expect_deprecated( 100 | expect_equal( 101 | epoxyHTML(.id = "test"), 102 | ui_epoxy_html(.id = "test") 103 | ) 104 | ) 105 | }) 106 | 107 | it("renderEpoxy()", { 108 | lifecycle::expect_deprecated(renderEpoxyHTML()) 109 | }) 110 | }) 111 | 112 | describe("engine replaces syntax", { 113 | it("epoxy_transform()", { 114 | lifecycle::expect_deprecated( 115 | expect_equal( 116 | epoxy_transform("bold", syntax = "md"), 117 | epoxy_transform("bold", engine = "md") 118 | ) 119 | ) 120 | }) 121 | 122 | it("epoxy_transform_set()", { 123 | lifecycle::expect_deprecated(epoxy_transform_set(syntax = "md")) 124 | }) 125 | 126 | it("epoxy_transform_wrap()", { 127 | lifecycle::expect_deprecated( 128 | expect_equal( 129 | epoxy_transform_wrap(syntax = "md"), 130 | epoxy_transform_wrap(engine = "md"), 131 | ignore_function_env = TRUE 132 | ) 133 | ) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /tests/testthat/test-utils-knitr.R: -------------------------------------------------------------------------------- 1 | test_that("knitr_current_label() returns the label of current chunk", { 2 | code_chunk_label <- "" 3 | epoxy_chunk_label <- "" 4 | inline_chunk_label <- "" 5 | 6 | render_basic_rmd( 7 | "```{r code-chunk}", 8 | "code_chunk_label <- knitr_current_label()", 9 | "```", 10 | "", 11 | "```{epoxy, epoxy-chunk}", 12 | "{epoxy_chunk_label <- knitr_current_label()}", 13 | "```", 14 | "", 15 | "`r inline_chunk_label <- knitr_current_label()`", 16 | "" 17 | ) 18 | 19 | expect_equal(code_chunk_label, "code-chunk") 20 | expect_equal(epoxy_chunk_label, "epoxy-chunk") 21 | expect_equal(inline_chunk_label, "___inline_chunk___") 22 | }) 23 | 24 | test_that("knitr_chunk_get() gets the chunk and opts from a chunk", { 25 | the_chunk <- NULL 26 | foo <- "bar" 27 | 28 | render_basic_rmd( 29 | "```{r code-chunk, eval = FALSE, foo = foo}", 30 | "this is the code you're looking for", 31 | "```", 32 | "", 33 | "```{r}", 34 | "the_chunk <- knitr_chunk_get('code-chunk')", 35 | "```" 36 | ) 37 | 38 | expect_equal( 39 | the_chunk, 40 | list( 41 | code = "this is the code you're looking for", 42 | opts = list( 43 | label = "code-chunk", 44 | eval = FALSE, 45 | # expressions in chunk options are evaluated 46 | foo = "bar" 47 | ) 48 | ) 49 | ) 50 | }) 51 | 52 | test_that("knitr_chunk_specific_options() returns current chunk options", { 53 | the_chunk <- NULL 54 | foo <- "bar" 55 | 56 | render_basic_rmd( 57 | "```{r code-chunk, echo = FALSE, foo = foo}", 58 | "the_chunk <- knitr_chunk_specific_options()", 59 | "```" 60 | ) 61 | 62 | expect_equal( 63 | the_chunk, 64 | list( 65 | label = "code-chunk", 66 | echo = FALSE, 67 | foo = "bar" 68 | ) 69 | ) 70 | }) 71 | 72 | test_that("knitr_chunk_specific_options() gets options for another chunk", { 73 | the_chunk <- NULL 74 | foo <- "bar" 75 | 76 | render_basic_rmd( 77 | "```{r code-chunk, echo = FALSE}", 78 | "the_chunk <- knitr_chunk_specific_options('other-chunk')", 79 | "```", 80 | "", 81 | "```{r other-chunk, eval = FALSE, bar = foo, .data = NULL}", 82 | "", 83 | "```" 84 | ) 85 | 86 | expect_equal( 87 | the_chunk, 88 | list( 89 | label = "other-chunk", 90 | eval = FALSE, 91 | bar = "bar", 92 | .data = NULL 93 | ) 94 | ) 95 | }) 96 | 97 | test_that("knitr_chunk_specific_options() returns NULL for inline chunks", { 98 | the_chunk <- NULL 99 | foo <- "bar" 100 | 101 | render_basic_rmd( 102 | "```{r code-chunk, echo = FALSE}", 103 | "the_chunk", 104 | "```", 105 | "", 106 | "`r the_chunk <- knitr_chunk_specific_options()`" 107 | ) 108 | 109 | expect_null(the_chunk) 110 | }) 111 | 112 | test_that("knitr_is_inline_chunk()", { 113 | first_chunk <- second_chunk <- third_chunk <- fourth_chunk <- NULL 114 | 115 | render_basic_rmd( 116 | "```{r first-chunk}", 117 | "first_chunk <- knitr_is_inline_chunk()", 118 | "```", 119 | "", 120 | "`r second_chunk <- knitr_is_inline_chunk()`", 121 | "", 122 | "```{r third-chunk}", 123 | "third_chunk <- knitr_is_inline_chunk()", 124 | "```", 125 | "", 126 | "`r fourth_chunk <- knitr_is_inline_chunk()`" 127 | ) 128 | 129 | expect_false(first_chunk) 130 | expect_true(second_chunk) 131 | expect_false(third_chunk) 132 | expect_true(fourth_chunk) 133 | }) 134 | -------------------------------------------------------------------------------- /R/epoxy_mustache.R: -------------------------------------------------------------------------------- 1 | #' Mustache-style string interpolation 2 | #' 3 | #' @description 4 | #' `r lifecycle::badge("experimental")` 5 | #' A wrapper around the [mustache templating 6 | #' language](http://mustache.github.io/), provided by the 7 | #' [whisker](https://cran.r-project.org/package=whisker) package. Under the 8 | #' hood, `epoxy_mustache()` uses [whisker::whisker.render()] to render the 9 | #' template, but adds a few conveniences: 10 | #' 11 | #' * The template can be passed in `...` as a single string, several strings or 12 | #' as a vector of strings. If multiple strings are passed, they are collapsed 13 | #' with `.sep` (`"\n"` by default). 14 | #' 15 | #' * `epoxy_mustache()` can be vectorized over the items in the `.data` 16 | #' argument. If `.data` is a data frame, vectorization is turned on by default 17 | #' so that you can iterate over the rows of the data frame. The output will 18 | #' be a character vector of the same length as the number of rows in the data 19 | #' frame. 20 | #' 21 | #' @example man/examples/epoxy_mustache.R 22 | #' 23 | #' @param ... A string or a vector of strings containing the template(s). Refer 24 | #' to the [mustache documentation](http://mustache.github.io/) for an overview 25 | #' of the syntax. If multiple strings are passed, they are collapsed with 26 | #' `.sep` (`"\n"` by default). 27 | #' @param .data A data frame or a list. If `.data` is a data frame, 28 | #' `epoxy_mustache()` will transform the data frame so that the template can 29 | #' be applied to each row of the data frame. To avoid this transformation, 30 | #' wrap the `.data` value in `I()`. 31 | #' @param .sep The separator to use when collapsing multiple strings passed in 32 | #' `...` into a single template. Defaults to `"\n"`. 33 | #' @param .vectorized If `TRUE` , `epoxy_mustache()` will vectorize over the 34 | #' items in `.data`. In other words, each item or row of `.data` will be used 35 | #' to render the template once. By default, `.vectorized` is set to `TRUE` if 36 | #' `.data` is a data frame and `FALSE` otherwise. 37 | #' @param .partials A named list with partial templates. See 38 | #' [whisker::whisker.render()] or the [mustache 39 | #' documentation](http://mustache.github.io/mustache.5.html#Partials) for 40 | #' details. 41 | #' 42 | #' @return A character vector of length 1 if `.vectorized` is `FALSE` or a 43 | #' character vector of the same length as the number of rows or items in 44 | #' `.data` if `.vectorized` is `TRUE`. 45 | #' 46 | #' @family Mustache-style template functions 47 | #' @export 48 | epoxy_mustache <- function( 49 | ..., 50 | .data = parent.frame(), 51 | .sep = "\n", 52 | .vectorized = inherits(.data, "data.frame"), 53 | .partials = list() 54 | ) { 55 | dots <- list_split_named(rlang::list2(...)) 56 | 57 | if (length(dots$named)) { 58 | rlang::abort("Named arguments are not supported in `epoxy_mustache()`.") 59 | } 60 | 61 | template <- paste(dots$unnamed, collapse = .sep) 62 | 63 | is_data_asis <- inherits(.data, "AsIs") 64 | .data <- maybe_collect(.data) 65 | 66 | whisker_render <- purrr::partial( 67 | whisker::whisker.render, 68 | template = template, 69 | partials = .partials, 70 | strict = TRUE 71 | ) 72 | 73 | if (!isTRUE(.vectorized)) { 74 | return(as_glue_chr(whisker_render(data = .data))) 75 | } 76 | 77 | if (!is_data_asis) { 78 | .data <- prep_whisker_data(.data) 79 | } 80 | 81 | as_glue_chr(purrr::map_chr(.data, whisker_render)) 82 | } 83 | -------------------------------------------------------------------------------- /man/render_epoxy.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/shiny.R 3 | \name{render_epoxy} 4 | \alias{render_epoxy} 5 | \alias{renderEpoxyHTML} 6 | \title{Render Epoxy Output} 7 | \usage{ 8 | render_epoxy( 9 | ..., 10 | .list = NULL, 11 | env = parent.frame(), 12 | outputFunc = ui_epoxy_html, 13 | outputArgs = list() 14 | ) 15 | 16 | renderEpoxyHTML(..., env = parent.frame()) 17 | } 18 | \arguments{ 19 | \item{...}{Named values corresponding to the template variables created with 20 | the associated \code{\link[=ui_epoxy_html]{ui_epoxy_html()}} UI element.} 21 | 22 | \item{.list}{A named list or a \code{\link[shiny:reactiveValues]{shiny::reactiveValues()}} list with names 23 | corresponding to the template variables created with the associated 24 | \code{\link[=ui_epoxy_html]{ui_epoxy_html()}} UI element.} 25 | 26 | \item{env}{The environment in which to evaluate the \code{...}} 27 | 28 | \item{outputFunc}{Either \code{\link[=ui_epoxy_html]{ui_epoxy_html()}} or \code{\link[=ui_epoxy_mustache]{ui_epoxy_mustache()}}, i.e. the 29 | UI function to be paired with this output. This is only used when calling 30 | \code{render_epoxy()} in an Shiny runtime R Markdown document and when you 31 | are only providing the output without an explicit, corresponding UI 32 | element.} 33 | 34 | \item{outputArgs}{A list of arguments to be passed through to the implicit 35 | call to \code{\link[=ui_epoxy_html]{ui_epoxy_html()}} when \code{render_epoxy} is used in an interactive R 36 | Markdown document.} 37 | } 38 | \value{ 39 | A server-side Shiny render function that should be assigned to 40 | Shiny's \code{output} object and named to match the \code{.id} of the corresponding 41 | \code{\link[=ui_epoxy_html]{ui_epoxy_html()}} call. 42 | } 43 | \description{ 44 | Server-side render function used to provide values for template items. Use 45 | named values matching the template variable names in the associated 46 | \code{\link[=ui_epoxy_html]{ui_epoxy_html()}} or \code{\link[=ui_epoxy_mustache]{ui_epoxy_mustache()}}. When the values are updated by 47 | the app, \code{render_epoxy()} will update the values shown in the app's UI. 48 | } 49 | \section{Functions}{ 50 | \itemize{ 51 | \item \code{renderEpoxyHTML()}: \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Deprecated alias, 52 | please use \code{render_epoxy()}. 53 | 54 | }} 55 | \examples{ 56 | \dontshow{if (rlang::is_installed("shiny") && rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 57 | # This small app shows the current time using `ui_epoxy_html()` 58 | # to provide the HTML template and `render_epoxy()` to 59 | # update the current time every second. 60 | 61 | ui <- shiny::fluidPage( 62 | shiny::h2("Current Time"), 63 | ui_epoxy_html( 64 | "time", 65 | shiny::p("The current time is {{strong time}}.") 66 | ) 67 | ) 68 | 69 | server <- function(input, output, session) { 70 | current_time <- shiny::reactive({ 71 | shiny::invalidateLater(1000) 72 | strftime(Sys.time(), "\%F \%T") 73 | }) 74 | 75 | output$time <- render_epoxy(time = current_time()) 76 | } 77 | 78 | if (rlang::is_interactive()) { 79 | shiny::shinyApp(ui, server) 80 | } 81 | \dontshow{\}) # examplesIf} 82 | \dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 83 | run_epoxy_example_app("render_epoxy") 84 | \dontshow{\}) # examplesIf} 85 | } 86 | \seealso{ 87 | \code{\link[=ui_epoxy_html]{ui_epoxy_html()}}, \code{\link[=ui_epoxy_mustache]{ui_epoxy_mustache()}} 88 | } 89 | -------------------------------------------------------------------------------- /man/run_epoxy_example_app.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/shiny.R 3 | \name{run_epoxy_example_app} 4 | \alias{run_epoxy_example_app} 5 | \title{Example epoxy Shiny apps} 6 | \usage{ 7 | run_epoxy_example_app( 8 | name = c("ui_epoxy_html", "ui_epoxy_markdown", "ui_epoxy_mustache", "render_epoxy"), 9 | display.mode = "showcase", 10 | ... 11 | ) 12 | } 13 | \arguments{ 14 | \item{name}{Name of the example, currently one of \code{"ui_epoxy_html"}, 15 | \code{"ui_epoxy_markdown"}, \code{"ui_epoxy_mustache"}, or \code{"render_epoxy"}.} 16 | 17 | \item{display.mode}{The mode in which to display the application. If set to 18 | the value \code{"showcase"}, shows application code and metadata from a 19 | \code{DESCRIPTION} file in the application directory alongside the 20 | application. If set to \code{"normal"}, displays the application normally. 21 | Defaults to \code{"auto"}, which displays the application in the mode given 22 | in its \code{DESCRIPTION} file, if any.} 23 | 24 | \item{...}{ 25 | Arguments passed on to \code{\link[shiny:runApp]{shiny::runApp}} 26 | \describe{ 27 | \item{\code{appDir}}{The application to run. Should be one of the following: 28 | \itemize{ 29 | \item A directory containing \code{server.R}, plus, either \code{ui.R} or 30 | a \code{www} directory that contains the file \code{index.html}. 31 | \item A directory containing \code{app.R}. 32 | \item An \code{.R} file containing a Shiny application, ending with an 33 | expression that produces a Shiny app object. 34 | \item A list with \code{ui} and \code{server} components. 35 | \item A Shiny app object created by \code{\link[shiny:shinyApp]{shinyApp()}}. 36 | }} 37 | \item{\code{port}}{The TCP port that the application should listen on. If the 38 | \code{port} is not specified, and the \code{shiny.port} option is set (with 39 | \code{options(shiny.port = XX)}), then that port will be used. Otherwise, 40 | use a random port between 3000:8000, excluding ports that are blocked 41 | by Google Chrome for being considered unsafe: 3659, 4045, 5060, 42 | 5061, 6000, 6566, 6665:6669 and 6697. Up to twenty random 43 | ports will be tried.} 44 | \item{\code{launch.browser}}{If true, the system's default web browser will be 45 | launched automatically after the app is started. Defaults to true in 46 | interactive sessions only. The value of this parameter can also be a 47 | function to call with the application's URL.} 48 | \item{\code{host}}{The IPv4 address that the application should listen on. Defaults 49 | to the \code{shiny.host} option, if set, or \code{"127.0.0.1"} if not. See 50 | Details.} 51 | \item{\code{workerId}}{Can generally be ignored. Exists to help some editions of 52 | Shiny Server Pro route requests to the correct process.} 53 | \item{\code{quiet}}{Should Shiny status messages be shown? Defaults to FALSE.} 54 | \item{\code{test.mode}}{Should the application be launched in test mode? This is 55 | only used for recording or running automated tests. Defaults to the 56 | \code{shiny.testmode} option, or FALSE if the option is not set.} 57 | }} 58 | } 59 | \value{ 60 | Runs the Shiny example app interactively. Nothing is returned. 61 | } 62 | \description{ 63 | Run an example epoxy Shiny app showcasing the Shiny UI and server components 64 | provided by epoxy. 65 | } 66 | \examples{ 67 | # List examples by passing `name = NULL` 68 | run_epoxy_example_app(name = NULL) 69 | 70 | } 71 | \seealso{ 72 | \code{\link[=ui_epoxy_html]{ui_epoxy_html()}}, \code{\link[=ui_epoxy_markdown]{ui_epoxy_markdown()}}, \code{\link[=ui_epoxy_mustache]{ui_epoxy_mustache()}}, \code{\link[=render_epoxy]{render_epoxy()}} 73 | } 74 | -------------------------------------------------------------------------------- /R/deprecated.R: -------------------------------------------------------------------------------- 1 | #' Deprecated: `epoxy_style()` 2 | #' 3 | #' `epoxy_style()` was renamed [epoxy_transform()] in \pkg{epoxy} version 0.1.0. 4 | #' 5 | #' @param ... Passed to the new `epoxy_transform` function. 6 | #' 7 | #' @return Returns the result of the new `epoxy_transform` function. 8 | #' 9 | #' @keywords internal 10 | #' @export 11 | #' @name epoxy_style 12 | NULL 13 | 14 | # TODO(lifecycle): epoxy_style functions were deprecated 2023-05-06. 15 | 16 | #' @describeIn epoxy_style is now [epoxy_transform()]. 17 | #' @export 18 | epoxy_style <- function(...) { 19 | lifecycle::deprecate_warn( 20 | when = "0.1.0", 21 | what = "epoxy_style()", 22 | with = "epoxy_transform()" 23 | ) 24 | epoxy_transform(...) 25 | } 26 | 27 | #' @describeIn epoxy_style is now [epoxy_transform_apply()]. 28 | #' @export 29 | epoxy_style_apply <- function(...) { 30 | lifecycle::deprecate_warn( 31 | when = "0.1.0", 32 | what = "epoxy_style_apply()", 33 | with = "epoxy_transform_apply()" 34 | ) 35 | epoxy_transform_apply(...) 36 | } 37 | 38 | #' @describeIn epoxy_style is now [epoxy_transform_bold()]. 39 | #' @export 40 | epoxy_style_bold <- function(...) { 41 | lifecycle::deprecate_warn( 42 | when = "0.1.0", 43 | what = "epoxy_style_bold()", 44 | with = "epoxy_transform_bold()" 45 | ) 46 | epoxy_transform_bold(...) 47 | } 48 | 49 | #' @describeIn epoxy_style is now [epoxy_transform_code()]. 50 | #' @export 51 | epoxy_style_code <- function(...) { 52 | lifecycle::deprecate_warn( 53 | when = "0.1.0", 54 | what = "epoxy_style_code()", 55 | with = "epoxy_transform_code()" 56 | ) 57 | epoxy_transform_code(...) 58 | } 59 | 60 | #' @describeIn epoxy_style is now [epoxy_transform_collapse()]. 61 | #' @export 62 | epoxy_style_collapse <- function(...) { 63 | lifecycle::deprecate_warn( 64 | when = "0.1.0", 65 | what = "epoxy_style_collapse()", 66 | with = "epoxy_transform_collapse()" 67 | ) 68 | epoxy_transform_collapse(...) 69 | } 70 | 71 | #' @describeIn epoxy_style is now [epoxy_transform_get()]. 72 | #' @export 73 | epoxy_style_get <- function(...) { 74 | lifecycle::deprecate_warn( 75 | when = "0.1.0", 76 | what = "epoxy_style_get()", 77 | with = "epoxy_transform_get()" 78 | ) 79 | epoxy_transform_get(...) 80 | } 81 | 82 | #' @describeIn epoxy_style is now [epoxy_transform_html()]. 83 | #' @export 84 | epoxy_style_html <- function(...) { 85 | lifecycle::deprecate_warn( 86 | when = "0.1.0", 87 | what = "epoxy_style_html()", 88 | with = "epoxy_transform_html()" 89 | ) 90 | epoxy_transform_html(...) 91 | } 92 | 93 | #' @describeIn epoxy_style is now [epoxy_transform_inline()]. 94 | #' @export 95 | epoxy_style_inline <- function(...) { 96 | lifecycle::deprecate_warn( 97 | when = "0.1.0", 98 | what = "epoxy_style_inline()", 99 | with = "epoxy_transform_inline()" 100 | ) 101 | epoxy_transform_inline(...) 102 | } 103 | 104 | #' @describeIn epoxy_style is now [epoxy_transform_italic()]. 105 | #' @export 106 | epoxy_style_italic <- function(...) { 107 | lifecycle::deprecate_warn( 108 | when = "0.1.0", 109 | what = "epoxy_style_italic()", 110 | with = "epoxy_transform_italic()" 111 | ) 112 | epoxy_transform_italic(...) 113 | } 114 | 115 | #' @describeIn epoxy_style is now [epoxy_transform_set()]. 116 | #' @export 117 | epoxy_style_set <- function(...) { 118 | lifecycle::deprecate_warn( 119 | when = "0.1.0", 120 | what = "epoxy_style_set()", 121 | with = "epoxy_transform_set()" 122 | ) 123 | epoxy_transform_set(...) 124 | } 125 | 126 | #' @describeIn epoxy_style is now [epoxy_transform_wrap()]. 127 | #' @export 128 | epoxy_style_wrap <- function(...) { 129 | lifecycle::deprecate_warn( 130 | when = "0.1.0", 131 | what = "epoxy_style_wrap()", 132 | with = "epoxy_transform_wrap()" 133 | ) 134 | epoxy_transform_wrap(...) 135 | } 136 | -------------------------------------------------------------------------------- /man/ui_epoxy_mustache.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/shiny.R 3 | \name{ui_epoxy_mustache} 4 | \alias{ui_epoxy_mustache} 5 | \alias{ui_epoxy_whisker} 6 | \title{Epoxy HTML Mustache Template} 7 | \usage{ 8 | ui_epoxy_mustache( 9 | id, 10 | ..., 11 | .file = NULL, 12 | .sep = "", 13 | .container = "epoxy-mustache" 14 | ) 15 | 16 | ui_epoxy_whisker( 17 | id, 18 | ..., 19 | .file = NULL, 20 | .sep = "", 21 | .container = "epoxy-mustache" 22 | ) 23 | } 24 | \arguments{ 25 | \item{id}{The ID of the output.} 26 | 27 | \item{...}{Character strings of HTML or \link[htmltools:builder]{htmltools::tags}. All elements 28 | should be unnamed.} 29 | 30 | \item{.file}{A path to a template file. If provided, no other template lines 31 | should be included in \code{...}.} 32 | 33 | \item{.sep}{The separator used to concatenate elements in \code{...}.} 34 | 35 | \item{.container}{A character tag name, e.g. \code{"div"} or \code{"span"}, or a 36 | function that returns an \code{\link[htmltools:builder]{htmltools::tag()}}.} 37 | } 38 | \value{ 39 | Returns a Shiny output UI element. 40 | } 41 | \description{ 42 | A Shiny output that uses \href{https://mustache.github.io/}{mustache templating} 43 | to render HTML. Mustache is a powerful template language with minimal 44 | internal logic. The advantage of \code{ui_epoxy_mustache()} is that all parts of 45 | the HTML can be templated -- including element attributes -- whereas 46 | \code{\link[=ui_epoxy_html]{ui_epoxy_html()}} requires that the dynamic template variables appear in the 47 | text portion of the UI. The downside is that the entire template is 48 | re-rendered (in the browser), each time that updated data is sent from the 49 | server -- unlike \code{\link[=ui_epoxy_html]{ui_epoxy_html()}}, whose updates are specific to the parts 50 | of the data that have changed. 51 | } 52 | \section{Functions}{ 53 | \itemize{ 54 | \item \code{ui_epoxy_whisker()}: An alias for \code{ui_epoxy_mustache()}, provided 55 | because R users are more familiar with this syntax via the \pkg{whisker} 56 | package. 57 | 58 | }} 59 | \examples{ 60 | \dontshow{if (rlang::is_installed("shiny") && rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 61 | library(shiny) 62 | 63 | ui <- fluidPage( 64 | fluidRow( 65 | style = "max-width: 600px; margin: 0 auto", 66 | column( 67 | width = 6, 68 | ui_epoxy_mustache( 69 | id = "template", 70 | h2(class = "{{heading_class}}", "Hello, {{name}}!"), 71 | "{{#favorites}}", 72 | p("Your favorite fruits are..."), 73 | tags$ul(HTML("{{#fruits}}
  • {{.}}
  • {{/fruits}}")), 74 | "{{/favorites}}", 75 | "{{^favorites}}

    Do you have any favorite fruits?

    {{/favorites}}" 76 | ) 77 | ), 78 | column( 79 | width = 6, 80 | h2("Inputs"), 81 | textInput("name", "Your name"), 82 | textInput("fruits", "Favorite fruits", placeholder = "apple, banana"), 83 | helpText("Enter a comma-separated list of fruits.") 84 | ) 85 | ) 86 | ) 87 | 88 | server <- function(input, output, session) { 89 | user_name <- reactive({ 90 | if (!nzchar(input$name)) return("user") 91 | input$name 92 | }) 93 | 94 | favorites <- reactive({ 95 | if (identical(input$fruits, "123456")) { 96 | # Errors are equivalent to "empty" values, 97 | # the rest of the template will still render. 98 | stop("Bad fruits, bad!") 99 | } 100 | 101 | if (!nzchar(input$fruits)) return(NULL) 102 | list(fruits = strsplit(input$fruits, "\\\\s*,\\\\s*")[[1]]) 103 | }) 104 | 105 | output$template <- render_epoxy( 106 | name = user_name(), 107 | heading_class = if (user_name() != "user") "text-success", 108 | favorites = favorites() 109 | ) 110 | } 111 | 112 | if (interactive()) { 113 | shiny::shinyApp(ui, server) 114 | } 115 | \dontshow{\}) # examplesIf} 116 | \dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 117 | run_epoxy_example_app("ui_epoxy_mustache") 118 | \dontshow{\}) # examplesIf} 119 | } 120 | \seealso{ 121 | \code{\link[=ui_epoxy_html]{ui_epoxy_html()}}, \code{\link[=render_epoxy]{render_epoxy()}} 122 | 123 | Other Mustache-style template functions: 124 | \code{\link{epoxy_mustache}()} 125 | } 126 | \concept{Mustache-style template functions} 127 | -------------------------------------------------------------------------------- /man/epoxy_mustache.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/epoxy_mustache.R 3 | \name{epoxy_mustache} 4 | \alias{epoxy_mustache} 5 | \title{Mustache-style string interpolation} 6 | \usage{ 7 | epoxy_mustache( 8 | ..., 9 | .data = parent.frame(), 10 | .sep = "\\n", 11 | .vectorized = inherits(.data, "data.frame"), 12 | .partials = list() 13 | ) 14 | } 15 | \arguments{ 16 | \item{...}{A string or a vector of strings containing the template(s). Refer 17 | to the \href{http://mustache.github.io/}{mustache documentation} for an overview 18 | of the syntax. If multiple strings are passed, they are collapsed with 19 | \code{.sep} (\code{"\\n"} by default).} 20 | 21 | \item{.data}{A data frame or a list. If \code{.data} is a data frame, 22 | \code{epoxy_mustache()} will transform the data frame so that the template can 23 | be applied to each row of the data frame. To avoid this transformation, 24 | wrap the \code{.data} value in \code{I()}.} 25 | 26 | \item{.sep}{The separator to use when collapsing multiple strings passed in 27 | \code{...} into a single template. Defaults to \code{"\\n"}.} 28 | 29 | \item{.vectorized}{If \code{TRUE} , \code{epoxy_mustache()} will vectorize over the 30 | items in \code{.data}. In other words, each item or row of \code{.data} will be used 31 | to render the template once. By default, \code{.vectorized} is set to \code{TRUE} if 32 | \code{.data} is a data frame and \code{FALSE} otherwise.} 33 | 34 | \item{.partials}{A named list with partial templates. See 35 | \code{\link[whisker:whisker.render]{whisker::whisker.render()}} or the \href{http://mustache.github.io/mustache.5.html#Partials}{mustache documentation} for 36 | details.} 37 | } 38 | \value{ 39 | A character vector of length 1 if \code{.vectorized} is \code{FALSE} or a 40 | character vector of the same length as the number of rows or items in 41 | \code{.data} if \code{.vectorized} is \code{TRUE}. 42 | } 43 | \description{ 44 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 45 | A wrapper around the \href{http://mustache.github.io/}{mustache templating language}, provided by the 46 | \href{https://cran.r-project.org/package=whisker}{whisker} package. Under the 47 | hood, \code{epoxy_mustache()} uses \code{\link[whisker:whisker.render]{whisker::whisker.render()}} to render the 48 | template, but adds a few conveniences: 49 | \itemize{ 50 | \item The template can be passed in \code{...} as a single string, several strings or 51 | as a vector of strings. If multiple strings are passed, they are collapsed 52 | with \code{.sep} (\code{"\\n"} by default). 53 | \item \code{epoxy_mustache()} can be vectorized over the items in the \code{.data} 54 | argument. If \code{.data} is a data frame, vectorization is turned on by default 55 | so that you can iterate over the rows of the data frame. The output will 56 | be a character vector of the same length as the number of rows in the data 57 | frame. 58 | } 59 | } 60 | \examples{ 61 | # The canonical mustache example 62 | epoxy_mustache( 63 | "Hello {{name}}!", 64 | "You have just won {{value}} dollars!", 65 | "{{#in_ca}}", 66 | "Well, {{taxed_value}} dollars, after taxes.", 67 | "{{/in_ca}}", 68 | .data = list( 69 | name = "Chris", 70 | value = 10000, 71 | taxed_value = 10000 - (10000 * 0.4), 72 | in_ca = TRUE 73 | ) 74 | ) 75 | 76 | # Vectorized over the rows of .data 77 | epoxy_mustache( 78 | "mpg: {{ mpg }}", 79 | "hp: {{ hp }}", 80 | "wt: {{ wt }}\n", 81 | .data = mtcars[1:2, ] 82 | ) 83 | 84 | # Non-vectorized 85 | epoxy_mustache( 86 | "mpg: {{ mpg }}", 87 | "hp: {{ hp }}", 88 | "wt: {{ wt }}", 89 | .data = mtcars[1:2, ], 90 | .vectorized = FALSE 91 | ) 92 | 93 | # With mustache partials 94 | epoxy_mustache( 95 | "Hello {{name}}!", 96 | "{{> salutation }}", 97 | "You have just won {{value}} dollars!", 98 | "{{#in_ca}}", 99 | "Well, {{taxed_value}} dollars, after taxes.", 100 | "{{/in_ca}}", 101 | .partials = list( 102 | salutation = c("Hope you are well, {{name}}.") 103 | ), 104 | .sep = " ", 105 | .data = list( 106 | name = "Chris", 107 | value = 10000, 108 | taxed_value = 10000 - (10000 * 0.4), 109 | in_ca = TRUE 110 | ) 111 | ) 112 | 113 | 114 | } 115 | \seealso{ 116 | Other Mustache-style template functions: 117 | \code{\link{ui_epoxy_mustache}()} 118 | } 119 | \concept{Mustache-style template functions} 120 | -------------------------------------------------------------------------------- /pkgdown/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # epoxy 5 | 6 | 7 | 8 | CRAN status 9 | epoxy r-universe badge 10 | R-CMD-check 11 | Codecov test coverage 12 | 13 | 14 | 15 |
    16 | 17 | Extra-strength glue for 18 | scripts, reports, and apps 19 | 20 |
    21 | 22 | ## epoxy is super glue 23 | 24 | ### [In R Markdown and Quarto reports](articles/epoxy-report.html) 25 | 26 | Use `epoxy` chunks for extra-strength inline syntax. Just 27 | `library(epoxy)` in your [R Markdown](https://rmarkdown.rstudio.com) or 28 | [Quarto](https://quarto.org) document to get started. All epoxy chunks 29 | make it easy to transform values in place with a `{cli}`-inspired inline 30 | syntax described in `?epoxy_transform_inline`. 31 | 32 | ### [In R scripts](articles/epoxy-script.html) 33 | 34 | The same functions that power epoxy chunks are availble in three 35 | flavors: 36 | 37 | - `epoxy()` for markdown and general purpose outputs 38 | 39 | - `epoxy_html()` for HTML outputs, with added support for HTML 40 | templating (see `?epoxy_transform_html`) 41 | 42 | - `epoxy_latex()` for LaTeX reports 43 | 44 | These functions are accompanied by a robust system for chained 45 | glue-transformers powered by `epoxy_transform()`. 46 | 47 | ### [In Shiny apps](articles/epoxy-shiny.html) 48 | 49 | `ui_epoxy_html()` makes it easy to update text or HTML dynamically, 50 | anywhere in your [Shiny](https://shiny.posit.co/) app’s UI. For more 51 | complicated situations, `ui_epoxy_mustache()` lets you turn any Shiny UI 52 | into a template that leverages the [Mustache templating 53 | language](https://mustache.github.io). 54 | 55 | ## Learn more 56 | 57 | There’s a whole lot more that epoxy can do! 58 | 59 |
    60 |
    61 | 62 |
    63 | 69 |
    70 | 71 |
    72 | 78 |
    79 | 80 |
    81 | 87 |
    88 |
    89 |
    90 | 91 | ## Installation 92 | 93 | You can install epoxy from CRAN: 94 | 95 | ``` r 96 | install.packages("epoxy") 97 | ``` 98 | 99 | You can install the latest development version of epoxy with 100 | [remotes](https://remotes.r-lib.org) 101 | 102 | ``` r 103 | # install.packages("remotes") 104 | remotes::install_github("gadenbuie/epoxy") 105 | ``` 106 | 107 | or from [gadenbuie.r-universe.dev](https://gadenbuie.r-universe.dev). 108 | 109 | ``` r 110 | options(repos = c( 111 | gadenbuie = "https://gadenbuie.r-universe.dev/", 112 | getOption("repos") 113 | )) 114 | 115 | install.packages("epoxy") 116 | ``` 117 | -------------------------------------------------------------------------------- /man/epoxy_transform_html.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/epoxy_transform_html.R 3 | \name{epoxy_transform_html} 4 | \alias{epoxy_transform_html} 5 | \title{Concise syntax for expressions inside HTML elements} 6 | \usage{ 7 | epoxy_transform_html( 8 | class = NULL, 9 | element = "span", 10 | collapse = TRUE, 11 | transformer = glue::identity_transformer 12 | ) 13 | } 14 | \arguments{ 15 | \item{class}{\verb{[character()]}\cr Additional classes to be added to the inline 16 | HTML element.} 17 | 18 | \item{element}{\verb{[character()}\cr The default HTML element tag name to be used 19 | when an element isn't specified in the expression.} 20 | 21 | \item{collapse}{\verb{[logical(1)]}\cr If \code{TRUE}, transformed HTML outputs will be 22 | collapsed into a single character string. This is helpful when you're 23 | including the value of a vector within an outer HTML tag. Use \code{collapse = FALSE} to return a vector of HTML character strings instead, which follows 24 | what you'd typically expect from \code{glue::glue()}, i.e. when you want to 25 | repeat the outer wrapping text for each element of the vector.} 26 | 27 | \item{transformer}{The transformer to apply to the replacement string. This 28 | argument is used for chaining the transformer functions. By providing a 29 | function to this argument you can apply an additional transformation after 30 | the current transformation. In nearly all cases, you can let 31 | \code{epoxy_transform()} handle this for you. The chain ends when 32 | \code{\link[glue:identity_transformer]{glue::identity_transformer()}} is used as the \code{transformer}.} 33 | } 34 | \value{ 35 | A function of \code{text} and \code{envir} suitable for the \code{.transformer} argument of 36 | \code{\link[glue:glue]{glue::glue()}}. 37 | } 38 | \description{ 39 | \code{epoxy_transform_html()} provides a 40 | \href{https://pughtml.com/what-is-pug-html}{pug}-like syntax for expressions in 41 | HTML that are wrapped in HTML elements. 42 | \subsection{Syntax}{ 43 | 44 | You can specify the HTML element and its \code{id} and \code{class} into which the 45 | text of the expression will be placed. The template is to specify the element 46 | using the syntax below, followed by the R expression, separated by a space: 47 | 48 | \if{html}{\out{
    }}\preformatted{\{\{ [][# | ....] expr \}\} 49 | }\if{html}{\out{
    }} 50 | 51 | For example, to place the expression in a \verb{
  • } element with \code{id = "food"} 52 | and \code{class = "fruit"}, you could write 53 | 54 | \if{html}{\out{
    }}\preformatted{\{\{ li#food.fruit fruit_name \}\} 55 | }\if{html}{\out{
    }} 56 | 57 | Each item in the HTML template is optional: 58 | \enumerate{ 59 | \item If a specific HTML element is desired, the element name must be first. If 60 | no element is specified, the default as set by the \code{element} argument of 61 | \code{\link[=epoxy_transform_html]{epoxy_transform_html()}} will be used. 62 | \item IDs are specified using \verb{#} and only one ID may be present 63 | \item Classes are written using \verb{.} and as many classes as desired are 64 | allowed. 65 | } 66 | 67 | If the expression is a vector, the same element container will be used for 68 | each item in the vector. 69 | 70 | Finally, if the expression returns HTML, it will be escaped by default. You 71 | can either use \code{\link[htmltools:HTML]{htmltools::HTML()}} to mark it as safe HTML in R, or you can 72 | write \code{!!expr} in the inline markup: \verb{\{\{ li#food.fruit !!fruit_name \}\}}. 73 | } 74 | } 75 | \examples{ 76 | epoxy_html("
      {{ li letters[1:3] }}
    ") 77 | epoxy_html("
      {{ li.alpha letters[1:3] }}
    ") 78 | epoxy_html("
      {{ li#my-letter letters[7] }}
    ") 79 | 80 | # The default element is used if no element is directly requested 81 | epoxy_html("My name starts with {{ .name-letter letters[7] }}") 82 | 83 | epoxy_html( 84 | "{{ h3#title title }}", 85 | title = "Epoxy for HTML" 86 | ) 87 | 88 | # If your replacement text contains HTML, it's escaped by default. 89 | hello <- "Hi there!" 90 | epoxy_html("{{ hello }}") 91 | 92 | # You can use !! inline to mark the text as safe HTML... 93 | epoxy_html("{{ !!hello }}") 94 | epoxy_html("{{ button !!hello }}") 95 | 96 | # ...or you can use htmltools::HTML() to mark it as safe HTML in R. 97 | hello <- htmltools::HTML("Hi there!") 98 | epoxy_html("{{ hello }}") 99 | 100 | } 101 | \seealso{ 102 | Used by default in \code{\link[=epoxy_html]{epoxy_html()}} 103 | 104 | Other epoxy's glue transformers: 105 | \code{\link{epoxy_transform}()}, 106 | \code{\link{epoxy_transform_inline}()} 107 | } 108 | \concept{epoxy's glue transformers} 109 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Deploys pkgdown for Pull Requests, tags, and pushes to main branch 2 | # PRs are deployed to /preview/pr/ 3 | # Tags are deployed to // 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | types: 9 | - opened 10 | - reopened 11 | - synchronize 12 | - closed 13 | paths: 14 | - 'man/**' 15 | - 'pkgdown/**' 16 | - 'vignettes/**' 17 | push: 18 | tags: 19 | - 'v[0-9]+.[0-9]+.[0-9]+' # build on version tags 20 | - '!v[0-9]+.[0-9]+.[0-9]+.[0-9]+' # but not if version involves a dev component 21 | branches: 22 | - main 23 | workflow_dispatch: 24 | inputs: 25 | tag: 26 | description: Tag to deploy 27 | required: true 28 | default: '' 29 | 30 | name: pkgdown 31 | 32 | jobs: 33 | pkgdown-build: 34 | runs-on: ubuntu-latest 35 | if: ${{ !(github.event_name == 'pull_request' && github.event.action == 'closed') }} 36 | env: 37 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 38 | steps: 39 | - uses: actions/checkout@v2 40 | 41 | - name: Configure git 42 | run: | 43 | git config --local user.name "$GITHUB_ACTOR" 44 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 45 | 46 | - uses: r-lib/actions/pr-fetch@v2 47 | if: ${{ github.event_name == 'pull_request' }} 48 | with: 49 | repo-token: ${{ github.token }} 50 | 51 | - uses: r-lib/actions/setup-pandoc@v2 52 | 53 | - uses: r-lib/actions/setup-r@v2 54 | with: 55 | use-public-rspm: true 56 | 57 | - uses: r-lib/actions/setup-r-dependencies@v2 58 | with: 59 | needs: | 60 | connect 61 | website 62 | extra-packages: | 63 | local::. 64 | any::pkgdown 65 | 66 | # If events is a PR, set subdir to 'preview/pr' 67 | - name: "[PR] Set documentation subdirectory" 68 | if: github.event_name == 'pull_request' 69 | run: | 70 | echo "PKGDOWN_DEV_MODE=unreleased" >> $GITHUB_ENV 71 | echo "subdir=preview/pr${{ github.event.number }}" >> $GITHUB_ENV 72 | 73 | # If event is a tag, set subdir to '' 74 | - name: "[tag] Set documentation subdirectory" 75 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 76 | run: | 77 | echo "PKGDOWN_DEV_MODE=release" >> $GITHUB_ENV 78 | echo "subdir=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 79 | 80 | # If event is workflow_dispatch, set subdir to 'inputs.tag' 81 | - name: '[dispatch] Set documentation subdirectory' 82 | if: github.event_name == 'workflow_dispatch' 83 | run: | 84 | echo "subdir=${{ github.event.inputs.tag }}" >> $GITHUB_ENV 85 | 86 | - name: Deploy pkgdown site 87 | id: deploy 88 | shell: Rscript {0} 89 | run: | 90 | subdir <- "${{ env.subdir }}" 91 | pkg <- pkgdown::as_pkgdown(".") 92 | 93 | # Deploy pkgdown site to branch 94 | pkgdown::deploy_to_branch(subdir = if (nzchar(subdir)) subdir, clean = nzchar(subdir)) 95 | 96 | # Report deployed site URL 97 | deployed_url <- file.path(pkg$meta$url, subdir) 98 | cat(sprintf('url=%s', deployed_url), file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) 99 | 100 | - name: Notify pkgdown deployment 101 | if: github.event_name == 'pull_request' 102 | uses: hasura/comment-progress@v2.2.0 103 | with: 104 | github-token: ${{ secrets.GITHUB_TOKEN }} 105 | repository: ${{ github.repository }} 106 | number: ${{ github.event.number }} 107 | id: pkgdown-deploy 108 | append: false 109 | message: > 110 | :book: ${{ steps.deploy.outputs.url }} 111 | 112 | Preview documentation for this PR (at commit ${{ github.event.pull_request.head.sha }}) 113 | 114 | pkgdown-clean: 115 | if: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' }} 116 | runs-on: ubuntu-latest 117 | env: 118 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 119 | steps: 120 | - uses: actions/checkout@v2 121 | with: 122 | ref: "gh-pages" 123 | 124 | - name: Clean up PR Preview 125 | run: | 126 | git config --local user.name "$GITHUB_ACTOR" 127 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 128 | 129 | preview_dir="preview/pr${{ github.event.pull_request.number }}" 130 | if [ -d "$preview_dir" ]; then 131 | git rm -r $preview_dir 132 | git commit -m "Remove $preview_dir (GitHub Actions)" || echo 'No preview to remove' 133 | git push origin || echo 'No preview to remove' 134 | else 135 | echo 'No preview to remove' 136 | fi 137 | 138 | - name: Notify pkgdown cleanup 139 | uses: hasura/comment-progress@v2.2.0 140 | with: 141 | github-token: ${{ secrets.GITHUB_TOKEN }} 142 | repository: ${{ github.repository }} 143 | number: ${{ github.event.number }} 144 | id: pkgdown-deploy 145 | message: | 146 | _:closed_book: Preview documentation for this PR has been cleaned up._ 147 | 148 | -------------------------------------------------------------------------------- /man/epoxy_transform_one_shot.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/transformers.R 3 | \name{epoxy_transform_one_shot} 4 | \alias{epoxy_transform_one_shot} 5 | \alias{epoxy_transform_wrap} 6 | \alias{epoxy_transform_bold} 7 | \alias{epoxy_transform_italic} 8 | \alias{epoxy_transform_apply} 9 | \alias{epoxy_transform_code} 10 | \alias{epoxy_transform_collapse} 11 | \title{One-shot epoxy transformers} 12 | \usage{ 13 | epoxy_transform_wrap( 14 | before = "**", 15 | after = before, 16 | engine = NULL, 17 | transformer = glue::identity_transformer, 18 | syntax = lifecycle::deprecated() 19 | ) 20 | 21 | epoxy_transform_bold(engine = NULL, transformer = glue::identity_transformer) 22 | 23 | epoxy_transform_italic(engine = NULL, transformer = glue::identity_transformer) 24 | 25 | epoxy_transform_apply( 26 | .f = identity, 27 | ..., 28 | transformer = glue::identity_transformer 29 | ) 30 | 31 | epoxy_transform_code(engine = NULL, transformer = glue::identity_transformer) 32 | 33 | epoxy_transform_collapse( 34 | sep = ", ", 35 | last = sep, 36 | language = NULL, 37 | ..., 38 | transformer = glue::identity_transformer 39 | ) 40 | } 41 | \arguments{ 42 | \item{before, after}{In \code{epoxy_transform_wrap()}, the characters to be added 43 | before and after variables in the template string.} 44 | 45 | \item{engine}{One of \code{"markdown"} (or \code{"md"}), \code{"html"}, or \code{"latex"}. The 46 | default is chosen based on the engine of the chunk where the transform 47 | function is called, or according to the option \code{epoxy.engine}. Caution: 48 | invalid options are silently ignored, falling back to \code{"markdown"}.} 49 | 50 | \item{transformer}{The transformer to apply to the replacement string. This 51 | argument is used for chaining the transformer functions. By providing a 52 | function to this argument you can apply an additional transformation after 53 | the current transformation. In nearly all cases, you can let 54 | \code{epoxy_transform()} handle this for you. The chain ends when 55 | \code{\link[glue:identity_transformer]{glue::identity_transformer()}} is used as the \code{transformer}.} 56 | 57 | \item{syntax}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{engine} instead.} 58 | 59 | \item{.f}{A function, function name or \code{\link[purrr:map]{purrr::map()}}-style inline function.} 60 | 61 | \item{...}{Transformer functions, e.g. 62 | \link{epoxy_transform_bold} or the name of an \pkg{epoxy} 63 | transform function, e.g. \code{"bold"}, or a call to a transform function, e.g. 64 | \code{\link[=epoxy_transform_bold]{epoxy_transform_bold()}}. \code{epoxy_transform()} chains the transformer 65 | functions together, applying the transformers in order from first to last. 66 | 67 | For example, \code{epoxy_transform("bold", "collapse")} results in replaced 68 | strings that are emboldened \emph{and then} collapsed, e.g. \verb{**a** and **b**}. 69 | On the other hand, \code{epoxy_transform("collapse", "bold")} will collapse the 70 | vector \emph{and then} embolden the entire string. 71 | 72 | In \code{epoxy_transform_apply()}, the \code{...} are passed to the underlying call 73 | the underlying function call. 74 | 75 | In \code{epoxy_transform_collapse()}, the \code{...} are ignored.} 76 | 77 | \item{sep, last}{The separator to use when joining the vector elements when 78 | the expression ends with a \code{*}. Elements are separated by \code{sep}, except for 79 | the last two elements, which use \code{last}.} 80 | 81 | \item{language}{In \code{epoxy_transform_collapse()}, \code{language} is passed to 82 | \code{\link[and:and]{and::and()}} or \code{\link[and:and]{and::or()}} to choose the correct and/or phrase and spacing 83 | for the \code{language}. By default, will follow the system language. See 84 | \link[and:and_languages]{and::and_languages} for supported languages.} 85 | } 86 | \value{ 87 | A function of \code{text} and \code{envir} suitable for the \code{.transformer} argument of 88 | \code{\link[glue:glue]{glue::glue()}}. 89 | } 90 | \description{ 91 | These transformers are useful for applying the same transformation to every 92 | replacement in the template. 93 | } 94 | \section{Functions}{ 95 | \itemize{ 96 | \item \code{epoxy_transform_wrap()}: Wrap variables with text added before or 97 | after the inline expression. 98 | 99 | \item \code{epoxy_transform_bold()}: Embolden variables using \verb{**} in 100 | markdown, \verb{} in HTML, or \verb{\\textbf\{\}} in LaTeX. 101 | 102 | \item \code{epoxy_transform_italic()}: Italicize variables using \verb{_} in 103 | markdown, \verb{} in HTML, or \verb{\\emph\{\}} in LaTeX. 104 | 105 | \item \code{epoxy_transform_apply()}: Apply a function to all replacement 106 | expressions. 107 | 108 | \item \code{epoxy_transform_code()}: Code format variables using \verb{``} in 109 | markdown, \verb{} in HTML, or \verb{\\texttt\{\}} in LaTeX. 110 | 111 | \item \code{epoxy_transform_collapse()}: Collapse vector variables with a 112 | succinct syntax (but see \code{\link[=epoxy_transform_inline]{epoxy_transform_inline()}} for a more readable 113 | option). 114 | 115 | }} 116 | \examples{ 117 | abc <- c("a", "b", "c") 118 | 119 | epoxy("{abc}", .transformer = epoxy_transform_wrap("'")) 120 | 121 | epoxy("{abc}", .transformer = epoxy_transform_bold()) 122 | 123 | epoxy("{abc}", .transformer = epoxy_transform_italic()) 124 | 125 | epoxy("{abc}", .transformer = epoxy_transform_code()) 126 | 127 | epoxy("{abc}", .transformer = epoxy_transform_apply(toupper)) 128 | } 129 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at garrick@adenbuie.com. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | . 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][https://github.com/mozilla/inclusion]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | . Translations are available at . 125 | 126 | [homepage]: https://www.contributor-covenant.org 127 | -------------------------------------------------------------------------------- /tests/testthat/test-epoxy_transform_inline.R: -------------------------------------------------------------------------------- 1 | # test_that 2 | 3 | describe("epoxy_transform_inline()", { 4 | it("applies transformations in order from outer to inner", { 5 | expect_equal( 6 | epoxy("{.strong {.dollar 1234}}"), 7 | "**$1,234**" 8 | ) 9 | 10 | expect_error( 11 | epoxy("{.dollar {.strong 1234}}") 12 | ) 13 | }) 14 | 15 | it("applies user-supplied format", { 16 | expect_equal( 17 | epoxy( 18 | "{ .test letters }", 19 | .transformer = epoxy_transform_inline( 20 | .test = function(x) "PASS" 21 | ) 22 | ), 23 | "PASS" 24 | ) 25 | }) 26 | 27 | it("applies user-supplied format over-riding internal alias", { 28 | expect_equal( 29 | epoxy( 30 | "{ .bold letters[1] }", 31 | .transformer = epoxy_transform_inline( 32 | .bold = function(x) "PASS" 33 | ) 34 | ), 35 | "PASS" 36 | ) 37 | 38 | expect_equal( 39 | epoxy( 40 | "{ .bold letters[1] }", 41 | .transformer = epoxy_transform_inline( 42 | .strong = function(x) "PASS" 43 | ) 44 | ), 45 | "PASS" 46 | ) 47 | 48 | expect_equal( 49 | epoxy( 50 | "{ .strong letters[1] }", 51 | .transformer = epoxy_transform_inline( 52 | .bold = function(x) "PASS" 53 | ) 54 | ), 55 | "**a**" 56 | ) 57 | }) 58 | 59 | it("applies squote and dquote", { 60 | expect_equal( 61 | epoxy("{.squote letters[1]}", .transformer = "inline"), 62 | "'a'" 63 | ) 64 | 65 | expect_equal( 66 | epoxy("{.dquote letters[1]}", .transformer = "inline"), 67 | '"a"' 68 | ) 69 | 70 | # from ?sQuote 71 | opts <- options(epoxy.fancy_quotes = c("\xc2\xab", "\xc2\xbb", "\xc2\xbf", "?")) 72 | on.exit(options(opts)) 73 | 74 | expect_equal( 75 | epoxy("{.squote letters[1]}", .transformer = "inline"), 76 | "\xc2\xaba\xc2\xbb" 77 | ) 78 | 79 | expect_equal( 80 | epoxy("{.dquote letters[1]}", .transformer = "inline"), 81 | '\xc2\xbfa?' 82 | ) 83 | }) 84 | 85 | it("applies .sentence and .sc", { 86 | start <- "it was a dark and stormy night" 87 | expect_equal( 88 | epoxy("{.sentence start}", .transformer = "inline"), 89 | "It was a dark and stormy night" 90 | ) 91 | 92 | expect_equal( 93 | epoxy("{.sc start}", .transformer = "inline"), 94 | "It was a dark and stormy night" 95 | ) 96 | }) 97 | 98 | it("errors if a non-dotted argument name is provided", { 99 | expect_snapshot_error( 100 | epoxy_transform_inline( 101 | this_thing = function(x) "bad", 102 | that_thing = function(x) "also bad" 103 | ) 104 | ) 105 | }) 106 | 107 | it("errors if an unnamed argument is provided", { 108 | expect_snapshot_error( 109 | epoxy_transform_inline("bad thing") 110 | ) 111 | }) 112 | 113 | it("passes text through if no transformation is found", { 114 | expect_equal( 115 | epoxy("{.nope letters[1]}", .transformer = "inline"), 116 | "a" 117 | ) 118 | }) 119 | 120 | it("returns an error if transformation fails", { 121 | expect_snapshot_error( 122 | epoxy( 123 | "{.blam letters[1]}", 124 | .transformer = epoxy_transform_inline( 125 | .blam = function(x) stop("this error is expected in the text") 126 | ) 127 | ) 128 | ) 129 | }) 130 | 131 | it("returns an error if evaluating the text fails", { 132 | expect_error( 133 | epoxy( 134 | "{.blam stop('passed test')}", 135 | .transformer = epoxy_transform_inline( 136 | .blam = function(x) stop("this error is not expected in the text") 137 | ) 138 | ), 139 | "passed test" 140 | ) 141 | }) 142 | }) 143 | 144 | test_that("epoxy_html() handles internal html at several levels", { 145 | h <- "" 146 | 147 | expect_equal( 148 | format(epoxy_html("{{span {{.strong h }} }}")), 149 | "<foo>" 150 | ) 151 | 152 | expect_equal( 153 | format(epoxy_html("{{span h }}")), 154 | "<foo>" 155 | ) 156 | 157 | expect_equal( 158 | format(epoxy_html("{{span !!h }}")), 159 | "" 160 | ) 161 | }) 162 | 163 | describe("epoxy internal inline formatters", { 164 | it("epoxy_comma() collapses with a comma", { 165 | expect_equal( 166 | epoxy("{.comma letters[1:3]}", .transformer = "inline"), 167 | "a, b, c" 168 | ) 169 | }) 170 | 171 | it("epoxy_bold() emboldens text", { 172 | expect_equal( 173 | epoxy("{.strong letters[1]}", .transformer = "inline"), 174 | "**a**" 175 | ) 176 | 177 | expect_equal( 178 | epoxy_html("{{.strong letters[1]}}", .transformer = "inline"), 179 | "a" 180 | ) 181 | 182 | expect_equal( 183 | epoxy_latex("<<.strong letters[1]>>", .transformer = "inline"), 184 | "\\textbf{a}" 185 | ) 186 | }) 187 | 188 | it("epoxy_italic() italicizes text", { 189 | expect_equal( 190 | epoxy("{.italic letters[1]}", .transformer = "inline"), 191 | "_a_" 192 | ) 193 | 194 | expect_equal( 195 | epoxy_html("{{.italic letters[1]}}", .transformer = "inline"), 196 | "a" 197 | ) 198 | 199 | expect_equal( 200 | epoxy_latex("<<.italic letters[1]>>", .transformer = "inline"), 201 | "\\emph{a}" 202 | ) 203 | }) 204 | 205 | it("epoxy_code() formats text as code", { 206 | expect_equal( 207 | epoxy("{.code letters[1]}", .transformer = "inline"), 208 | "`a`" 209 | ) 210 | 211 | expect_equal( 212 | epoxy_html("{{.code letters[1]}}", .transformer = "inline"), 213 | "a" 214 | ) 215 | 216 | expect_equal( 217 | epoxy_latex("<<.code letters[1]>>", .transformer = "inline"), 218 | "\\texttt{a}" 219 | ) 220 | }) 221 | }) 222 | 223 | test_that("detect_wrapped_delims", { 224 | expect_true(detect_wrapped_delims("{{ foo }}", open = "{{", close = "}}")) 225 | expect_true(detect_wrapped_delims("[x]", open = "[", close = "]")) 226 | expect_true(detect_wrapped_delims("{x]", open = "{", close = "]")) 227 | expect_true(detect_wrapped_delims("{x}", open = "{", close = "}")) 228 | 229 | expect_false(detect_wrapped_delims("{{ foo }}", open = "[", close = "]")) 230 | expect_false(detect_wrapped_delims("[x]", open = "{", close = "}")) 231 | expect_false(detect_wrapped_delims("x")) 232 | 233 | expect_false(detect_wrapped_delims("{{ x }}", open = "{", close = "}")) 234 | expect_false(detect_wrapped_delims("{{ x }", open = "{", close = "}")) 235 | expect_false(detect_wrapped_delims("{ x }}", open = "{", close = "}")) 236 | }) 237 | -------------------------------------------------------------------------------- /tests/testthat/test-engines.R: -------------------------------------------------------------------------------- 1 | describe("knitr engines", { 2 | opts <- list( 3 | echo = FALSE, 4 | eval = TRUE 5 | ) 6 | 7 | it("works with knitr_engine_epoxy()", { 8 | opts$code <- "{.bold 'hello'}" 9 | opts$engine <- "epoxy" 10 | 11 | expect_equal( 12 | knitr_engine_epoxy(opts), 13 | "**hello**\n" 14 | ) 15 | }) 16 | 17 | it("works with knitr_engine_epoxy_html()", { 18 | opts$code <- "{{.bold 'hello'}}" 19 | opts$engine <- "epoxy_html" 20 | 21 | expect_equal( 22 | knitr_engine_epoxy_html(opts), 23 | "```{=html}\nhello\n```\n" 24 | ) 25 | }) 26 | 27 | it("works with knitr_engine_epoxy_latex()", { 28 | opts$code <- "<<.bold 'hello'>>" 29 | opts$engine <- "epoxy_latex" 30 | 31 | expect_equal( 32 | knitr_engine_epoxy_latex(opts), 33 | "```{=latex}\n\\textbf{hello}\n```\n" 34 | ) 35 | }) 36 | 37 | describe("use data chunk options", { 38 | opts$code <- "{x} and {y}" 39 | opts$engine <- "epoxy" 40 | opts$.data <- data.frame(x = 1:2, y = 3:4) 41 | 42 | it("accepts a data frame in the .data chunk option", { 43 | expect_equal( 44 | knitr_engine_epoxy(opts), 45 | "1 and 3\n2 and 4\n" 46 | ) 47 | }) 48 | 49 | it("supports legacy data chunk option", { 50 | opts$data <- opts$.data 51 | opts$.data <- NULL 52 | 53 | lifecycle::expect_deprecated( 54 | expect_equal( 55 | knitr_engine_epoxy(opts), 56 | "1 and 3\n2 and 4\n" 57 | ) 58 | ) 59 | }) 60 | 61 | it("prefers .data over data", { 62 | opts$data <- data.frame(x = 5:6, y = 7:8) 63 | lifecycle::expect_deprecated( 64 | expect_equal( 65 | knitr_engine_epoxy(opts), 66 | "1 and 3\n2 and 4\n" 67 | ) 68 | ) 69 | }) 70 | 71 | it("handles list-column subsetting", { 72 | opts$.data <- data.frame(x = 1:2) 73 | opts$.data$a <- list(list(y = 3), list(y = 4)) 74 | opts$code <- "{x} and {a$y}" 75 | 76 | expect_equal( 77 | knitr_engine_epoxy(opts), 78 | "1 and 3\n2 and 4\n" 79 | ) 80 | }) 81 | }) 82 | }) 83 | 84 | test_that("whisker template works", { 85 | # standard usage ---- 86 | rmd <- ' 87 | ```{r echo=FALSE} 88 | library(epoxy) 89 | data <- list(name = "Chris", value = 1000, taxed = 600, in_ca = TRUE) 90 | ``` 91 | 92 | ```{whisker .data = data, data_asis = TRUE, echo=FALSE} 93 | Hello {{name}}, 94 | You have just won ${{value}}! 95 | {{#in_ca}} 96 | Well, ${{taxed}}, after taxes. 97 | {{/in_ca}} 98 | ``` 99 | ' 100 | 101 | expect_equal( 102 | render_rmd(rmd), 103 | "Hello Chris, You have just won $1000! Well, $600, after taxes." 104 | ) 105 | 106 | # base case is the same as `data_asis = TRUE` ---- 107 | rmd <- ' 108 | ```{r include=FALSE} 109 | library(epoxy) 110 | knitr::opts_chunk$set(echo = FALSE) 111 | data <- list(name = "Chris", value = 1000, taxed = 600, in_ca = TRUE) 112 | ``` 113 | 114 | ```{whisker .data = data, echo=FALSE} 115 | Hello {{name}}, 116 | You have just won ${{value}}! 117 | {{#in_ca}} 118 | Well, ${{taxed}}, after taxes. 119 | {{/in_ca}} 120 | ``` 121 | ' 122 | 123 | expect_equal( 124 | render_rmd(rmd), 125 | "Hello Chris, You have just won $1000! Well, $600, after taxes." 126 | ) 127 | 128 | # with multiple values per list item ---- 129 | rmd <- ' 130 | ```{r include=FALSE} 131 | library(epoxy) 132 | knitr::opts_chunk$set(echo = FALSE) 133 | data <- data.frame(name = c("Chris", "Jane"), value = c(1000, 2000), taxed = c(600, 600), in_ca = c(TRUE, FALSE)) 134 | ``` 135 | 136 | ```{whisker .data = data, echo=FALSE} 137 | Hello {{name}}, 138 | You have just won ${{value}}! 139 | {{#in_ca}} 140 | Well, ${{taxed}}, after taxes. 141 | {{/in_ca}} 142 | ``` 143 | ' 144 | 145 | expect_equal( 146 | render_rmd(rmd), 147 | c("Hello Chris, You have just won $1000! Well, $600, after taxes.", 148 | "", 149 | "Hello Jane, You have just won $2000!" 150 | ) 151 | ) 152 | 153 | # NULL list items are ignored 154 | rmd <- ' 155 | ```{r include=FALSE} 156 | library(epoxy) 157 | knitr::opts_chunk$set(echo = FALSE) 158 | data <- data.frame(name = c("Chris", "Jane"), value = c(1000, 2000), taxed = c(600, 600)) 159 | ``` 160 | 161 | ```{whisker .data = data, echo=FALSE} 162 | Hello {{name}}, 163 | You have just won ${{value}}! 164 | {{#in_ca}} 165 | Well, ${{taxed}}, after taxes. 166 | {{/in_ca}} 167 | ``` 168 | ' 169 | 170 | expect_equal( 171 | render_rmd(rmd), 172 | c("Hello Chris, You have just won $1000!", 173 | "", 174 | "Hello Jane, You have just won $2000!" 175 | ) 176 | ) 177 | 178 | # Use .vectorized = TRUE for lists 179 | rmd <- ' 180 | ```{r include=FALSE} 181 | library(epoxy) 182 | knitr::opts_chunk$set(echo = FALSE) 183 | data <- list(name = c("Chris", "Jane"), value = 1000, taxed = c(600, 600), in_ca = NULL) 184 | ``` 185 | 186 | ```{whisker .data = data, .vectorized = TRUE, echo=FALSE} 187 | Hello {{name}}, 188 | You have just won ${{value}}! 189 | {{#in_ca}} 190 | Well, ${{taxed}}, after taxes. 191 | {{/in_ca}} 192 | ``` 193 | ' 194 | 195 | expect_equal( 196 | render_rmd(rmd), 197 | c("Hello Chris, You have just won $1000!", 198 | "", 199 | "Hello Jane, You have just won $1000!" 200 | ) 201 | ) 202 | }) 203 | 204 | 205 | describe("chunk engine deprecations", { 206 | it ("warns about `epoxy_style` deprecation", { 207 | lifecycle::expect_deprecated( 208 | deprecate_epoxy_style_chunk_option(list(epoxy_style = "bold")) 209 | ) 210 | }) 211 | 212 | it ("warns about `glue_data` chunk option deprecation", { 213 | lifecycle::expect_defunct( 214 | deprecate_glue_data_chunk_option(list(glue_data = list())) 215 | ) 216 | }) 217 | 218 | it ("warns about `glue` chunk engine usage", { 219 | lifecycle::expect_deprecated( 220 | deprecate_glue_engine_prefix(list(engine = "glue")) 221 | ) 222 | }) 223 | 224 | it ("warns about `glue` chunk engine prefix", { 225 | lifecycle::expect_deprecated( 226 | deprecate_glue_engine_prefix(list(engine = "glue_html")), 227 | "epoxy_html" 228 | ) 229 | 230 | lifecycle::expect_deprecated( 231 | deprecate_glue_engine_prefix(list(engine = "glue_latex")), 232 | "epoxy_latex" 233 | ) 234 | }) 235 | }) 236 | 237 | test_that(".collapse chunk option", { 238 | rmd <- test_path("rmds", "use-chunk_collapse.Rmd") 239 | 240 | res <- render_rmd(rmd) 241 | 242 | expect_equal( 243 | res[[1]], 244 | "one followed by two == three followed by four" 245 | ) 246 | 247 | expect_equal( 248 | res[[3]], 249 | "one followed by two || three followed by four" 250 | ) 251 | }) 252 | -------------------------------------------------------------------------------- /tests/testthat/test-epoxy_use.R: -------------------------------------------------------------------------------- 1 | # test_that() 2 | 3 | describe("epoxy_use_chunk()", { 4 | 5 | it("can be called in an R chunk", { 6 | expect_equal( 7 | render_rmd(test_path("rmds", "use-chunk_chunk.Rmd")), 8 | c( 9 | "one followed by two", 10 | "", 11 | "three followed by four" 12 | ) 13 | ) 14 | }) 15 | 16 | it("uses `.data` in the expected order", { 17 | res <- render_rmd(test_path("rmds", "use-chunk_chunk-opts.Rmd")) 18 | 19 | # uses the chunk option of the epoxy chunk 20 | expect_equal(res[1], "three followed by four") 21 | 22 | # uses the `.data` argument of `epoxy_use_chunk()` 23 | expect_equal(res[3], "five followed by six") 24 | 25 | # uses the `.data` argument even if the chunk has a `.data` option 26 | expect_equal(res[5], "seven followed by eight") 27 | 28 | # uses the `.data` chunk option if the argument isn't provided 29 | expect_equal(res[7], "nine followed by ten") 30 | 31 | # inline chunks fall back to the template chunks' .data option 32 | expect_equal(res[9], "three followed by four") 33 | }) 34 | 35 | it("picks the correct default transformer function", { 36 | epoxy_transform_set(epoxy_transform_bold, engine = "md") 37 | epoxy_transform_set(epoxy_transform_italic, engine = "html") 38 | epoxy_transform_set(epoxy_transform_code, engine = "latex") 39 | on.exit({ epoxy_transform_set(NULL) }) 40 | 41 | picked_md <- NULL 42 | picked_html <- NULL 43 | picked_latex <- NULL 44 | 45 | render_basic_rmd( 46 | "```{epoxy md}", 47 | "{picked_md <- epoxy_default_transformer(); 'ignore'}", 48 | "```", 49 | "```{epoxy_html html}", 50 | "{{ picked_html <- epoxy_default_transformer(); 'ignore' }}", 51 | "```", 52 | "```{epoxy_latex latex}", 53 | "<< picked_latex <- epoxy_default_transformer(); 'ignore' >>", 54 | "```", 55 | "```{r}", 56 | "epoxy_use_chunk(label = 'md')", 57 | "epoxy_use_chunk(label = 'html')", 58 | "epoxy_use_chunk(label = 'latex')", 59 | "```" 60 | ) 61 | 62 | expect_equal( 63 | picked_md, 64 | epoxy_transform_bold() 65 | ) 66 | expect_equal( 67 | picked_html, 68 | with_epoxy_engine("html", epoxy_transform_italic()) 69 | ) 70 | expect_equal( 71 | picked_latex, 72 | with_epoxy_engine("latex", epoxy_transform_code()) 73 | ) 74 | }) 75 | 76 | it("throws for bad labels", { 77 | expect_error(epoxy_use_chunk(label = 27)) 78 | expect_error(epoxy_use_chunk(label = c("bad", "label"))) 79 | expect_error(epoxy_use_chunk(label = NULL)) 80 | }) 81 | 82 | it("throws for unknown labels", { 83 | render_basic_rmd( 84 | "```{r}", 85 | "expect_error(epoxy_use_chunk(label = 'bad-label'))", 86 | "```" 87 | ) 88 | }) 89 | }) 90 | 91 | describe("epoxy_use_file()", { 92 | template <- test_path("rmds", "use-file_example-1.md") 93 | template <- normalizePath(template) 94 | 95 | data1 <- list(one = "first", two = "second", three = "third") 96 | data2 <- list(one = "apple", two = "banana", three = "mango") 97 | 98 | it("reads from a file", { 99 | expect_equal( 100 | epoxy_use_file(data1, template), 101 | knitr::asis_output("first then second then third") 102 | ) 103 | expect_equal( 104 | epoxy_use_file(data2, template), 105 | knitr::asis_output("apple then banana then mango") 106 | ) 107 | }) 108 | 109 | it("works inside an Rmd", { 110 | rmd_res <- render_basic_rmd( 111 | "```{r echo=FALSE}", 112 | "epoxy_use_file(data1, template)", 113 | "```" 114 | ) 115 | 116 | expect_equal( 117 | rmd_res, 118 | "first then second then third" 119 | ) 120 | 121 | rmd_res2 <- render_basic_rmd( 122 | "```{r echo=FALSE, .data = data2}", 123 | "epoxy_use_file(file = template)", 124 | "```" 125 | ) 126 | 127 | expect_equal( 128 | rmd_res2, 129 | "apple then banana then mango" 130 | ) 131 | }) 132 | 133 | it("allows .data to be defined in the yaml header", { 134 | template <- test_path("rmds", "use-file_example-2.md") 135 | 136 | expect_equal( 137 | epoxy_use_file(file = template), 138 | knitr::asis_output("one fish, two fish, red fish, blue fish") 139 | ) 140 | 141 | expect_equal( 142 | epoxy_use_file( 143 | .data = list(one = "one", two = "two", three = "red", four = "blue"), 144 | file = template 145 | ), 146 | knitr::asis_output("one, two, red, blue") 147 | ) 148 | }) 149 | 150 | it("sets html engine via engine yaml option", { 151 | template_html <- test_path("rmds", "use-file_html.md") 152 | 153 | expect_equal( 154 | epoxy_use_file( 155 | .data = list( 156 | link = "https://example.com", 157 | text = "example link" 158 | ), 159 | file = template_html 160 | ), 161 | knitr::asis_output("example link") 162 | ) 163 | }) 164 | 165 | it("sets latex engine via engine yaml option", { 166 | template_latex <- test_path("rmds", "use-file_latex.md") 167 | 168 | expect_equal( 169 | epoxy_use_file( 170 | .data = list( 171 | link = "https://example.com", 172 | text = "example link" 173 | ), 174 | file = template_latex 175 | ), 176 | knitr::asis_output("\\href{https://example.com}{example link}") 177 | ) 178 | }) 179 | 180 | it("works without a yaml header", { 181 | template_no_header <- test_path("rmds", "use-file_example-no-yaml.md") 182 | 183 | expect_equal( 184 | epoxy_use_file( 185 | .data = list( 186 | one = "first", 187 | two = "second", 188 | three = "third", 189 | four = "fourth" 190 | ), 191 | file = template_no_header 192 | ), 193 | knitr::asis_output("first, second, third, fourth") 194 | ) 195 | }) 196 | 197 | it("errors when the file doesn't exist", { 198 | expect_error(epoxy_use_file(file = "bad-file.md")) 199 | }) 200 | 201 | it("errors when the file is essentially empty", { 202 | tmpfile <- tempfile(fileext = ".md") 203 | on.exit(unlink(tmpfile)) 204 | 205 | writeLines(c("", " ", "\t\t", ""), tmpfile) 206 | expect_error(epoxy_use_file(file = tmpfile)) 207 | }) 208 | 209 | it("allows the user to set the engine from the fn call", { 210 | tmpl <- test_path("rmds", "use-file_html.md") 211 | expect_equal( 212 | epoxy_use_file(file = tmpl, engine = "epoxy"), 213 | knitr::asis_output( 214 | gsub( 215 | "}}", "}", 216 | gsub( 217 | "{{", "{", 218 | read_body_without_yaml(tmpl), 219 | fixed = TRUE 220 | ), 221 | fixed = TRUE 222 | ) 223 | ) 224 | ) 225 | }) 226 | }) 227 | -------------------------------------------------------------------------------- /inst/srcjs/output-epoxy.js: -------------------------------------------------------------------------------- 1 | /* globals Shiny,$,CustomEvent */ 2 | 3 | class EpoxyHTML extends window.HTMLElement { 4 | static is_set_global_event_listener = false 5 | 6 | last = null 7 | 8 | constructor () { 9 | super() 10 | 11 | if (EpoxyHTML.is_set_global_event_listener) return 12 | 13 | window.addEventListener('epoxy-message.html', ev => { 14 | // {example: {thing: "dolphin", color: "blue", height: 5}} 15 | EpoxyHTML.update_all(ev.detail) 16 | }) 17 | EpoxyHTML.is_set_global_event_listener = true 18 | } 19 | 20 | static update_all (data, partial = false) { 21 | // { [id]: { [itemKey]: value }} 22 | // { example: { thing: "dolphin", color: "blue", height: 5 }} 23 | if (partial) { 24 | for (const key of Object.keys(data)) { 25 | data[key].__partial = true 26 | } 27 | } 28 | 29 | for (const [key, value] of Object.entries(data)) { 30 | const el = document.getElementById(key) 31 | if (!el) { 32 | console.warn( 33 | `[epoxy-html] [${key}] No element found with id`, { [key]: value } 34 | ) 35 | continue 36 | } 37 | el.update(value) 38 | } 39 | } 40 | 41 | /* ---- Private methods ---- */ 42 | _is_empty (x) { 43 | if (x === undefined || x === null) return true 44 | if (typeof x === 'number') return false 45 | if (typeof x === 'string') return false 46 | if (typeof x === 'boolean') return false 47 | if (Array.isArray(x) && x.length) return false 48 | if (x instanceof Object && Object.keys(x).length) return false 49 | return true 50 | } 51 | 52 | _deep_equal (x, y) { 53 | if (x === y) { 54 | return true 55 | } 56 | 57 | if ( 58 | typeof x !== 'object' || 59 | typeof y !== 'object' || 60 | x === null || 61 | y === null 62 | ) { 63 | return false 64 | } 65 | 66 | const keysX = Object.keys(x) 67 | const keysY = Object.keys(y) 68 | 69 | if (keysX.length !== keysY.length) { 70 | return false 71 | } 72 | 73 | for (const key of keysX) { 74 | if (!keysY.includes(key) || !this._deep_equal(x[key], y[key])) { 75 | return false 76 | } 77 | } 78 | 79 | return true 80 | } 81 | 82 | _remove_item_copies (item) { 83 | const itemKey = item.dataset.epoxyItem 84 | this.querySelectorAll(`[data-epoxy-copy="${itemKey}"]`).forEach(item => 85 | item.parentElement.removeChild(item) 86 | ) 87 | } 88 | 89 | _event_updated (key, data) { 90 | return new CustomEvent('epoxy-updated', { 91 | bubbles: true, 92 | detail: { output: this.id, key, data, outputType: 'html' } 93 | }) 94 | } 95 | 96 | _event_errored (key, data) { 97 | console.error(`[epoxy-html] [${this.id}]: ${data}`) 98 | 99 | return new CustomEvent('epoxy-errored', { 100 | bubbles: true, 101 | detail: { 102 | output: this.id, 103 | key, 104 | message: data, 105 | outputType: 'html' 106 | } 107 | }) 108 | } 109 | 110 | error_classes = ['epoxy-item__error', 'hint--top-right', 'hint--error'] 111 | 112 | _item_clear_error (item) { 113 | this.error_classes.forEach(c => item.classList.remove(c)) 114 | item.removeAttribute('aria-label') 115 | } 116 | 117 | _item_show_error (item, data) { 118 | const itemKey = item.dataset.epoxyItem 119 | this.error_classes.forEach(c => item.classList.add(c)) 120 | this._remove_item_copies(item) 121 | 122 | this._item_update_contents(item, item.dataset.epoxyPlaceholder || '') 123 | item.style.removeProperty('display') 124 | item.setAttribute('aria-label', data) 125 | item.dispatchEvent(this._event_errored(itemKey, data)) 126 | } 127 | 128 | _item_update_contents (item, contents) { 129 | const asHTML = item.dataset.epoxyAsHtml === 'true' 130 | 131 | asHTML ? (item.innerHTML = contents) : (item.textContent = contents) 132 | return item 133 | } 134 | 135 | update (data) { 136 | const items = this.querySelectorAll('[data-epoxy-item]') 137 | 138 | items.forEach(item => { 139 | item.classList.remove('epoxy-item__placeholder') 140 | const itemKey = item.dataset.epoxyItem 141 | 142 | let itemData = data[itemKey] 143 | 144 | if (data.__errors__ && data.__errors__.includes(itemKey)) { 145 | this._item_show_error(item, itemData) 146 | return 147 | } else { 148 | this._item_clear_error(item) 149 | } 150 | 151 | if (this._last && this._deep_equal(itemData, this._last[itemKey])) { 152 | // don't do anything, the value hasn't changed 153 | return 154 | } 155 | 156 | this._remove_item_copies(item) 157 | 158 | if (this._is_empty(itemData)) { 159 | if (data.__partial) { 160 | // This is partial update, so the empty value is ignored. 161 | data[itemKey] = this._last[itemKey] 162 | return 163 | } 164 | item.style.display = 'none' 165 | return 166 | } else { 167 | item.style.removeProperty('display') 168 | } 169 | 170 | if (!(itemData instanceof Array)) { 171 | this._item_update_contents(item, itemData) 172 | item.dispatchEvent(this._event_updated(itemKey, itemData)) 173 | return 174 | } 175 | 176 | // If an array, use the initial item as a pattern 177 | const itemEventUpdated = this._event_updated(itemKey, itemData) 178 | itemEventUpdated.detail.copies = [] 179 | const itemParent = item.parentElement 180 | 181 | this._item_update_contents(item, itemData[0]) 182 | itemData = itemData.slice(1) 183 | 184 | for (const itemDataThis of itemData) { 185 | const itemNew = item.cloneNode() 186 | itemNew.removeAttribute('data-epoxy-item') 187 | itemNew.dataset.epoxyCopy = itemKey 188 | itemParent.insertAdjacentElement('beforeend', itemNew) 189 | this._item_update_contents(itemNew, itemDataThis) 190 | itemEventUpdated.detail.copies.push(itemNew) 191 | } 192 | 193 | item.dispatchEvent(itemEventUpdated) 194 | }) 195 | 196 | this._last = data 197 | this.classList.remove('epoxy-init') 198 | } 199 | } 200 | 201 | window.customElements.define('epoxy-html', EpoxyHTML) 202 | 203 | if (window.Shiny) { 204 | const epoxyOutputBinding = new Shiny.OutputBinding() 205 | 206 | $.extend(epoxyOutputBinding, { 207 | find: function (scope) { 208 | return $(scope).find('epoxy-html') 209 | }, 210 | renderValue: function (el, data) { 211 | el.update(data) 212 | }, 213 | renderError: function (el, err) { 214 | this.clearError(el) 215 | if (err.message !== '') { 216 | console.error(`[epoxy-html] [${el.id}] ${err.message}`) 217 | el.classList.add('epoxy-error') 218 | } 219 | }, 220 | clearError: function (el) { 221 | el.classList.remove('epoxy-error') 222 | } 223 | }) 224 | 225 | Shiny.outputBindings.register(epoxyOutputBinding, 'shiny.ui_epoxy_html') 226 | } 227 | -------------------------------------------------------------------------------- /R/epoxy_transform_html.R: -------------------------------------------------------------------------------- 1 | #' Concise syntax for expressions inside HTML elements 2 | #' 3 | #' @description 4 | #' `epoxy_transform_html()` provides a 5 | #' [pug](https://pughtml.com/what-is-pug-html)-like syntax for expressions in 6 | #' HTML that are wrapped in HTML elements. 7 | #' 8 | #' ## Syntax 9 | #' 10 | #' You can specify the HTML element and its `id` and `class` into which the 11 | #' text of the expression will be placed. The template is to specify the element 12 | #' using the syntax below, followed by the R expression, separated by a space: 13 | #' 14 | #' ``` 15 | #' {{ [][# | ....] expr }} 16 | #' ``` 17 | #' 18 | #' For example, to place the expression in a `
  • ` element with `id = "food"` 19 | #' and `class = "fruit"`, you could write 20 | #' 21 | #' ``` 22 | #' {{ li#food.fruit fruit_name }} 23 | #' ``` 24 | #' 25 | #' Each item in the HTML template is optional: 26 | #' 27 | #' 1. If a specific HTML element is desired, the element name must be first. If 28 | #' no element is specified, the default as set by the `element` argument of 29 | #' [epoxy_transform_html()] will be used. 30 | #' 31 | #' 2. IDs are specified using `#` and only one ID may be present 32 | #' 33 | #' 3. Classes are written using `.` and as many classes as desired are 34 | #' allowed. 35 | #' 36 | #' If the expression is a vector, the same element container will be used for 37 | #' each item in the vector. 38 | #' 39 | #' Finally, if the expression returns HTML, it will be escaped by default. You 40 | #' can either use [htmltools::HTML()] to mark it as safe HTML in R, or you can 41 | #' write `!!expr` in the inline markup: `{{ li#food.fruit !!fruit_name }}`. 42 | #' 43 | #' @examples 44 | #' epoxy_html("
      {{ li letters[1:3] }}
    ") 45 | #' epoxy_html("
      {{ li.alpha letters[1:3] }}
    ") 46 | #' epoxy_html("
      {{ li#my-letter letters[7] }}
    ") 47 | #' 48 | #' # The default element is used if no element is directly requested 49 | #' epoxy_html("My name starts with {{ .name-letter letters[7] }}") 50 | #' 51 | #' epoxy_html( 52 | #' "{{ h3#title title }}", 53 | #' title = "Epoxy for HTML" 54 | #' ) 55 | #' 56 | #' # If your replacement text contains HTML, it's escaped by default. 57 | #' hello <- "Hi there!" 58 | #' epoxy_html("{{ hello }}") 59 | #' 60 | #' # You can use !! inline to mark the text as safe HTML... 61 | #' epoxy_html("{{ !!hello }}") 62 | #' epoxy_html("{{ button !!hello }}") 63 | #' 64 | #' # ...or you can use htmltools::HTML() to mark it as safe HTML in R. 65 | #' hello <- htmltools::HTML("Hi there!") 66 | #' epoxy_html("{{ hello }}") 67 | #' 68 | #' @param class `[character()]`\cr Additional classes to be added to the inline 69 | #' HTML element. 70 | #' @param element `[character()`\cr The default HTML element tag name to be used 71 | #' when an element isn't specified in the expression. 72 | #' @param collapse `[logical(1)]`\cr If `TRUE`, transformed HTML outputs will be 73 | #' collapsed into a single character string. This is helpful when you're 74 | #' including the value of a vector within an outer HTML tag. Use `collapse = 75 | #' FALSE` to return a vector of HTML character strings instead, which follows 76 | #' what you'd typically expect from `glue::glue()`, i.e. when you want to 77 | #' repeat the outer wrapping text for each element of the vector. 78 | #' 79 | #' @inheritParams epoxy_transform_inline 80 | #' @inherit epoxy_transform params return 81 | #' 82 | #' @seealso Used by default in [epoxy_html()] 83 | #' @family epoxy's glue transformers 84 | #' @export 85 | epoxy_transform_html <- function( 86 | class = NULL, 87 | element = "span", 88 | collapse = TRUE, 89 | transformer = glue::identity_transformer 90 | ) { 91 | function(text, envir) { 92 | '!DEBUG html {text: "`text`"}' 93 | 94 | if (grepl(epoxy_transform_inline_regex("@"), text, perl = TRUE)) { 95 | return(transformer(text, envir)) 96 | } 97 | 98 | markup <- parse_html_markup(text) 99 | 100 | text <- transformer(markup$item, envir) 101 | if (inherits(text, "html")) { 102 | markup$as_html <- TRUE 103 | } 104 | 105 | is_bare_item <- identical(names(markup), c("item", "as_html")) 106 | if (is_bare_item) { 107 | # regular glue text, no added html markup 108 | if (!markup$as_html) { 109 | text <- escape_html(text) 110 | } 111 | return(text) 112 | } 113 | 114 | tag_name <- markup$element 115 | if (is.null(tag_name)) tag_name <- element 116 | 117 | html <- lapply(text, function(x) { 118 | if (markup$as_html) x <- htmltools::HTML(x) 119 | htmltools::tag( 120 | tag_name, 121 | list(class = class, class = markup$class, id = markup$id, x), 122 | .noWS = c("inside", "outside") 123 | ) 124 | }) 125 | 126 | if (!isTRUE(collapse)) { 127 | # Return a vector of html character strings 128 | return(html_chr(vapply(html, format, character(1)))) 129 | } 130 | 131 | # otherwise, collapse length-1 html tags into a single character string 132 | out <- 133 | if (length(html) == 1) { 134 | html[[1]] 135 | } else { 136 | htmltools::tagList(html) 137 | } 138 | 139 | html_chr(out) 140 | } 141 | } 142 | 143 | html_chr <- function(x) { 144 | if (!is.character(x)) { 145 | x <- format(x) 146 | } 147 | class(x) <- c("html", class(x)) 148 | x 149 | } 150 | 151 | parse_html_markup <- function(x) { 152 | x_og <- x 153 | x <- trimws(x) 154 | n_spaces <- str_count(x, " ") 155 | 156 | if (n_spaces == 0) { 157 | return(parse_placeholder(x)) 158 | } 159 | 160 | # pug-like syntax starts with # (id), . (class), or element name 161 | has_el_syntax <- 162 | substr(x, 1, 1) %in% c("#", "%", ".") || 163 | grepl(html_element_rgx(), x) 164 | 165 | if (!has_el_syntax) { 166 | return(parse_placeholder(x)) 167 | } 168 | 169 | x <- strsplit(x, " ")[[1]] 170 | item_id <- paste(x[-1], collapse = " ") 171 | 172 | rgx_markup <- "(([#%. ]|^)[[:alnum:]_-]+)" 173 | m <- str_extract_all(x[1], rgx_markup)[[1]] 174 | 175 | out <- parse_placeholder(item_id) 176 | 177 | if (!length(m)) { 178 | # should have been caught but just in case 179 | return(out) 180 | } 181 | 182 | for (m_part in m) { 183 | if (grepl("^[.]", m_part)) { 184 | out$class <- c(out$class, sub("^[.]", "", m_part)) 185 | } else if (grepl("^[#%]", m_part)) { 186 | this_id <- sub("^[#%]", "", m_part) 187 | if (!is.null(out$id)) { 188 | rlang::abort(c( 189 | "Multiple IDs were specified, please specify only one ID.", 190 | i = out$id, 191 | x = this_id 192 | )) 193 | } 194 | out$id <- this_id 195 | } else { 196 | if (!is.null(out$element)) { 197 | rlang::abort("Multiple elements were specified, please specify only one element.") 198 | } 199 | if (!m_part %in% names(htmltools::tags)) { 200 | rlang::abort(glue::glue("Unknown tag used in markup: `{m_part}`")) 201 | } 202 | out$element <- m_part 203 | } 204 | } 205 | 206 | if (!is.null(out$class)) { 207 | out$class <- paste(out$class, collapse = " ") 208 | } 209 | 210 | keep_names <- c("item", "element", "class", "id", "as_html") 211 | out <- out[intersect(keep_names, names(out))] 212 | 213 | out 214 | } 215 | 216 | parse_placeholder <- function(x) { 217 | as_html <- grepl("^!!", x) 218 | list( 219 | item = sub("^!!", "", x), 220 | as_html = as_html 221 | ) 222 | } 223 | 224 | html_element_rgx <- function() { 225 | rgx <- paste(names(htmltools::tags), collapse = "|") 226 | sprintf("^(%s)[#%%.[:alnum:]_-]* ", rgx) 227 | } 228 | -------------------------------------------------------------------------------- /R/epoxy_use.R: -------------------------------------------------------------------------------- 1 | #' Reuse a Template Chunk 2 | #' 3 | #' @description 4 | #' Reuse a template from another chunk or file. By calling `epoxy_use_chunk()` 5 | #' in an R chunk or inline R expression, you can reuse a template defined in 6 | #' another chunk in your document. 7 | #' 8 | #' Alternatively, you can store the template in a separate file and use 9 | #' `epoxy_use_file()` to reuse it. When stored in a file, the template file can 10 | #' contain YAML front matter (following the [same rules as pandoc 11 | #' documents](https://pandoc.org/MANUAL.html#extension-yaml_metadata_block)) 12 | #' with options that should be applied when calling an epoxy function. The 13 | #' specific function called by `epoxy_use_file()` can be set via the `engine` 14 | #' option in the YAML front matter; the default is [epoxy()]. 15 | #' 16 | #' @section Use in R Markdown or Quarto: 17 | #' 18 | #' `````` 19 | #' ```{epoxy movie-release} 20 | #' {.emph title} was released in {year}. 21 | #' ``` 22 | #' 23 | #' ```{r} 24 | #' # Re-using the template we defined above 25 | #' epoxy_use_chunk(bechdel[1, ], "movie-release") 26 | #' ``` 27 | #' 28 | #' ```{r} 29 | #' # Using in a dplyr pipeline 30 | #' bechdel |> 31 | #' dplyr::filter(year == 1989) |> 32 | #' epoxy_use_chunk("movie-release") 33 | #' ``` 34 | #' `````` 35 | #' 36 | #' Or you can even use it inline: 37 | #' 38 | #' ``````markdown 39 | #' It's hard to believe that 40 | #' `r epoxy_use_chunk(bechdel[2, ], "movie-release")`. 41 | #' `````` 42 | #' 43 | #' It's hard to believe that 44 | #' _Back to the Future Part II_ was released in 1989. 45 | #' 46 | #' The same template could also be stored in a file, e.g. `movie-release.md`: 47 | #' 48 | #' ```markdown 49 | #' --- 50 | #' engine: epoxy 51 | #' --- 52 | #' 53 | #' {.emph title} was released in {year}. 54 | #' ``` 55 | #' 56 | #' The YAML front matter is used in template files to set options for the 57 | #' template. You can use the `engine` option to choose the epoxy function to be 58 | #' applied to the template, e.g. `engine: epoxy_html` or `engine: epoxy_latex`. 59 | #' By default, `engine: epoxy` is assumed unless otherwise specified. 60 | #' 61 | #' @section Template Options: 62 | #' 63 | #' When rendering a template, `epoxy_use_chunk()` and `epoxy_use_file()` will 64 | #' inherit the options set in a number of different ways. The final template 65 | #' options are determined in the following order, ranked by importance. Options 66 | #' set in a higher-ranked location will override options set in a lower-ranked 67 | #' location. 68 | #' 69 | #' 1. The arguments passed to `epoxy_use_chunk()`, such as `.data` or any 70 | #' arguments passed in the `...`. These options always have preference over 71 | #' options set anywhere else. 72 | #' 73 | #' 1. The chunk options from the chunk where `epoxy_use_chunk()` or 74 | #' `epoxy_use_file()` is called. 75 | #' 76 | #' 1. The chunk options from the template chunk or file. These options typically 77 | #' are relevant to the template itself, such as the engine used or the 78 | #' opening and closing delimiters. 79 | #' 80 | #' 1. Global knitr chunk options for the document. You can set these with 81 | #' `knitr::opts_chunk$set()`, see `?knitr::opts_chunk` for more information. 82 | #' 83 | #' @param label The chunk label, i.e. the human-readable name, of the chunk 84 | #' containing the template string. This chunk should be an `epoxy`, 85 | #' `epoxy_html` or other epoxy-provided chunk type and it must have a label. 86 | #' `epoxy_use_chunk()` will apply the options from this chunk to the template, 87 | #' giving preference to arguments in `epoxy_use_chunk()` or the chunk options 88 | #' where it is called. See the "Template Options" section for more details. 89 | #' @inheritDotParams epoxy 90 | #' @inheritParams epoxy 91 | #' 92 | #' @return A character string of the rendered template based on the `label` 93 | #' chunk. The results are marked as `"asis"` output so that they are treated 94 | #' as regular text rather than being displayed as code results. 95 | #' 96 | #' @family Templating functions 97 | #' @name epoxy_use 98 | #' @export 99 | epoxy_use_chunk <- function(.data = NULL, label, ...) { 100 | if (!rlang::is_string(label)) { 101 | rlang::abort("`label` must be a string") 102 | } 103 | 104 | if (!label %in% knitr::all_labels()) { 105 | rlang::abort(paste0('Unknown chunk label "', label, '"')) 106 | } 107 | 108 | template <- knitr_chunk_get(label) 109 | 110 | epoxy_use_template( 111 | template$code, 112 | .data = .data, 113 | ..., 114 | options = template$opts 115 | ) 116 | } 117 | 118 | #' @param file The template file, i.e. a plain text file, containing the 119 | #' template. An `.md` or `.txt` file extension is recommended. In addition to 120 | #' the template, the file may also contain YAML front matter containing 121 | #' options that are used when rendering the template via [epoxy()]. 122 | #' 123 | #' @rdname epoxy_use 124 | #' @export 125 | epoxy_use_file <- function(.data = NULL, file, ...) { 126 | if (!file.exists(file)) { 127 | rlang::abort(paste0("File '", file, "' does not exist")) 128 | } 129 | 130 | options <- rmarkdown::yaml_front_matter(file) 131 | template <- read_body_without_yaml(file) 132 | 133 | epoxy_use_template( 134 | template, 135 | .data = .data, 136 | ..., 137 | options = options 138 | ) 139 | } 140 | 141 | read_body_without_yaml <- function(path) { 142 | x <- readLines(path) 143 | x_trimmed <- trimws(x) 144 | 145 | if (!any(nzchar(x_trimmed))) { 146 | rlang::abort(paste0("File '", path, "' is empty")) 147 | } 148 | 149 | idx_nzchar <- which(nzchar(x_trimmed))[1] 150 | idx_start <- grep("^---$", x_trimmed) 151 | 152 | if (length(idx_start)) idx_start <- idx_start[1] 153 | 154 | if (length(idx_start) == 0 || idx_nzchar < idx_start) { 155 | return(paste(x, collapse = "\n")) 156 | } 157 | 158 | idx_end <- grep("^([-]{3}|[.]{3})$", x_trimmed) 159 | if (!length(idx_end)) { 160 | return(paste(x, collapse = "\n")) 161 | } 162 | 163 | idx_end <- idx_end[idx_end > idx_start][1] 164 | 165 | idx_body <- which(nzchar(x_trimmed)) 166 | idx_body <- idx_body[idx_body > idx_end][1] 167 | 168 | x <- x[-2:-idx_body + 1] 169 | paste(x, collapse = "\n") 170 | } 171 | 172 | epoxy_use_template <- function( 173 | template, 174 | .data = NULL, 175 | ..., 176 | options = list(), 177 | engine = NULL 178 | ) { 179 | # For options, we want to apply options in this order: 180 | # 0. `.data` from this fn and `eval` from this chunk 181 | # 1. Options from this function call in the ... 182 | # 2. Options specifically on the calling chunk 183 | # 2. Options from the chunk in the template 184 | # 3. Global knitr options in the current environment 185 | opts_fn <- rlang::list2(eval = TRUE, ...) 186 | if (!is.null(.data)) opts_fn[[".data"]] <- .data 187 | 188 | opts_global <- knitr::opts_current$get() 189 | opts_current <- knitr_chunk_specific_options() 190 | 191 | # global << template << current << function 192 | opts <- opts_global 193 | opts <- purrr::list_assign(opts, !!!options) 194 | opts <- purrr::list_assign(opts, !!!opts_current) 195 | opts <- purrr::list_assign(opts, !!!purrr::compact(opts_fn)) 196 | 197 | engine <- engine %||% options$engine %||% "epoxy" 198 | 199 | fn <- switch( 200 | engine, 201 | epoxy = epoxy, 202 | html = , 203 | epoxy_html = epoxy_html, 204 | latex = , 205 | epoxy_latex = epoxy_latex, 206 | mustache = , 207 | whisker = epoxy_mustache, 208 | glue = epoxy, 209 | glue_html = epoxy_html, 210 | glue_latex = epoxy_latex, 211 | { 212 | rlang::warn(c( 213 | glue("Unexpected engine '{engine}', defaulting to `epoxy()`."), 214 | "i" = "Set an epoxy knitr engine in the chunk or file." 215 | )) 216 | epoxy 217 | } 218 | ) 219 | 220 | call <- rlang::call2( 221 | "eval_epoxy_engine", 222 | fn = fn, 223 | code = template, 224 | options = opts 225 | ) 226 | 227 | with_epoxy_engine( 228 | engine, 229 | knitr::asis_output(eval(call)) 230 | ) 231 | } 232 | -------------------------------------------------------------------------------- /man/epoxy_use.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/epoxy_use.R 3 | \name{epoxy_use} 4 | \alias{epoxy_use} 5 | \alias{epoxy_use_chunk} 6 | \alias{epoxy_use_file} 7 | \title{Reuse a Template Chunk} 8 | \usage{ 9 | epoxy_use_chunk(.data = NULL, label, ...) 10 | 11 | epoxy_use_file(.data = NULL, file, ...) 12 | } 13 | \arguments{ 14 | \item{.data}{A data set} 15 | 16 | \item{label}{The chunk label, i.e. the human-readable name, of the chunk 17 | containing the template string. This chunk should be an \code{epoxy}, 18 | \code{epoxy_html} or other epoxy-provided chunk type and it must have a label. 19 | \code{epoxy_use_chunk()} will apply the options from this chunk to the template, 20 | giving preference to arguments in \code{epoxy_use_chunk()} or the chunk options 21 | where it is called. See the "Template Options" section for more details.} 22 | 23 | \item{...}{ 24 | Arguments passed on to \code{\link[=epoxy]{epoxy}} 25 | \describe{ 26 | \item{\code{.transformer}}{A transformer function or transformer chain created with 27 | \code{\link[=epoxy_transform]{epoxy_transform()}}. Alternatively, a character vector of epoxy transformer 28 | names, e.g. \code{c("bold", "collapse")} or a list of epoxy transformers, e.g. 29 | \code{list(epoxy_transform_bold(), epoxy_transform_collapse())}. 30 | 31 | In \pkg{epoxy}, you'll most likely want to use the defaults or consult 32 | \code{\link[=epoxy_transform]{epoxy_transform()}} for more information. See also \code{\link[glue:glue]{glue::glue()}} for more 33 | information on transformers.} 34 | \item{\code{.style}}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Please use \code{.transformer} 35 | instead.} 36 | \item{\code{.open}}{[\code{character(1)}: \sQuote{\\\{}]\cr The opening delimiter around 37 | the template variable or expression. Doubling the full delimiter escapes 38 | it.} 39 | \item{\code{.close}}{[\code{character(1)}: \sQuote{\\\}}]\cr The closing delimiter 40 | around the template variable or expression. Doubling the full delimiter 41 | escapes it.} 42 | \item{\code{.collapse}}{A character string used to collapse a vector result into a 43 | single value. If \code{NULL} (the default), the result is not collapsed.} 44 | \item{\code{.sep}}{[\code{character(1)}: \sQuote{""}]\cr Separator used to separate elements.} 45 | \item{\code{.envir}}{[\code{environment}: \code{parent.frame()}]\cr Environment to evaluate each expression in. Expressions are 46 | evaluated from left to right. If \code{.x} is an environment, the expressions are 47 | evaluated in that environment and \code{.envir} is ignored. If \code{NULL} is passed, it is equivalent to \code{\link[=emptyenv]{emptyenv()}}.} 48 | \item{\code{.na}}{[\code{character(1)}: \sQuote{NA}]\cr Value to replace \code{NA} values 49 | with. If \code{NULL} missing values are propagated, that is an \code{NA} result will 50 | cause \code{NA} output. Otherwise the value is replaced by the value of \code{.na}.} 51 | \item{\code{.null}}{[\code{character(1)}: \sQuote{character()}]\cr Value to replace 52 | NULL values with. If \code{character()} whole output is \code{character()}. If 53 | \code{NULL} all NULL values are dropped (as in \code{paste0()}). Otherwise the 54 | value is replaced by the value of \code{.null}.} 55 | \item{\code{.comment}}{[\code{character(1)}: \sQuote{#}]\cr Value to use as the comment 56 | character.} 57 | \item{\code{.literal}}{[\code{boolean(1)}: \sQuote{FALSE}]\cr Whether to treat single or 58 | double quotes, backticks, and comments as regular characters (vs. as 59 | syntactic elements), when parsing the expression string. Setting \code{.literal = TRUE} probably only makes sense in combination with a custom 60 | \code{.transformer}, as is the case with \code{glue_col()}. Regard this argument 61 | (especially, its name) as experimental.} 62 | \item{\code{.trim}}{[\code{logical(1)}: \sQuote{TRUE}]\cr Whether to trim the input 63 | template with \code{\link[glue:trim]{trim()}} or not.} 64 | }} 65 | 66 | \item{file}{The template file, i.e. a plain text file, containing the 67 | template. An \code{.md} or \code{.txt} file extension is recommended. In addition to 68 | the template, the file may also contain YAML front matter containing 69 | options that are used when rendering the template via \code{\link[=epoxy]{epoxy()}}.} 70 | } 71 | \value{ 72 | A character string of the rendered template based on the \code{label} 73 | chunk. The results are marked as \code{"asis"} output so that they are treated 74 | as regular text rather than being displayed as code results. 75 | } 76 | \description{ 77 | Reuse a template from another chunk or file. By calling \code{epoxy_use_chunk()} 78 | in an R chunk or inline R expression, you can reuse a template defined in 79 | another chunk in your document. 80 | 81 | Alternatively, you can store the template in a separate file and use 82 | \code{epoxy_use_file()} to reuse it. When stored in a file, the template file can 83 | contain YAML front matter (following the \href{https://pandoc.org/MANUAL.html#extension-yaml_metadata_block}{same rules as pandoc documents}) 84 | with options that should be applied when calling an epoxy function. The 85 | specific function called by \code{epoxy_use_file()} can be set via the \code{engine} 86 | option in the YAML front matter; the default is \code{\link[=epoxy]{epoxy()}}. 87 | } 88 | \section{Use in R Markdown or Quarto}{ 89 | 90 | 91 | \if{html}{\out{
    }}\preformatted{```\{epoxy movie-release\} 92 | \{.emph title\} was released in \{year\}. 93 | ``` 94 | 95 | ```\{r\} 96 | # Re-using the template we defined above 97 | epoxy_use_chunk(bechdel[1, ], "movie-release") 98 | ``` 99 | 100 | ```\{r\} 101 | # Using in a dplyr pipeline 102 | bechdel |> 103 | dplyr::filter(year == 1989) |> 104 | epoxy_use_chunk("movie-release") 105 | ``` 106 | }\if{html}{\out{
    }} 107 | 108 | Or you can even use it inline: 109 | 110 | \if{html}{\out{
    }}\preformatted{It's hard to believe that 111 | `r epoxy_use_chunk(bechdel[2, ], "movie-release")`. 112 | }\if{html}{\out{
    }} 113 | 114 | It's hard to believe that 115 | \emph{Back to the Future Part II} was released in 1989. 116 | 117 | The same template could also be stored in a file, e.g. \code{movie-release.md}: 118 | 119 | \if{html}{\out{
    }}\preformatted{--- 120 | engine: epoxy 121 | --- 122 | 123 | \{.emph title\} was released in \{year\}. 124 | }\if{html}{\out{
    }} 125 | 126 | The YAML front matter is used in template files to set options for the 127 | template. You can use the \code{engine} option to choose the epoxy function to be 128 | applied to the template, e.g. \code{engine: epoxy_html} or \code{engine: epoxy_latex}. 129 | By default, \code{engine: epoxy} is assumed unless otherwise specified. 130 | } 131 | 132 | \section{Template Options}{ 133 | 134 | 135 | When rendering a template, \code{epoxy_use_chunk()} and \code{epoxy_use_file()} will 136 | inherit the options set in a number of different ways. The final template 137 | options are determined in the following order, ranked by importance. Options 138 | set in a higher-ranked location will override options set in a lower-ranked 139 | location. 140 | \enumerate{ 141 | \item The arguments passed to \code{epoxy_use_chunk()}, such as \code{.data} or any 142 | arguments passed in the \code{...}. These options always have preference over 143 | options set anywhere else. 144 | \item The chunk options from the chunk where \code{epoxy_use_chunk()} or 145 | \code{epoxy_use_file()} is called. 146 | \item The chunk options from the template chunk or file. These options typically 147 | are relevant to the template itself, such as the engine used or the 148 | opening and closing delimiters. 149 | \item Global knitr chunk options for the document. You can set these with 150 | \code{knitr::opts_chunk$set()}, see \code{?knitr::opts_chunk} for more information. 151 | } 152 | } 153 | 154 | \concept{Templating functions} 155 | --------------------------------------------------------------------------------