3 min read

Selfcontained Freedom on iPad (without Pandoc)

Maybe you are like me. Sometimes I just need a nudge. Last week I received a nudge in the form of an email from Mike Stein saying (with the magic words - selfcontained and webR)

Any future possibility for the htmlwidgets package to be able to create a self-contained .html file without Pandoc so that users create self-contained htmlwidgets via webR on their mobile devices? Self-contained htmlwidget .html files are a nice format for me to send data (e.g. a reactable table)…

I love the selfcontained argument, and actually the entire https://buildingwidgets.com was written by converting Rmd to selfcontained html (with the help of Pandoc) and pasting the html contents into the Squarespace editor. See the modified bit of code.

So off to Github I went inspecting htmlwidgets code to see if my initial impression of feasibility was accurate. Along the way I also found the brilliant Rust tool monolith also mentioned in today’s drop from Bob Rudis Hoarding Can Be A Good Thing. However, given the need to run in a controlled environment or the iPad, monolith would not be a viable option. I ultimately got a working solution with a tiny bit of a code and a couple regexes so ugly that I am not sure why I am sharing in public.

It works for me locally and in the webR repl / stackblitz repl, and Mike says it works for him in the repl on his iPad, so hopefully it will work for you as well. There is lots of room for improvement with type checking and error handling, so please let me know if you convert it into something more legitimate and production-worthy. Also, this will likely work with tagList if you would like non-widget selfcontained content. Thanks Mike for the nudge.

save_selfcontained <- function(widget)  {
  
  temp_dir <- tempfile()
  dir.create(temp_dir)
  temp_file <- file.path(temp_dir,"widget.html")
  
  htmlwidgets::saveWidget(widget, file = temp_file, selfcontained = FALSE)

  # read not self-contained html
  html_text <- readLines(temp_file)
  
  # convert <script src=*> to <script>js file contents</script>
  js_lines <- which(grepl(
    x = html_text,
    pattern = '(src=.*js)'
  ))
  
  # convert link[rel=stylesheet] to <style>css file contents</style>
  css_lines <- which(grepl(
    x = html_text,
    pattern = '(href=.*css)'
  ))
  
  # perform self-contained conversion/replacement of JS
  if(length(js_lines) > 0) {
    html_text[js_lines] <- lapply(js_lines, function(js_line) {
      js_file <- sub(x=html_text[js_line], pattern='.*src=[":\'](.*\\.js).*', replacement="\\1")
      js_content <- paste0(
        "<script>",
        paste0(readLines(file.path(temp_dir,js_file)), collapse="\n"),
        "</script>",
        collapse="\n"
      )
    })
  }
  
  # perform self-contained conversion/replacement of JS
  if(length(css_lines) > 0) {
    html_text[css_lines] <- lapply(css_lines, function(css_line) {
      css_file <- sub(x=html_text[css_line], pattern='.*href=[":\'](.*\\.css).*', replacement="\\1")
      css_content <- paste0(
        "<style>",
        paste0(readLines(file.path(temp_dir,css_file)), collapse="\n"),
        "</style>",
        collapse="\n"
      )
    })
  }
  
  # save self-contained html
  write(paste0(html_text,collapse="\n"), file=file.path(temp_dir,"index.html"))
  
  return(file.path(temp_dir,"index.html"))
}