From 5f05951e9ba1ff51f4f6542bf1cddff7c9efa7ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= <hugo@lysator.liu.se>
Date: Sat, 1 Jul 2023 09:41:03 +0200
Subject: [PATCH] Rework how highlighting works.

See static-src/README.rst for details about how it works.
---
 Makefile                               |   8 +-
 muppet/__main__.py                     |   3 +-
 muppet/format.py                       |   9 +-
 muppet/output.py                       |  12 +-
 muppet/syntax_highlight/__init__.py    |  39 +++++++
 muppet/syntax_highlight/andre_simon.py |  36 ++++++
 muppet/syntax_highlight/plain.py       |  14 +++
 muppet/syntax_highlight/pygments.py    |  24 ++++
 static-src/.gitignore                  |   3 +
 static-src/Makefile                    |  58 +++++++++
 static-src/README.rst                  | 109 +++++++++++++++++
 static-src/_breadcrumb.scss            | 155 ++-----------------------
 static-src/_breadcrumb.scss.map        |   7 --
 static-src/_highlight.scss             |  49 --------
 static-src/build_css.py                | 146 +++++++++++++++++++++++
 static-src/colorscheme/default.yaml    |  23 ++++
 static-src/highlight/andre_simon.yaml  |  26 +++++
 static-src/highlight/muppet.yaml       |  25 ++++
 static-src/highlight/pygments.yaml     |  61 ++++++++++
 static-src/style.scss                  |  20 +++-
 static/.gitignore                      |   1 -
 templates/code_page.html               |   4 +-
 22 files changed, 614 insertions(+), 218 deletions(-)
 create mode 100644 muppet/syntax_highlight/__init__.py
 create mode 100644 muppet/syntax_highlight/andre_simon.py
 create mode 100644 muppet/syntax_highlight/plain.py
 create mode 100644 muppet/syntax_highlight/pygments.py
 create mode 100644 static-src/.gitignore
 create mode 100644 static-src/Makefile
 create mode 100644 static-src/README.rst
 delete mode 100644 static-src/_breadcrumb.scss.map
 delete mode 100644 static-src/_highlight.scss
 create mode 100755 static-src/build_css.py
 create mode 100644 static-src/colorscheme/default.yaml
 create mode 100644 static-src/highlight/andre_simon.yaml
 create mode 100644 static-src/highlight/muppet.yaml
 create mode 100644 static-src/highlight/pygments.yaml
 delete mode 100644 static/.gitignore

diff --git a/Makefile b/Makefile
index 3000cef..b8cf57a 100644
--- a/Makefile
+++ b/Makefile
@@ -7,9 +7,9 @@ DOC_OUTPUT = doc.rendered
 OUTPUT_FLAGS = --path-base /code/muppet-strings/output \
 			   --env ~/puppet/generated-environments/production/modules/
 
-SCSS = sass
+SCSS = scss
 
-output: static/style.css
+output: static-src/style.css
 	python -m muppet $(OUTPUT_FLAGS)
 
 check_style:
@@ -37,5 +37,5 @@ documentation: $(DOC_OUTPUT)/index.html
 clean:
 	-rm -r output
 
-static/%.css: static-src/style.scss $(wildcard static-src/_*.scss)
-	$(SCSS) --sourcemap=auto -Istatic-src $< $@
+static-src/style.css:
+	$(MAKE) -C $(dir $@) $(notdir $@)
diff --git a/muppet/__main__.py b/muppet/__main__.py
index 943f9ae..b064cf1 100644
--- a/muppet/__main__.py
+++ b/muppet/__main__.py
@@ -47,7 +47,8 @@ def __main() -> None:
         # print(module)
         setup_module('output', module, path_base=args.path_base)
 
-    os.system("cp -r static output")
+    os.system('make -C static-src --silent install-full PREFIX=$PWD/output')
+    os.system("cp -r static/* output/static/")
 
 
 if __name__ == '__main__':
diff --git a/muppet/format.py b/muppet/format.py
index 97c38d1..870dfd3 100644
--- a/muppet/format.py
+++ b/muppet/format.py
@@ -1117,7 +1117,7 @@ def format_class(d_type: dict[str, Any]) -> str:
     # print(name, file=sys.stderr)
     out += print_docstring(name, d_type['docstring'])
 
-    out += '<pre><code class="puppet">'
+    out += '<pre class="highlight-muppet"><code class="puppet">'
     out += render(renderer, data)
     out += '</code></pre>'
     return out
@@ -1136,7 +1136,7 @@ def format_type_alias(d_type: dict[str, Any]) -> str:
     # print(name, file=sys.stderr)
     out += print_docstring(name, d_type['docstring'])
     out += '\n'
-    out += '<pre><code class="puppet">'
+    out += '<pre class="highlight-muppet"><code class="puppet">'
     t = parse_puppet(d_type['alias_of'])
     data = parse(t, 0, ['root'])
     out += render(renderer, data)
@@ -1152,7 +1152,7 @@ def format_defined_type(d_type: dict[str, Any]) -> str:
     # print(name, file=sys.stderr)
     out += print_docstring(name, d_type['docstring'])
 
-    out += '<pre><code class="puppet">'
+    out += '<pre class="highlight-muppet"><code class="puppet">'
     t = parse_puppet(d_type['source'])
     out += render(renderer, parse(t, 0, ['root']))
     out += '</code></pre>\n'
@@ -1201,9 +1201,10 @@ def format_puppet_function(function: dict[str, Any]) -> str:
         signature['signature']
         signature['docstring']
     if t in ['ruby3x', 'ruby4x']:
+        # TODO manual highlight here
         out += f'<pre><code class="ruby">{function["source"]}</code></pre>\n'
     elif t == 'puppet':
-        out += '<pre><code class="puppet">'
+        out += '<pre class="highlight-muppet"><code class="puppet">'
         try:
             t = parse_puppet(function['source'])
             out += str(parse(t, 0, ['root']))
diff --git a/muppet/output.py b/muppet/output.py
index 0e83c58..7e2266b 100644
--- a/muppet/output.py
+++ b/muppet/output.py
@@ -30,6 +30,7 @@ from collections.abc import (
 from .util import group_by
 from .puppet.strings import isprivate
 from .breadcrumbs import breadcrumbs
+from .syntax_highlight import highlight
 
 
 # TODO replace 'output' with base, or put this somewhere else
@@ -291,8 +292,15 @@ def setup_module(base: str, module: ModuleEntry, *, path_base: str) -> None:
 
         # TODO option to add .txt extension (for web serverse which
         # treat .pp as application/binary)
-        with open(os.path.join(dir, 'source.pp.txt'), 'w') as f:
-            f.write(puppet_class['source'])
+        with open(os.path.join(dir, 'source.pp.txt'), 'wb') as f:
+            with open(module.file(puppet_class['file']), 'rb') as g:
+                f.write(g.read())
+
+        with open(os.path.join(dir, 'source.pp.html'), 'w') as f:
+            template = jinja.get_template('code_page.html')
+            with open(module.file(puppet_class['file']), 'r') as g:
+                f.write(template.render(content=highlight(g.read(), 'puppet'),
+                                        path_base=path_base))
 
         with open(os.path.join(dir, 'source.json'), 'w') as f:
             json.dump(puppet_class, f, indent=2)
diff --git a/muppet/syntax_highlight/__init__.py b/muppet/syntax_highlight/__init__.py
new file mode 100644
index 0000000..bcadf81
--- /dev/null
+++ b/muppet/syntax_highlight/__init__.py
@@ -0,0 +1,39 @@
+"""
+Syntax highlight with the best available backend.
+
+Syntax highlight the given code by the best, available, syntax
+highlighter, returning the result as HTML. The default highlighter
+simply wraps the output in
+``<pre><code class={language}>{code}</code></pre>``
+(with escaping). This means that it can still be handled by JavaScript if so desired.
+"""
+
+from . import pygments
+from . import andre_simon
+from . import plain
+
+from typing import cast
+
+
+for module in [pygments, andre_simon, plain]:
+    if module.available:
+        backend = module
+        break
+else:
+    # This should never happen, since ``plain`` should always be
+    # available.
+    raise ValueError("No applicable highlight module")
+
+
+def highlight(source: str, language: str) -> str:
+    """
+    Highlight the given source as language, retuning HTML.
+
+    :param source:
+        Source code to highlight.
+    :param language:
+        Language of the source code.
+    :returns:
+        An HTML string.
+    """
+    return cast(str, backend.highlight(source, language))
diff --git a/muppet/syntax_highlight/andre_simon.py b/muppet/syntax_highlight/andre_simon.py
new file mode 100644
index 0000000..7bbcd43
--- /dev/null
+++ b/muppet/syntax_highlight/andre_simon.py
@@ -0,0 +1,36 @@
+"""
+Syntax highlighting through the highlight(1) command.
+
+The name "Andre Simon" is choosen based of the author, and that
+"highlight" by itself it really non-descriptive.
+"""
+
+import subprocess
+
+try:
+    subprocess.run(['highlight', '--version'], stdout=subprocess.DEVNULL)
+    available = True
+except FileNotFoundError:
+    available = False
+
+
+def highlight(code: str, language: str) -> str:
+    """Highlight code through the ``highlight`` command."""
+    # TODO line- vs pygments line_
+    cmd = subprocess.run(['highlight',
+                          '--out-format', 'html',
+                          '--fragment',
+                          '--line-numbers',
+                          '--anchors',
+                          '--anchors-prefix=line',
+                          '--class-name=NONE',
+                          '--syntax', language,
+                          '--enclose-pre'],
+                         capture_output=True,
+                         text=True,
+                         input=code,
+                         check=True)
+    return f"""
+    <!-- Generated through highlight(1), as language {language} -->
+    <div class="highlight-andre-simon">{cmd.stdout}</div>
+    """
diff --git a/muppet/syntax_highlight/plain.py b/muppet/syntax_highlight/plain.py
new file mode 100644
index 0000000..0d1234f
--- /dev/null
+++ b/muppet/syntax_highlight/plain.py
@@ -0,0 +1,14 @@
+"""Non-highlighter useful as a backup."""
+
+import html
+
+available = True
+
+
+def highlight(code: str, language: str) -> str:
+    """Return the code "highlighted" by wrapping it in <pre> tags."""
+    out = f'<pre><code class="{language}">{html.escape(code)}</code></pre>'
+    return f"""
+    <!-- "Genererated" as plain output -->
+    <div class"highlight-plain">{out}</div>
+    """
diff --git a/muppet/syntax_highlight/pygments.py b/muppet/syntax_highlight/pygments.py
new file mode 100644
index 0000000..05e8562
--- /dev/null
+++ b/muppet/syntax_highlight/pygments.py
@@ -0,0 +1,24 @@
+"""Syntax highlighting through pygments."""
+
+try:
+    from pygments.formatters import HtmlFormatter
+    from pygments.lexers import get_lexer_by_name
+    import pygments
+    available = True
+except ModuleNotFoundError:
+    available = False
+
+
+def highlight(code: str, language: str) -> str:
+    """Highlight code through pygments."""
+    out = pygments.highlight(code, get_lexer_by_name(language),
+                             HtmlFormatter(cssclass='highlight-pygments',
+                                           lineanchors='line',
+                                           linenos='table',
+                                           # linenos='inline'
+                                           anchorlinenos=True,
+                                           ))
+    return f"""
+    <!-- Generated through pygments, as {language} -->
+    {out}
+    """
diff --git a/static-src/.gitignore b/static-src/.gitignore
new file mode 100644
index 0000000..6de9d50
--- /dev/null
+++ b/static-src/.gitignore
@@ -0,0 +1,3 @@
+out/
+style.css
+*.map
diff --git a/static-src/Makefile b/static-src/Makefile
new file mode 100644
index 0000000..e01aded
--- /dev/null
+++ b/static-src/Makefile
@@ -0,0 +1,58 @@
+.PHONY: all clean install install-full install-bare
+
+SCSS = scss
+OUT_DIR = out
+TARGET = style.css
+
+DESTDIR =
+PREFIX = /usr/local/share/muppet/
+# Path relative install root where the static files should end up
+LOCAL_PATH = static
+
+# Highlighting files
+highlights = $(wildcard highlight/*.yaml)
+# Colorscheme files
+colorschemes = $(wildcard colorscheme/*.yaml)
+# Generated highlighting and colorscheme files
+outputs = $(patsubst colorscheme/%.yaml,$(OUT_DIR)/_colorscheme_%.scss,$(colorschemes)) \
+		  $(patsubst highlight/%.yaml,$(OUT_DIR)/_highlight_%.scss,$(highlights))
+
+# Included scss files
+SCSS_FILES = $(wildcard _*.scss)
+
+all: style.css
+
+clean:
+	-rm $(outputs)
+	-rmdir $(OUT_DIR)
+	-rm $(TARGET)
+	-rm $(TARGET).map
+
+$(TARGET): style.scss $(SCSS_FILES) $(outputs)
+	$(SCSS) --sourcemap=auto -I. -Iout $< $@
+
+$(OUT_DIR)/_highlight_%.scss: highlight/%.yaml $(OUT_DIR)
+	./build_css.py highlight $< > $@
+
+$(OUT_DIR)/_colorscheme_%.scss: colorscheme/%.yaml $(OUT_DIR)
+	./build_css.py colorscheme $< > $@
+
+$(OUT_DIR):
+	mkdir -p $@
+
+
+dest=$(abspath $(DESTDIR)/$(PREFIX)/$(LOCAL_PATH))
+
+# Install generated files, along with most source files and the map
+# file, to aid in debugging
+install-full: $(TARGET)
+	install -d "$(dest)"
+	install -d "$(dest)/$(OUT_DIR)"
+	install -m644 $(outputs) "$(dest)/$(OUT_DIR)"
+	install -m644 "$(TARGET)" "$(TARGET).map" style.scss $(SCSS_FILES) "$(dest)"
+
+# Only install the generated files
+install-bare: $(TARGET)
+	install -D -m644 -t "$(dest)" $(TARGET)
+
+install: install-full
diff --git a/static-src/README.rst b/static-src/README.rst
new file mode 100644
index 0000000..3d8b52f
--- /dev/null
+++ b/static-src/README.rst
@@ -0,0 +1,109 @@
+.. _highlight-readme:
+
+.. Introduction
+
+Syntax highlighting in the HTML output is handled in a way to allow
+easy changing of highlighting engine, and of colorschemes.
+All syntax objects are grouped into one of the Abstract highlight
+groups (see :ref:`abstract-highlight-groups`). Highlighting files maps
+these groups onto distinct CSS classes depending on the highlighting
+engine, and colorscheme files maps these into CSS styles.
+
+
+
+Colorschemes
+============
+
+Colorscheme files should be placed in ``colorscheme/*.yaml``, and
+should contain a mapping for all (wanted) abstract highlight groups,
+onto mappings containing (any of) the following keys:
+
+- ``color``
+- ``background``
+- ``font-style``
+- ``font-weight``
+
+These will be mapped directly into the CSS.
+
+The special key *raw-append* also exists, which, if present, should
+contain raw CSS code which will be appended to this colorscheme.
+This is mainly useful to target highlighting specific classes not
+exposed by the abstract highlight groups.
+
+.. TODO useful raw selectors
+    - ``:root``
+    - ``.highlight-pygments``
+    - ``.highlight-andre-simon``
+    - ``.highlight-muppet``
+
+Sample CSS
+----------
+
+The generated CSS code will have the following form detailed in
+:ref:`sample-css`. Note that the selector (``.comment``) will depend
+on the highlight file, while the variable names (``--hl-comment-*``)
+depend on the abstract highlight group.
+
+.. code-block:: css
+   :name: sample-css
+   :caption: fragment of generated CSS for a colorscheme.
+
+    .comment {
+        color:            var(--hl-comment-color);
+        background-color: var(--hl-comment-background);
+        font-style:       var(--hl-comment-font-style);
+        font-weight:      var(--hl-comment-font-weight);
+    }
+
+
+Highlight files
+===============
+
+Highlight files map ref:`abstract-highlight_group` to concrete CSS
+classes. Each Yaml file (located in ``highlight/*.yaml``) should
+contain a mapping from abstract highlight groups, to lists of css
+classes.
+
+
+.. _abstract-highlight-group:
+
+Abstract highlight groups
+=========================
+
+The available Abstract highlight groups are as follows:
+
+``comment``
+	Comments
+
+``error``
+	TODO
+
+``escape``
+	Escape sequences embedded in strings
+
+``interpolate``
+	Variables interpolated in strings
+
+``keyword``
+	Language reserved words (like ``class``, ...)
+
+``line``
+	Line numbers (not part of the actual code)
+
+``number``
+	Numeric literals
+
+``operator``
+	operators (``+``, ``in``, ...)
+
+``regex``
+	Regex literals
+
+``special``
+	TODO
+
+``string``
+	String literals
+
+``variable``
+	Variables
diff --git a/static-src/_breadcrumb.scss b/static-src/_breadcrumb.scss
index 63534d6..6918af0 100644
--- a/static-src/_breadcrumb.scss
+++ b/static-src/_breadcrumb.scss
@@ -1,148 +1,11 @@
-@charset "UTF-8";
-/* -------------------------------------------------- */
-.parse-error {
-  background: red;
-  color: yellow; }
-
-/* -------------------------------------------------- */
-h2 {
-  position: sticky;
-  top: 0;
-  background: white;
-  display: block;
-  z-index: 100; }
-
-/* -------------------------------------------------- */
-.documentation {
-  display: none; }
-
-/*
-.var {
-	position: relative;
-}
-
-.var .documentation {
-	display: none;
-	position: absolute;
-	top: 2ch;
-	left: 0;
-	border: 1px solid black;
-	background: lightblue;
-	z-index: 10;
-}
-
-.var:hover .documentation {
-	display: block;
-}
-*/
-.noscript {
-  display: none; }
-
-code.json {
-  font-size: 80%; }
-
-:target {
-  background-color: yellow; }
-
-.overview-list p {
-  display: inline; }
-
-.example {
-  background: lightgray;
-  padding: 1em;
-  border-radius: 1ex; }
-
-.comment {
-  border-left: 1ex;
-  border-left-style: dotted;
-  display: inline-block;
-  padding-left: 1em;
-  font-family: sans;
-  font-size: 80%; }
-  .comment p:first-child {
-    margin-top: 0; }
-  .comment p:last-child {
-    margin-bottom: 0; }
-
-/* -------------------------------------------------- */
 .breadcrumb {
-  padding: 0; }
-  .breadcrumb li {
-    display: inline-block;
-    padding: 0; }
-  .breadcrumb li:not(:first-child)::before {
-    content: "»";
-    padding: 1ex; }
-
-/*
-.case { color: ; }
-.splat { color: ; }
-.array { color: ; }
-.parse-error { color: ; }
-.parameter { color: ; }
-.string { color: ; }
-.regex { color: ; }
-.invoke { color: ; }
-.default { color: ; }
-.call { color: ; }
-.qr { color: ; }
-.lambda { color: ; }
-.number { color: ; }
-.regex-body { color: ; }
-.call-method { color: ; }
-*/
-.literal {
-  color: green; }
-
-.keyword {
-  color: orange; }
-
-.type {
-  color: darkgreen; }
-
-.qn {
-  color: darkgreen; }
-
-.var {
-  color: blue; }
-
-.str-var {
-  color: blue; }
-
-.name {
-  color: red; }
-
-.string {
-  color: olive; }
-
-.comment {
-  color: grey; }
-
-/* Style for tabgroups */
-.tabs {
-  display: grid;
-  grid-template-columns: 1fr;
-  grid-template-rows: auto 1fr;
-  border: 1px solid red; }
-  .tabs menu {
-    display: flex;
-    flex-direction: row;
-    padding: 0;
-    margin: 0; }
-  .tabs menu li {
-    display: block; }
-
-/*
-.tabs menu li {
-	display: block;
-	background-color: lightgray;
-	border: 1px solid gray;
+	padding: 0;
+	li {
+		display: inline-block;
+		padding: 0; 
+		&:not(:first-child)::before {
+			content: "»";
+			padding: 1ex; 
+		}
+	}
 }
-*/
-.tab {
-  display: none;
-  border: 1px solid green; }
-  .tab.selected {
-    display: block !important; }
-
-/*# sourceMappingURL=_breadcrumb.scss.map */
diff --git a/static-src/_breadcrumb.scss.map b/static-src/_breadcrumb.scss.map
deleted file mode 100644
index 5770a45..0000000
--- a/static-src/_breadcrumb.scss.map
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-"version": 3,
-"mappings": ";AACA,wDAAwD;AAExD,YAAa;EACZ,UAAU,EAAE,GAAG;EACf,KAAK,EAAE,MAAM;;AAGd,wDAAwD;AAExD,EAAG;EACF,QAAQ,EAAE,MAAM;EAChB,GAAG,EAAE,CAAC;EACN,UAAU,EAAE,KAAK;EACjB,OAAO,EAAE,KAAK;EACd,OAAO,EAAE,GAAG;;AAGb,wDAAwD;AAExD,cAAe;EACd,OAAO,EAAE,IAAI;;AAGd;;;;;;;;;;;;;;;;;;EAkBE;AAEF,SAAU;EACT,OAAO,EAAE,IAAI;;AAGd,SAAU;EACT,SAAS,EAAE,GAAG;;AAGf,OAAQ;EACP,gBAAgB,EAAE,MAAM;;AAGzB,gBAAiB;EAChB,OAAO,EAAE,MAAM;;AAGhB,QAAS;EACR,UAAU,EAAE,SAAS;EACrB,OAAO,EAAE,GAAG;EACZ,aAAa,EAAE,GAAG;;AAGnB,QAAS;EACR,WAAW,EAAE,GAAG;EAChB,iBAAiB,EAAE,MAAM;EACzB,OAAO,EAAE,YAAY;EACrB,YAAY,EAAE,GAAG;EACjB,WAAW,EAAE,IAAI;EACjB,SAAS,EAAE,GAAG;EAEd,sBAAc;IACb,UAAU,EAAE,CAAC;EAGd,qBAAa;IACZ,aAAa,EAAE,CAAC;;AAIlB,wDAAwD;ACnFxD,WAAY;EACX,OAAO,EAAE,CAAC;EAEV,cAAG;IACF,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,CAAC;EAGX,wCAA6B;IAC5B,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,GAAG;;ACVd;;;;;;;;;;;;;;;;EAgBE;AAEF,QAAS;EAAE,KAAK,EAAE,KAAK;;AAKvB,QAAS;EAAE,KAAK,EAAE,MAAM;;AAWxB,KAAM;EAAE,KAAK,EAAE,SAAS;;AACxB,GAAI;EAAE,KAAK,EAAE,SAAS;;AAEtB,IAAK;EAAE,KAAK,EAAE,IAAI;;AAClB,QAAS;EAAE,KAAK,EAAE,IAAI;;AAEtB,KAAM;EAAE,KAAK,EAAE,GAAG;;AAElB,OAAQ;EACP,KAAK,EAAE,KAAK;;AAGb,QAAS;EACR,KAAK,EAAE,IAAI;;AC/CZ,yBAAyB;AACzB,KAAM;EACL,OAAO,EAAE,IAAI;EACb,qBAAqB,EAAE,GAAG;EAC1B,kBAAkB,EAAE,QAAQ;EAC5B,MAAM,EAAE,aAAa;EAErB,UAAK;IACJ,OAAO,EAAE,IAAI;IACb,cAAc,EAAE,GAAG;IACnB,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;EAGV,aAAQ;IACP,OAAO,EAAE,KAAK;;AAKhB;;;;;;EAME;AAEF,IAAK;EACJ,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,eAAe;EAEvB,aAAW;IACV,OAAO,EAAE,gBAAgB;;ACjC3B,EAAG;EAAE,KAAK,EAAE,OAAO;;AACnB,EAAG;EAAE,KAAK,EAAE,OAAO;;AACnB,EAAG;EAAE,KAAK,EAAE,OAAO;;AACnB,EAAG;EAAE,KAAK,EAAE,OAAO;;AACnB,EAAG;EAAE,KAAK,EAAE,OAAO;;AACnB,EAAG;EAAE,KAAK,EAAE,OAAO",
-"sources": ["style.scss","_breadcrumb.scss","_highlight.scss","_tabset.scss","_color-headers.scss"],
-"names": [],
-"file": "_breadcrumb.scss"
-}
diff --git a/static-src/_highlight.scss b/static-src/_highlight.scss
deleted file mode 100644
index b3aa5d0..0000000
--- a/static-src/_highlight.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
-.case { color: ; }
-.splat { color: ; }
-.array { color: ; }
-.parse-error { color: ; }
-.parameter { color: ; }
-.string { color: ; }
-.regex { color: ; }
-.invoke { color: ; }
-.default { color: ; }
-.call { color: ; }
-.qr { color: ; }
-.lambda { color: ; }
-.number { color: ; }
-.regex-body { color: ; }
-.call-method { color: ; }
-*/
-
-.literal { color: green; }
-.true {}
-.false {}
-.undef {}
-
-.keyword { color: orange; }
-.class {}
-.or {}
-.define {}
-.unless {}
-.if {}
-.else {}
-.function {}
-.and {}
-.in {}
-
-.type { color: darkgreen; }
-.qn { color: darkgreen; }
-
-.var { color: blue; }
-.str-var { color: blue; }
-
-.name { color: red; }
-
-.string {
-	color: olive;
-}
-
-.comment {
-	color: grey;
-}
diff --git a/static-src/build_css.py b/static-src/build_css.py
new file mode 100755
index 0000000..38cb2c6
--- /dev/null
+++ b/static-src/build_css.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+
+"""
+(S)CSS generators for help with highlighting.
+
+:ref:`highlight-readme`.
+"""
+
+import yaml
+from datetime import datetime
+import argparse
+
+CSS_RULES = '''
+    color:            var(--hl-{name}-color);
+    background-color: var(--hl-{name}-background);
+    font-style:       var(--hl-{name}-font-style);
+    font-weight:      var(--hl-{name}-font-weight);
+'''
+
+HEADER = '''/*
+* File autogenerated from {source}.
+* Generated at {time}
+*/
+'''
+
+HIGHLIGHT_GROUPS = [
+    "comment",
+    "error",
+    "escape",
+    "interpolate",
+    "keyword",
+    "line",
+    "number",
+    "operator",
+    "regex",
+    "special",
+    "string",
+    "variable",
+]
+
+HIGHLIGHT_VARIABLES = [
+    'color',
+    'background',
+    'font-style',
+    'font-weight',
+]
+
+
+def build_highlight_map(source_file: str) -> str:
+    """
+    Generate CSS rules from source file.
+
+    :param source_file:
+        A yaml file containing mappings from our abstract highlight
+        groups, to concrete CSS classes.
+    :returns:
+        Generated CSS code, applicable to be written directly to a
+        file.
+    """
+    with open(source_file) as f:
+        data = yaml.full_load(f)
+    result = ''
+    result += HEADER.format(source=source_file,
+                            time=datetime.now())
+    for name, classes in data.items():
+        output: str = '\n'
+        output += ', '.join(f'.{c}' for c in classes)
+        output += ' {'
+        output += CSS_RULES.format(name=name)
+        output += '}'
+
+        result += output + '\n'
+
+    return result
+
+
+def build_colorscheme(source_file: str,
+                      selector: str = ':root') -> str:
+    """
+    Build CSS colorscheme from source file.
+
+    :param selector:
+        CSS selector to use for this group.
+    :param source_file:
+        Yaml file containing mapping from abrtract highlighting groups,
+        to CSS attributes.
+    :returns:
+        Generated CSS code, applicable to be written directly to a
+        file.
+    """
+    with open(source_file) as f:
+        data = yaml.full_load(f)
+
+    result = ''
+    result += HEADER.format(source=source_file,
+                            time=datetime.now())
+    result += f'{selector} {{\n'
+    for group_key in HIGHLIGHT_GROUPS:
+        if group := data.get(group_key):
+            for value_key in HIGHLIGHT_VARIABLES:
+                if value := group.get(value_key):
+                    result += f'    --hl-{group_key}-{value_key}: {value};\n'
+            if raw := group.get('raw-append'):
+                result += raw + '\n'
+    # Raw CSS rules, useful to capture something not handled by
+    # the abstract groups.
+    if raw := data.get('raw-append'):
+        result += raw + '\n'
+
+    result += '}\n'
+    return result
+
+
+def run_colorscheme(args: argparse.Namespace) -> None:
+    """Entry point when using the colorscheme mode."""
+    print(build_colorscheme(selector=args.selector,
+                            source_file=args.source))
+
+
+def run_highlight(args: argparse.Namespace) -> None:
+    """Entry point when using the highlight mode."""
+    print(build_highlight_map(source_file=args.source))
+
+
+def main() -> None:
+    """Primary entry point of program."""
+    parser = argparse.ArgumentParser()
+    subparsers = parser.add_subparsers()
+
+    parser_hl = subparsers.add_parser('highlight')
+    parser_hl.set_defaults(func=run_highlight)
+    parser_hl.add_argument('source')
+
+    parser_cs = subparsers.add_parser('colorscheme')
+    parser_cs.set_defaults(func=run_colorscheme)
+    parser_cs.add_argument('--selector',
+                           default=':root')
+    parser_cs.add_argument('source')
+
+    args = parser.parse_args()
+
+    args.func(args)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/static-src/colorscheme/default.yaml b/static-src/colorscheme/default.yaml
new file mode 100644
index 0000000..5fa61d3
--- /dev/null
+++ b/static-src/colorscheme/default.yaml
@@ -0,0 +1,23 @@
+comment:
+  color: '#3D7B7B'
+  font-style: italic
+error:
+escape:
+  color: red
+interpolate:
+keyword:
+  color: green
+  font-weight: bold
+line:
+number:
+  color: green
+operator:
+  color: '#666666'
+regex:
+  color: green
+special:
+  color: olive
+string:
+  color: '#BA2121'
+variable:
+  color: darkblue
diff --git a/static-src/highlight/andre_simon.yaml b/static-src/highlight/andre_simon.yaml
new file mode 100644
index 0000000..84dd00d
--- /dev/null
+++ b/static-src/highlight/andre_simon.yaml
@@ -0,0 +1,26 @@
+# highlight(1)
+comment:
+  - mlc
+escape:
+  - esc
+interpolate:
+  - ipl
+keyword:
+  - kwa
+line:
+  - lin
+number:
+  - num
+operator:
+  - sym
+regex:
+  - kwc
+string:
+  - sng
+variable:
+  - kwb
+error:
+  - err
+  - erm
+special:
+  - kwd
diff --git a/static-src/highlight/muppet.yaml b/static-src/highlight/muppet.yaml
new file mode 100644
index 0000000..93491ad
--- /dev/null
+++ b/static-src/highlight/muppet.yaml
@@ -0,0 +1,25 @@
+# Muppet's built in output
+comment:
+  - comment
+error:
+  - parse-error
+interpolate:
+  - str-var
+keyword:
+  - keyword
+  - literal
+operator:
+  - and
+  - or
+regex:
+  - regex
+special:
+  - parameter
+  - qr
+number:
+  - number
+string:
+  - string
+variable:
+  - qn
+  - var
diff --git a/static-src/highlight/pygments.yaml b/static-src/highlight/pygments.yaml
new file mode 100644
index 0000000..1f57a7a
--- /dev/null
+++ b/static-src/highlight/pygments.yaml
@@ -0,0 +1,61 @@
+# Pygmentize(1)
+comment:
+  - c
+  - ch
+  - cm
+  - cp
+  - cpf
+  - c1
+  - cs
+error:
+  - err
+escape:
+  - se
+interpolate:
+  - si
+keyword:
+  - k
+  - kc
+  - kd
+  - kn
+  - kp
+  - kr
+  - kt
+number:
+  - m
+  - mb
+  - mf
+  - mh
+  - mi
+  - mo
+operator:
+  - o
+regex:
+  - sr
+string:
+  - s
+  - sa
+  - sb
+  - sc
+  - dl
+  - sd
+  - s2
+  - sh
+  - sx
+  - sr
+  - s1
+variable:
+  - n
+  - nb
+  - nc
+  - 'no'
+  - nd
+  - ni
+  - ne
+  - nf
+  - nl
+  - nn
+  - nt
+  - nv
+special:
+  - na
diff --git a/static-src/style.scss b/static-src/style.scss
index b5c4407..2f0fba2 100644
--- a/static-src/style.scss
+++ b/static-src/style.scss
@@ -1,4 +1,3 @@
-
 /* -------------------------------------------------- */
 
 .parse-error {
@@ -28,9 +27,11 @@ h3 {
 
 /* -------------------------------------------------- */
 
+/*
 .documentation {
 	display: none;
 }
+*/
 
 /*
 .var {
@@ -79,6 +80,7 @@ code.json {
 	border-left-style: dotted;
 	display: inline-block;
 	padding-left: 1em;
+
 	font-family: sans;
 	font-size: 80%;
 
@@ -93,8 +95,20 @@ code.json {
 
 /* -------------------------------------------------- */
 
+@import "colorscheme_default";
+
+.highlight-pygments {
+	@import "highlight_pygments";
+}
+
+.highlight-andre-simon {
+	@import "highlight_andre_simon";
+}
+
+.highlight-muppet {
+	@import "highlight_muppet";
+}
+
 @import "breadcrumb";
-@import "highlight";
 @import "tabset";
-
 @import "color-headers";
diff --git a/static/.gitignore b/static/.gitignore
deleted file mode 100644
index b3a5267..0000000
--- a/static/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.css
diff --git a/templates/code_page.html b/templates/code_page.html
index 80ad037..0b588cc 100644
--- a/templates/code_page.html
+++ b/templates/code_page.html
@@ -8,7 +8,9 @@ Parameters:
 {% extends "base.html" %}
 {% block content %}
 	<ul>
-		<li><a href="source.pp.txt">Raw Source code</a></li>
+		<li><a href="index.html">Rendered</a></li>
+		<li><a href="source.pp.html">Source</a></li>
+		<li><a href="source.pp.txt">Raw Source</a></li>
 		<li><a href="source.json">JSON blob</a></li>
 	</ul>
 {{ content }}
-- 
GitLab