rstudio / bslib

Tools for theming Shiny and R Markdown via Bootstrap 3, 4, or 5.
https://rstudio.github.io/bslib/
Other
478 stars 57 forks source link

Add css for pandoc syntax highlighting themes #145

Open hadley opened 3 years ago

hadley commented 3 years ago

If your html contains code, you're likely to want to match the syntax highlighting colours with the them as a whole. So I think it makes sense for bootstraplib to provide some tools to turn pandoc themes into css. Could building from something like this:

library(jsonlite)
library(purrr)
library(dplyr)
library(farver)
library(ggplot2)

# From https://github.com/jgm/skylighting/blob/a1d02a0db6260c73aaf04aae2e6e18b569caacdc/skylighting-core/src/Skylighting/Format/HTML.hs#L117-L147
abbr <- c(
  "Keyword"        = "kw",
  "DataType"       = "dt",
  "DecVal"         = "dv",
  "BaseN"          = "bn",
  "Float"          = "fl",
  "Char"           = "ch",
  "String"         = "st",
  "Comment"        = "co",
  "Other"          = "ot",
  "Alert"          = "al",
  "Function"       = "fu",
  "RegionMarker"   = "re",
  "Error"          = "er",
  "Constant"       = "cn",
  "SpecialChar"    = "sc",
  "VerbatimString" = "vs",
  "SpecialString"  = "ss",
  "Import"         = "im",
  "Documentation"  = "do",
  "Annotation"     = "an",
  "CommentVar"     = "cv",
  "Variable"       = "va",
  "ControlFlow"    = "cf",
  "Operator"       = "op",
  "BuiltIn"        = "bu",
  "Extension"      = "ex",
  "Preprocessor"   = "pp",
  "Attribute"      = "at",
  "Information"    = "in",
  "Warning"        = "wa",
  "Normal"         = ""
)

theme <- jsonlite::read_json("https://raw.githubusercontent.com/rstudio/distill/master/inst/rmarkdown/templates/distill_article/resources/arrow.theme")

as_row <- function(x) {
  x %>%
    modify_if(is.null, ~ NA) %>%
    as_tibble()
}

styles_df <- theme$`text-styles` %>%
  purrr::map_df(as_row, .id = "name") %>%
  rename(color = `text-color`, background = `background-color`) %>%
  mutate(abbr = unname(abbr[name]))

# From https://accessible-colors.com
rel_l <- function(x) {
  scale <- function(x) {
    ifelse(x <= 0.03928, x / 12.92, ((x + 0.055) / 1.055)^2.4)
  }
  rgb <- farver::decode_colour(x) / 255
  0.2126 * scale(rgb[, 1]) + 0.7152 * scale(rgb[, 2]) + 0.0722 * scale(rgb[, 3])
}
contrast_ratio <- function(x, y) {
  x_l <- rel_l(x)
  y_l <- rel_l(y)

  (pmax(x_l, y_l) + 0.05) / (pmin(x_l, y_l) + 0.05)
}

styles_df %>% mutate(contrast = contrast_ratio(color, "#fefefe"))
cpsievert commented 3 years ago

Thanks, we'll have to think about how this should interplay with html_document()'s existing highlight param (it probably makes sense to set highlight = NULL if theme is a bs_theme() and providing our own highlighting?)

https://github.com/rstudio/rmarkdown/blob/2787b5038c1080fe4dbfabcf73f76277d86df8e0/inst/rmd/h/default.html#L65-L102

hadley commented 3 years ago

@cpsievert I don't think setting it to NULL is correct because that would suppress syntax highlighting altogether (rather than just changing the visual appearance)

cpsievert commented 3 years ago

@hadley is there any reason why you've omitted this code to translate the data frame to css (which was in this gist you shared with me previously)?

# https://github.com/jgm/skylighting/blob/a1d02a0db6260c73aaf04aae2e6e18b569caacdc/skylighting-core/src/Skylighting/Format/HTML.hs#L203
to_css <- function(abbr, color, background, bold, italic, underline, ...) {
    attr <- c(
        if (!is.na(color)) paste0("color:", color),
        if (!is.na(background)) paste0("background-color:", background),
        if (bold) "font-weight: bold",
        if (italic) "font-style: italic",
        if (underline) "text-decoration: underline"
    )

    paste0("code span.", abbr, " {", paste0(attr, collapse = "; "), "}")
}

styles_df %>% pmap_chr(to_css) %>% writeLines()
code span.ot {color:#ff4000}
code span.at {}
code span.ss {color:#008080}
code span.an {color:#008000}
code span.st {color:#036a07}
code span.dv {color:#0000cd}
code span.fl {color:#0000cd}
code span.cf {color:#0000ff}
code span.op {color:#687687}
code span.er {color:#ff0000; font-weight: bold}
code span.al {color:#ff0000}
code span.va {}
code span.bu {}
code span.ex {}
code span.pp {color:#ff4000}
code span.in {color:#008000}
code span.vs {color:#008080}
code span.wa {color:#008000; font-weight: bold}
code span.do {color:#008000}
code span.im {}
code span.ch {color:#008080}
code span.co {color:#4c886b}
code span.cv {color:#008000}
code span.cn {color:#585cf6}
code span.sc {color:#008080}
code span.kw {color:#0000ff}
hadley commented 3 years ago

No, I just forgot about it. BTW another possible home for this is the pandoc package that @cderv might eventually build.

cderv commented 3 years ago

I am still not yet enough familiar with bslib, but CSS files derived from Pandoc highlighting theme would indeed fit in the future mentioned package. Or a least a function to build them.

cpsievert commented 3 years ago

Ok great, I'm currently thinking this wouldn't be {bslib}'s job anyway, but rmarkdown (or this pandoc package) would be responsible for translating a relevant bs_theme() into a pandoc highlighting theme via bs_get_variables(). For Bootstrap 4, it could do something like this (we'll have to do something slightly different for Bootstrap 3):


# Translate Bootstrap Sass semantics to pandoc's syntax highlighting
# This translation is inspired by distill's arrow.theme semantics
# https://raw.githubusercontent.com/rstudio/distill/master/inst/rmarkdown/templates/distill_article/resources/arrow.theme
bs_theme_pandoc <- function(theme) {
  if (!is_bs_theme(theme)) return()

  vars <- bs_get_variables(
    theme, c("bg", "fg", "primary", "success", "warning", "danger")
  )
  vars <- setNames(htmltools::parseCssColors(vars), names(vars))
  gray_pal <- scales::colour_ramp(vars[c("bg", "fg")])
  jsonlite::fromJSON(glue::glue(
    .open = "{{",
    .close = "}}",
    '{
      "text-color": null,
      "background-color": null,
      "line-number-color": "{{gray_pal(2/3)}}",
      "line-number-background-color": null,
      "text-styles": {
          "Other": {
              "text-color": "{{vars[["primary"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Attribute": {
              "text-color": "{{vars[["success"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "SpecialString": {
              "text-color": "{{vars[["success"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Annotation": {
              "text-color": "{{gray_pal(0.37)}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Function": {
              "text-color": "{{scales::colour_ramp(vars[["fg"]], vars[["primary"]])(0.2)}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "String": {
              "text-color": "{{vars[["success"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "ControlFlow": {
              "text-color": "{{vars[["primary"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Operator": {
              "text-color": "{{gray_pal(0.37)}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Error": {
              "text-color": "{{vars[["danger"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "BaseN": {
              "text-color": "{{vars[["danger"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Alert": {
              "text-color": "{{vars[["danger"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Variable": {
              "text-color": "{{gray_pal(0.06)}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "BuiltIn": {
              "text-color": null,
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Extension": {
              "text-color": null,
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Preprocessor": {
              "text-color": "{{vars[["danger"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Information": {
              "text-color": "{{gray_pal(0.37)}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "VerbatimString": {
              "text-color": "{{vars[["success"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Warning": {
              "text-color": "{{gray_pal(0.37)}}",
              "background-color": null,
              "bold": false,
              "italic": true,
              "underline": false
          },
          "Documentation": {
              "text-color": "{{gray_pal(0.37)}}",
              "background-color": null,
              "bold": false,
              "italic": true,
              "underline": false
          },
          "Import": {
              "text-color": null,
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Char": {
              "text-color": "{{vars[["success"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "DataType": {
              "text-color": "{{vars[["danger"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Float": {
              "text-color": "{{vars[["danger"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Comment": {
              "text-color": "{{gray_pal(0.37)}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "CommentVar": {
              "text-color": "{{gray_pal(0.37)}}",
              "background-color": null,
              "bold": false,
              "italic": true,
              "underline": false
          },
          "Constant": {
              "text-color": "{{vars[["warning"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "SpecialChar": {
              "text-color": "{{vars[["success"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "DecVal": {
              "text-color": "{{vars[["danger"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          },
          "Keyword": {
              "text-color": "{{vars[["primary"]]}}",
              "background-color": null,
              "bold": false,
              "italic": false,
              "underline": false
          }
        }
      }'
  ))
}
cderv commented 3 years ago

So the idea would be to create a highlighting theme to pass to Pandoc from a selected bs_theme() ? I find the idea interesting.

But I am now not so sure I understood correctly in the first place now... I thought this issue was about turning Pandoc's highlighting theme (like our custom arrow.theme or rstudio.theme) into css to be used with {bslib} - Did I misunderstand from the beginning or do you think this is better the other way around ?

cpsievert commented 3 years ago

Yea, after looking into it more, I don't think it's right for bslib to be including css, for a couple reasons:

hadley commented 2 years ago

I'm going to do this in pkgdown because I need to bundle some syntax highlighting choices: https://github.com/r-lib/pkgdown/pull/1841