bslib icon indicating copy to clipboard operation
bslib copied to clipboard

Add css for pandoc syntax highlighting themes

Open hadley opened this issue 3 years ago • 9 comments

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"))

hadley avatar Oct 29 '20 13:10 hadley

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

cpsievert avatar Oct 29 '20 15:10 cpsievert

@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)

hadley avatar Oct 29 '20 16:10 hadley

@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}

cpsievert avatar Dec 02 '20 23:12 cpsievert

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

hadley avatar Dec 03 '20 12:12 hadley

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.

cderv avatar Dec 03 '20 14:12 cderv

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 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
}
}
}'
))
}

cpsievert avatar Dec 03 '20 15:12 cpsievert

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 ?

cderv avatar Dec 03 '20 17:12 cderv

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

  • I'm fairly sure, when opt-ing into pandoc syntax highlighting, a theme must be specified and pandoc always includes the relevant CSS. Thus, we'd have to overwrite those rules without knowing whether or not you're even using syntax highlighting (making things unnecessarily bloated and convoluted)
  • Pandoc could decide to change the HTML markup structure at some point.

cpsievert avatar Dec 03 '20 19:12 cpsievert

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

hadley avatar Oct 19 '21 16:10 hadley