Building Themes from (S)CSS

nbpresent themes are a simple data structure with a lot of flexibility. They reflect a data-driven view of style, and make some use of references, as for colors. More work needs to be done, especially surrounding reuse!

In this notebook, we look at the themes from revealjs which has provided so many inspirations for nbpresent.


In [407]:
from os.path import join, basename, splitext, abspath
from glob import glob
import re
from pprint import pprint
from collections import defaultdict
from copy import deepcopy
import json
from uuid import uuid4

import colour

import jinja2

import yaml

from IPython.display import Javascript, display, Markdown

Extract SCSS variables

The revealjs themes are written in SCSS on a base template. Great! With one little regular expression we can pull out variable names, and have a pretty good idea of what they mean.


In [383]:
var_re = r"^(\$.*)\s*:\s*(.*);"

Provide alternates

All sizes in nbpresent are specified in rem, a device-aware measure that helps avoid surprises. reveal uses a selection of measures, mostly based on em. I figured these out just by trying it, but there may indeed be a more elegant and robust way to calculate these.


In [384]:
sizes = {
    "38px": 6,
    "36px": 5,
    "30px": 4,
    "1.0em": 3,
    "1.00em": 3,
    "1.3em": 3.5,
    "1.55em": 3.75,
    "1.6em": 4,
    "2.11em": 5,
    "2.5em": 5.25,
    "3.77em": 7
}

fonts = {
    "League Gothic": "Oswald",
    "Palatino Linotype": "EB Garamond"
}

Headings

Headings end up being really important, but are pretty verbose. While it's theoretically possible to use traditional CSS selectors like h1, h2, h3{...}, for the time being we store them all verbosely.


In [385]:
def update_headings(rules, directives):
    for i in range(1, 8):
        rules["h{}".format(i)].update(directives)

Reading themes

While a bit verbose, they can be quite readable.


In [412]:
def pretty(theme):
    display(Markdown("""```yaml\n{}\n```""".format(
        yaml.safe_dump(yaml.safe_load(json.dumps(theme))))))

Importing the variables

make_theme runs through all the variables it found, and tries to determine where they should go in the theme data model.


In [413]:
def make_theme(scss, theme_id=None, theme=None):
    theme = deepcopy(theme) or {}
    theme["id"] = theme_id = theme_id or str(uuid4())

    palette = theme["palette"] = theme.get("palette", {})
    backgrounds = theme["backgrounds"] = theme.get("backgrounds", {})
    rules = theme["rules"] = theme.get("rules", defaultdict(dict))
    base = theme["text-base"] = rules["p"] = rules["li"] = theme.get("text-base", {"font-size": 3})
    
    all_vars = dict([
        (name, val)
        for name, val in
        re.findall(var_re, scss, flags=re.M)
    ])
    
    # handle colors
    for name, val in all_vars.items():
        if "Color" in name and "selection" not in name and "Hover" not in name:
            if val in all_vars:
                val = all_vars[val]

            cid = name[1:] 
            palette[cid] = {
                "id": cid,
                "rgb": [int(256 * c) for c in colour.Color(val).rgb]
            }

            if "background" in name:
                backgrounds[cid] = {
                    "id": cid,
                    "background-color": cid
                }
            elif "heading" in name:
                update_headings(rules, {"color": cid})
            elif "main" in name:
                base["color"] = cid
            elif "link" in name:
                rules["a"]["color"] = cid
            else:
                print(theme_id, name, val)
        elif re.match(r".*Font$", name):
            font = val.split(",")[0].replace("'", "")
            if font in fonts:
                font = fonts[font]
            
            if "heading" in name:
                update_headings(rules, {"font-family": font})
            elif "main" in name:
                base["font-family"] = font
        elif "Size" in name:
            size = sizes[val]
            if "main" in name:
                base["font-size"] = size
            elif "heading" in name:
                h = "h{}".format(*re.findall(r'\d+', name))
                rules[h]["font-size"] = size
    return theme

Whew!

Great, we could parse some themes... where can we get them? A handy approach would be to use git: try uncommenting and running the line below: this will make a directory next to nbpresent.


In [414]:
#!git clone https://github.com/hakimel/reveal.js.git ../../revealjs

Reveal details

Let's capture the version of the reveal from which we extracted these.


In [415]:
reveal = abspath(join("..", "..", "revealjs"))
reveal_version = json.load(open(join(reveal, "package.json")))["version"]
reveal_version


Out[415]:
'3.2.0'

The base theme

css/theme/template/settings.scss gives us the reveal baseline. If we could inherit themes, we'd do something clever with references, but for the time being, we can just use it as a data baseline.


In [416]:
settings = open(join(reveal, "css", "theme", "template", "settings.scss")).read()
base_theme = make_theme(settings)
pretty(base_theme)


backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: ed85d078-af39-42e7-a9f7-d56cee9232ce
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [43, 43, 43]
  headingColor:
    id: headingColor
    rgb: [238, 238, 238]
  linkColor:
    id: linkColor
    rgb: [19, 218, 236]
  mainColor:
    id: mainColor
    rgb: [238, 238, 238]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Oswald, font-size: 7}
  h2: {color: headingColor, font-family: Oswald, font-size: 5}
  h3: {color: headingColor, font-family: Oswald, font-size: 3.75}
  h4: {color: headingColor, font-family: Oswald, font-size: 3}
  h5: {color: headingColor, font-family: Oswald}
  h6: {color: headingColor, font-family: Oswald}
  h7: {color: headingColor, font-family: Oswald}
  li: {color: mainColor, font-family: Lato, font-size: 5}
  p: {color: mainColor, font-family: Lato, font-size: 5}
text-base: {color: mainColor, font-family: Lato, font-size: 5}

Reading the SCSS

Since these aren't too big, let's build up a dictionary with:

  • the name of the them, indexing...
  • ...the SCSS source

In [390]:
scss = dict([
    (
        "{}-by-revealjs-{}".format(splitext(basename(fname))[0],
                                   reveal_version),
        open(fname).read()
    )
    for fname in glob(join(reveal, "css", "theme", "source", "*.scss"))
])
len(scss)


Out[390]:
11

Put it all together

Let's use that dictionary to build some themes!


In [391]:
themes = dict([
    (name, make_theme(s, name, base_theme))
    for name, s in scss.items()])

In [423]:
list(map(pretty, themes.values()));


backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: black-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [34, 34, 34]
  headingColor:
    id: headingColor
    rgb: [256, 256, 256]
  linkColor:
    id: linkColor
    rgb: [66, 175, 250]
  mainColor:
    id: mainColor
    rgb: [256, 256, 256]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Source Sans Pro, font-size: 5.25}
  h2: {color: headingColor, font-family: Source Sans Pro, font-size: 4}
  h3: {color: headingColor, font-family: Source Sans Pro, font-size: 3.5}
  h4: {color: headingColor, font-family: Source Sans Pro, font-size: 3}
  h5: {color: headingColor, font-family: Source Sans Pro}
  h6: {color: headingColor, font-family: Source Sans Pro}
  h7: {color: headingColor, font-family: Source Sans Pro}
  li: {color: mainColor, font-family: Source Sans Pro, font-size: 6}
  p: {color: mainColor, font-family: Source Sans Pro, font-size: 6}
text-base: {color: mainColor, font-family: Source Sans Pro, font-size: 6}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: simple-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [256, 256, 256]
  headingColor:
    id: headingColor
    rgb: [0, 0, 0]
  linkColor:
    id: linkColor
    rgb: [0, 0, 139]
  mainColor:
    id: mainColor
    rgb: [0, 0, 0]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: News Cycle, font-size: 7}
  h2: {color: headingColor, font-family: News Cycle, font-size: 5}
  h3: {color: headingColor, font-family: News Cycle, font-size: 3.75}
  h4: {color: headingColor, font-family: News Cycle, font-size: 3}
  h5: {color: headingColor, font-family: News Cycle}
  h6: {color: headingColor, font-family: News Cycle}
  h7: {color: headingColor, font-family: News Cycle}
  li: {color: mainColor, font-family: Lato, font-size: 5}
  p: {color: mainColor, font-family: Lato, font-size: 5}
text-base: {color: mainColor, font-family: Lato, font-size: 5}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: sky-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [247, 251, 252]
  headingColor:
    id: headingColor
    rgb: [51, 51, 51]
  linkColor:
    id: linkColor
    rgb: [59, 117, 158]
  mainColor:
    id: mainColor
    rgb: [51, 51, 51]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Quicksand, font-size: 7}
  h2: {color: headingColor, font-family: Quicksand, font-size: 5}
  h3: {color: headingColor, font-family: Quicksand, font-size: 3.75}
  h4: {color: headingColor, font-family: Quicksand, font-size: 3}
  h5: {color: headingColor, font-family: Quicksand}
  h6: {color: headingColor, font-family: Quicksand}
  h7: {color: headingColor, font-family: Quicksand}
  li: {color: mainColor, font-family: Open Sans, font-size: 5}
  p: {color: mainColor, font-family: Open Sans, font-size: 5}
text-base: {color: mainColor, font-family: Open Sans, font-size: 5}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: solarized-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [253, 246, 227]
  headingColor:
    id: headingColor
    rgb: [88, 110, 117]
  linkColor:
    id: linkColor
    rgb: [38, 139, 210]
  mainColor:
    id: mainColor
    rgb: [101, 123, 131]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Oswald, font-size: 7}
  h2: {color: headingColor, font-family: Oswald, font-size: 5}
  h3: {color: headingColor, font-family: Oswald, font-size: 3.75}
  h4: {color: headingColor, font-family: Oswald, font-size: 3}
  h5: {color: headingColor, font-family: Oswald}
  h6: {color: headingColor, font-family: Oswald}
  h7: {color: headingColor, font-family: Oswald}
  li: {color: mainColor, font-family: Lato, font-size: 5}
  p: {color: mainColor, font-family: Lato, font-size: 5}
text-base: {color: mainColor, font-family: Lato, font-size: 5}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: beige-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [247, 243, 222]
  headingColor:
    id: headingColor
    rgb: [51, 51, 51]
  linkColor:
    id: linkColor
    rgb: [139, 116, 61]
  mainColor:
    id: mainColor
    rgb: [51, 51, 51]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Oswald, font-size: 7}
  h2: {color: headingColor, font-family: Oswald, font-size: 5}
  h3: {color: headingColor, font-family: Oswald, font-size: 3.75}
  h4: {color: headingColor, font-family: Oswald, font-size: 3}
  h5: {color: headingColor, font-family: Oswald}
  h6: {color: headingColor, font-family: Oswald}
  h7: {color: headingColor, font-family: Oswald}
  li: {color: mainColor, font-family: Lato, font-size: 5}
  p: {color: mainColor, font-family: Lato, font-size: 5}
text-base: {color: mainColor, font-family: Lato, font-size: 5}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: blood-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [34, 34, 34]
  headingColor:
    id: headingColor
    rgb: [238, 238, 238]
  linkColor:
    id: linkColor
    rgb: [170, 34, 51]
  mainColor:
    id: mainColor
    rgb: [238, 238, 238]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Ubuntu, font-size: 7}
  h2: {color: headingColor, font-family: Ubuntu, font-size: 5}
  h3: {color: headingColor, font-family: Ubuntu, font-size: 3.75}
  h4: {color: headingColor, font-family: Ubuntu, font-size: 3}
  h5: {color: headingColor, font-family: Ubuntu}
  h6: {color: headingColor, font-family: Ubuntu}
  h7: {color: headingColor, font-family: Ubuntu}
  li: {color: mainColor, font-family: Ubuntu, font-size: 5}
  p: {color: mainColor, font-family: Ubuntu, font-size: 5}
text-base: {color: mainColor, font-family: Ubuntu, font-size: 5}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: night-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [17, 17, 17]
  headingColor:
    id: headingColor
    rgb: [238, 238, 238]
  linkColor:
    id: linkColor
    rgb: [231, 173, 82]
  mainColor:
    id: mainColor
    rgb: [238, 238, 238]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Montserrat, font-size: 7}
  h2: {color: headingColor, font-family: Montserrat, font-size: 5}
  h3: {color: headingColor, font-family: Montserrat, font-size: 3.75}
  h4: {color: headingColor, font-family: Montserrat, font-size: 3}
  h5: {color: headingColor, font-family: Montserrat}
  h6: {color: headingColor, font-family: Montserrat}
  h7: {color: headingColor, font-family: Montserrat}
  li: {color: mainColor, font-family: Open Sans, font-size: 4}
  p: {color: mainColor, font-family: Open Sans, font-size: 4}
text-base: {color: mainColor, font-family: Open Sans, font-size: 4}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: serif-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [240, 241, 235]
  headingColor:
    id: headingColor
    rgb: [56, 61, 61]
  linkColor:
    id: linkColor
    rgb: [81, 72, 61]
  mainColor:
    id: mainColor
    rgb: [0, 0, 0]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: EB Garamond, font-size: 7}
  h2: {color: headingColor, font-family: EB Garamond, font-size: 5}
  h3: {color: headingColor, font-family: EB Garamond, font-size: 3.75}
  h4: {color: headingColor, font-family: EB Garamond, font-size: 3}
  h5: {color: headingColor, font-family: EB Garamond}
  h6: {color: headingColor, font-family: EB Garamond}
  h7: {color: headingColor, font-family: EB Garamond}
  li: {color: mainColor, font-family: EB Garamond, font-size: 5}
  p: {color: mainColor, font-family: EB Garamond, font-size: 5}
text-base: {color: mainColor, font-family: EB Garamond, font-size: 5}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: white-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [256, 256, 256]
  headingColor:
    id: headingColor
    rgb: [34, 34, 34]
  linkColor:
    id: linkColor
    rgb: [42, 118, 221]
  mainColor:
    id: mainColor
    rgb: [34, 34, 34]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Source Sans Pro, font-size: 5.25}
  h2: {color: headingColor, font-family: Source Sans Pro, font-size: 4}
  h3: {color: headingColor, font-family: Source Sans Pro, font-size: 3.5}
  h4: {color: headingColor, font-family: Source Sans Pro, font-size: 3}
  h5: {color: headingColor, font-family: Source Sans Pro}
  h6: {color: headingColor, font-family: Source Sans Pro}
  h7: {color: headingColor, font-family: Source Sans Pro}
  li: {color: mainColor, font-family: Source Sans Pro, font-size: 6}
  p: {color: mainColor, font-family: Source Sans Pro, font-size: 6}
text-base: {color: mainColor, font-family: Source Sans Pro, font-size: 6}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: moon-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [0, 43, 54]
  headingColor:
    id: headingColor
    rgb: [238, 232, 213]
  linkColor:
    id: linkColor
    rgb: [38, 139, 210]
  mainColor:
    id: mainColor
    rgb: [147, 161, 161]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Oswald, font-size: 7}
  h2: {color: headingColor, font-family: Oswald, font-size: 5}
  h3: {color: headingColor, font-family: Oswald, font-size: 3.75}
  h4: {color: headingColor, font-family: Oswald, font-size: 3}
  h5: {color: headingColor, font-family: Oswald}
  h6: {color: headingColor, font-family: Oswald}
  h7: {color: headingColor, font-family: Oswald}
  li: {color: mainColor, font-family: Lato, font-size: 5}
  p: {color: mainColor, font-family: Lato, font-size: 5}
text-base: {color: mainColor, font-family: Lato, font-size: 5}
backgrounds:
  backgroundColor: {background-color: backgroundColor, id: backgroundColor}
id: league-by-revealjs-3.2.0
palette:
  backgroundColor:
    id: backgroundColor
    rgb: [43, 43, 43]
  headingColor:
    id: headingColor
    rgb: [238, 238, 238]
  linkColor:
    id: linkColor
    rgb: [19, 218, 236]
  mainColor:
    id: mainColor
    rgb: [238, 238, 238]
rules:
  a: {color: linkColor}
  h1: {color: headingColor, font-family: Oswald, font-size: 7}
  h2: {color: headingColor, font-family: Oswald, font-size: 5}
  h3: {color: headingColor, font-family: Oswald, font-size: 3.75}
  h4: {color: headingColor, font-family: Oswald, font-size: 3}
  h5: {color: headingColor, font-family: Oswald}
  h6: {color: headingColor, font-family: Oswald}
  h7: {color: headingColor, font-family: Oswald}
  li: {color: mainColor, font-family: Lato, font-size: 5}
  p: {color: mainColor, font-family: Lato, font-size: 5}
text-base: {color: mainColor, font-family: Lato, font-size: 5}

Load the data

This will load up all of the themes right into this presentation


In [424]:
Javascript(jinja2.Template("""
require(["nbextensions/nbpresent/js/nbpresent.min"], function(nbpresent){
    nbpresent.initialized().then(function(nbpresent){
        nbpresent.mode.tree.set(["themes", "theme"], {{ themes }});
    });
});
""").render(themes=json.dumps(themes, indent=2)))


Out[424]:

Test it out!

the two cells below have already been added to a slide... try opening the per-slide theme view to see it looks with different themes!

h1 h1 h1

h2 h2 h2

h3 h3 h3

h4 h4 h4

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae

Export the data

Oh, yeah: I did this to actually make this available in nbpresent!


In [433]:
es6_tmpl = jinja2.Template("""/*
THIS IS GENERATED CODE BY /notebooks/Importing revealjs themes.ipynb
DO NOT EDIT DIRECTLY

Source content:
https://github.com/hakimel/reveal.js/tree/master/css/theme

All original themes copyright their respective creators:

    beige       Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
    black       Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
    blood       Author: Walther http://github.com/Walther
    league      Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
    moon        Author: Achim Staebler
    night       Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
    serif       Copyright (C) 2012-2013 Owen Versteeg, http://owenversteeg.com - it is MIT licensed.
    simple      Copyright (C) 2012 Owen Versteeg, https://github.com/StereotypicalApps. It is MIT licensed.
    sky         Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se
    solarized   Author: Achim Staebler
    white       By Hakim El Hattab, http://hakim.se

*/

export const REVEAL_THEMES = {{ themes }};
""")
with open(join("..", "src", "es6", "theme", "theme", "reveal.es6"), "w") as es6:
    es6.write(es6_tmpl.render(themes=json.dumps(themes, indent=2)))