├── .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 | -------------------------------------------------------------------------------- /man/figures/lifecycle-defunct.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /man/figures/lifecycle-archived.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /man/figures/lifecycle-maturing.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /man/figures/lifecycle-questioning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | `<
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}}
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 |
36 | Use epoxy to blend data and prose with inline templating and formatting.
39 |
46 | Use epoxy for reactive templating and targeted updates in Shiny apps.
49 |
56 | Use epoxy in your R scripts, anywhere you've used glue.
59 |.+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 Get up and running with epoxy in reports or Shiny apps. Function reference with usage and examples. Longer posts and tutorials about using epoxy in your reports and apps. extra-strength glue for scripts, reports, and apps.
25 |
26 | Do you have any favorite fruits? Get up and running with epoxy in reports or Shiny apps. Function reference with usage and examples. Longer posts and tutorials about using epoxy in your reports and apps.
18 |
19 |
20 |
21 |
22 |
23 |
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 |
Get started
46 | Reference
55 | Articles
64 |
19 |
22 | 
20 | {epoxy}
21 |
27 |
28 |
29 |
30 |
31 |
32 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
Get started
66 | Reference
75 | Articles
84 | {{ 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} 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 | 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 | #' {{ [{{ 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{