Skip to main content

Create the TextMate theme programatically

ID
694b1f4
date
2025-11-27 04:49:08+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
aad50ee
message
Create the TextMate theme programatically
changed files
15 files, 548 additions, 301 deletions

Changed files

.github/workflows/test.yml (758) → .github/workflows/test.yml (739)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f5d1a61..375477b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -38,7 +38,7 @@ jobs:
         ruff format --check .
 
     - name: Check types
-      run: mypy scripts/*.py --strict
+      run: mypy . --strict
 
     - name: Run tests
-      run: pytest scripts
+      run: pytest

README.md (16) → README.md (349)

diff --git a/README.md b/README.md
index d984164..2efe4bc 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,19 @@
 # colour-scheme
+
+## Steps
+
+1.  Get a new copy of the CSS files from the `alexwlchan.net` repo:
+
+    ```console
+    $ python3 vendor_css_files.py
+    ```
+
+2.  Generate a new set of theme files based on those colours:
+
+    ```console
+    $ python3 generate_palette_files.py
+    ```
+
+## TextMate
+
+Copy the generated theme files into the Bundle Editor.

TextMate.tmTheme (5729) → TextMate.tmTheme (0)

diff --git a/TextMate.tmTheme b/TextMate.tmTheme
deleted file mode 100644
index f410b33..0000000
--- a/TextMate.tmTheme
+++ /dev/null
@@ -1,264 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>author</key>
-	<string>Alex Chan</string>
-	<key>name</key>
-	<string>alexwlchan</string>
-	<key>semanticClass</key>
-	<string>theme.alexwlchan</string>
-	<key>settings</key>
-	<array>
-		<dict>
-			<key>settings</key>
-			<dict>
-				<key>background</key>
-				<string>#F7F7F7</string>
-				<key>caret</key>
-				<string>#000000</string>
-				<key>foreground</key>
-				<string>#000000</string>
-				<key>invisibles</key>
-				<string>#999999</string>
-				<key>lineHighlight</key>
-				<string>#FFEB12B3</string>
-				<key>selection</key>
-				<string>#FFEB12B3</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Text base</string>
-			<key>scope</key>
-			<string>text</string>
-			<key>settings</key>
-			<dict>
-				<key>background</key>
-				<string>#F7F7F7</string>
-				<key>foreground</key>
-				<string>#000000</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Source base</string>
-			<key>scope</key>
-			<string>source - source source</string>
-			<key>settings</key>
-			<dict>
-				<key>background</key>
-				<string>#F7F7F7</string>
-				<key>foreground</key>
-				<string>#000000</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Embedded source (text)</string>
-			<key>scope</key>
-			<string>text meta.embedded</string>
-			<key>settings</key>
-			<dict>
-				<key>background</key>
-				<string>#F7F7F7</string>
-				<key>foreground</key>
-				<string>#000000</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Embedded source (source)</string>
-			<key>scope</key>
-			<string>source meta.embedded</string>
-			<key>settings</key>
-			<dict>
-				<key>background</key>
-				<string>#F7F7F7</string>
-				<key>foreground</key>
-				<string>#000000</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Comment</string>
-			<key>scope</key>
-			<string>comment</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#d01c11</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Constant</string>
-			<key>scope</key>
-			<string>constant</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#3387CC</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Function name</string>
-			<key>scope</key>
-			<string>entity.name.function</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#115bda</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Variable</string>
-			<key>scope</key>
-			<string>variable</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#115bda</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Block comment</string>
-			<key>scope</key>
-			<string>source comment.block</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#d01c11</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>String</string>
-			<key>scope</key>
-			<string>string</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#1c9611</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>String escapes</string>
-			<key>scope</key>
-			<string>string constant.character.escape</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#1c9611</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>String (executed)</string>
-			<key>scope</key>
-			<string>string.interpolated</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#1c9611</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Regular expression</string>
-			<key>scope</key>
-			<string>string.regexp</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#CCCC33</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>String (literal)</string>
-			<key>scope</key>
-			<string>string.literal</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#1bad0e</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>String escapes (executed)</string>
-			<key>scope</key>
-			<string>string.interpolated constant.character.escape</string>
-			<key>settings</key>
-			<dict>
-				<key>foreground</key>
-				<string>#1bad0e</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Type name</string>
-			<key>scope</key>
-			<string>entity.name.type</string>
-			<key>settings</key>
-			<dict>
-				<key>fontStyle</key>
-				<string>underline</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Class inheritance</string>
-			<key>scope</key>
-			<string>entity.other.inherited-class</string>
-			<key>settings</key>
-			<dict>
-				<key>fontStyle</key>
-				<string>italic underline</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Tag name</string>
-			<key>scope</key>
-			<string>entity.name.tag</string>
-			<key>settings</key>
-			<dict>
-				<key>fontStyle</key>
-				<string>underline</string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Tag attribute</string>
-			<key>scope</key>
-			<string>entity.other.attribute-name</string>
-			<key>settings</key>
-			<dict>
-				<key>fontStyle</key>
-				<string></string>
-			</dict>
-		</dict>
-		<dict>
-			<key>name</key>
-			<string>Support function</string>
-			<key>scope</key>
-			<string>support.function</string>
-			<key>settings</key>
-			<dict>
-				<key>fontStyle</key>
-				<string></string>
-				<key>foreground</key>
-				<string>#C83730</string>
-			</dict>
-		</dict>
-	</array>
-	<key>uuid</key>
-	<string>37F22BDC-B2F4-11D9-850C-000A95A89C98</string>
-</dict>
-</plist>

dev_requirements.in (17) → dev_requirements.in (38)

diff --git a/dev_requirements.in b/dev_requirements.in
index daf7d81..ca2402a 100644
--- a/dev_requirements.in
+++ b/dev_requirements.in
@@ -1,3 +1,5 @@
+-r requirements.txt
+
 mypy
 pytest
 ruff

dev_requirements.txt (520) → dev_requirements.txt (635)

diff --git a/dev_requirements.txt b/dev_requirements.txt
index f2b9f6c..14e7a93 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -2,6 +2,12 @@
 #    uv pip compile dev_requirements.in --output-file dev_requirements.txt
 iniconfig==2.3.0
     # via pytest
+jinja2==3.1.6
+    # via -r requirements.txt
+markupsafe==3.0.3
+    # via
+    #   -r requirements.txt
+    #   jinja2
 mypy==1.18.2
     # via -r dev_requirements.in
 mypy-extensions==1.1.0

generate_palette_files.py (0) → generate_palette_files.py (4006)

diff --git a/generate_palette_files.py b/generate_palette_files.py
new file mode 100755
index 0000000..f371a6b
--- /dev/null
+++ b/generate_palette_files.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+
+from datetime import datetime, timezone
+import json
+from pathlib import Path
+import textwrap
+
+from jinja2 import Template
+
+from palette import Colours, Palette, enrich_colours
+
+
+def get_palette() -> tuple[str, Palette]:
+    """
+    Read the palette colours from `palette.json`.
+    """
+    with open("palette.json") as in_file:
+        data = json.load(in_file)
+
+    return data["id"], {
+        "light": enrich_colours(data["light"]),
+        "dark": enrich_colours(data["dark"]),
+    }
+
+
+def generate_textmate_theme(colours: Colours, palette_id: str) -> str:
+    """
+    Generate a TextMate theme based on my palette.
+    """
+    template = Template("""
+    // Generated from palette {{palette_id}} at {{now}}
+    // See https://github.com/alexwlchan/colour-scheme
+    {\tsettings = (
+    \t\t{%- for block in settings %}
+    \t\t{\t{% for k, v in block.items() -%}
+    \t\t\t\t{{ k }} = {% if v is mapping %}{
+    \t\t\t\t{%- for kk, vv in v.items() %}
+    \t\t\t\t{{ kk }} = '{{ vv }}';{% endfor %}
+    \t\t\t};{% else %}'{{ v }}';
+    \t\t\t{% endif %}{% endfor %}
+    \t\t},{% endfor %}
+    \t);
+    }
+    """)
+
+    settings = [
+        {
+            "settings": {
+                "foreground": colours["text"],
+                "background": colours["background"],
+                "caret": colours["text"],
+                "invisibles": colours["punctuation"],
+                "selection": colours["highlight"],
+                "lineHighlight": colours["highlight"],
+            }
+        },
+        {
+            "name": "Text base",
+            "scope": "text",
+            "settings": {
+                "foreground": colours["text"],
+                "background": colours["background"],
+            },
+        },
+        {
+            "name": "Source base",
+            "scope": "source - source source",
+            "settings": {
+                "foreground": colours["text"],
+                "background": colours["background"],
+            },
+        },
+    ]
+
+    for name, scope in [
+        ("Text base", "text"),
+        ("Source base", "source - source source"),
+        ("Embedded source (text)", "text meta.embedded"),
+        ("Embedded source (source)", "source meta.embedded"),
+    ]:
+        settings.append(
+            {
+                "name": name,
+                "scope": scope,
+                "settings": {
+                    "foreground": colours["text"],
+                    "background": colours["background"],
+                },
+            }
+        )
+
+    for scope, colour in [
+        ("comment", colours["comment"]),
+        ("source comment.block", colours["comment"]),
+        ("constant", colours["literal"]),
+        ("entity.name.function", colours["name"]),
+        ("variable", colours["name"]),
+        ("meta.class.ruby", colours["name"]),
+        ("keyword.control.class.ruby", colours["text"]),
+        ("meta.identifier.python", colours["name"]),
+        ("markup.heading.2.markdown", colours["name"]),
+        ("string", colours["string"]),
+        ("string constant.character.escape", colours["string"]),
+        ("string.interpolated", colours["string"]),
+        ("string.literal", colours["string"]),
+        ("string.interpolated constant.character.escape", colours["string"]),
+    ]:
+        settings.append(
+            {"name": scope, "scope": scope, "settings": {"foreground": colour}}
+        )
+
+    out = template.render(
+        settings=settings,
+        palette_id=palette_id,
+        now=datetime.now(tz=timezone.utc).isoformat(),
+    )
+    out = textwrap.dedent(out)
+    out = out.strip()
+    return out
+
+
+if __name__ == "__main__":
+    palette_id, palette = get_palette()
+
+    out_dir = Path("out")
+    out_dir.mkdir(exist_ok=True)
+
+    (out_dir / "TextMate_light.tmTheme").write_text(
+        generate_textmate_theme(colours=palette["light"], palette_id=palette_id)
+    )
+    (out_dir / "TextMate_dark.tmTheme").write_text(
+        generate_textmate_theme(colours=palette["dark"], palette_id=palette_id)
+    )

out/TextMate_dark.tmTheme (0) → out/TextMate_dark.tmTheme (2924)

diff --git a/out/TextMate_dark.tmTheme b/out/TextMate_dark.tmTheme
new file mode 100644
index 0000000..8fdd4f4
--- /dev/null
+++ b/out/TextMate_dark.tmTheme
@@ -0,0 +1,140 @@
+// Generated from palette 2477498-94fa872 at 2025-11-27T04:47:00.156478+00:00
+// See https://github.com/alexwlchan/colour-scheme
+{	settings = (
+		{	settings = {
+				foreground = '#c7c7c7';
+				background = '#0d0d0d';
+				caret = '#c7c7c7';
+				invisibles = '#9a9a9a';
+				selection = '#fffc4244';
+				lineHighlight = '#fffc4244';
+			};
+		},
+		{	name = 'Text base';
+			scope = 'text';
+			settings = {
+				foreground = '#c7c7c7';
+				background = '#0d0d0d';
+			};
+		},
+		{	name = 'Source base';
+			scope = 'source - source source';
+			settings = {
+				foreground = '#c7c7c7';
+				background = '#0d0d0d';
+			};
+		},
+		{	name = 'Text base';
+			scope = 'text';
+			settings = {
+				foreground = '#c7c7c7';
+				background = '#0d0d0d';
+			};
+		},
+		{	name = 'Source base';
+			scope = 'source - source source';
+			settings = {
+				foreground = '#c7c7c7';
+				background = '#0d0d0d';
+			};
+		},
+		{	name = 'Embedded source (text)';
+			scope = 'text meta.embedded';
+			settings = {
+				foreground = '#c7c7c7';
+				background = '#0d0d0d';
+			};
+		},
+		{	name = 'Embedded source (source)';
+			scope = 'source meta.embedded';
+			settings = {
+				foreground = '#c7c7c7';
+				background = '#0d0d0d';
+			};
+		},
+		{	name = 'comment';
+			scope = 'comment';
+			settings = {
+				foreground = '#f45858';
+			};
+		},
+		{	name = 'source comment.block';
+			scope = 'source comment.block';
+			settings = {
+				foreground = '#f45858';
+			};
+		},
+		{	name = 'constant';
+			scope = 'constant';
+			settings = {
+				foreground = '#ff42fc';
+			};
+		},
+		{	name = 'entity.name.function';
+			scope = 'entity.name.function';
+			settings = {
+				foreground = '#40c3ff';
+			};
+		},
+		{	name = 'variable';
+			scope = 'variable';
+			settings = {
+				foreground = '#40c3ff';
+			};
+		},
+		{	name = 'meta.class.ruby';
+			scope = 'meta.class.ruby';
+			settings = {
+				foreground = '#40c3ff';
+			};
+		},
+		{	name = 'keyword.control.class.ruby';
+			scope = 'keyword.control.class.ruby';
+			settings = {
+				foreground = '#c7c7c7';
+			};
+		},
+		{	name = 'meta.identifier.python';
+			scope = 'meta.identifier.python';
+			settings = {
+				foreground = '#40c3ff';
+			};
+		},
+		{	name = 'markup.heading.2.markdown';
+			scope = 'markup.heading.2.markdown';
+			settings = {
+				foreground = '#40c3ff';
+			};
+		},
+		{	name = 'string';
+			scope = 'string';
+			settings = {
+				foreground = '#5ff042';
+			};
+		},
+		{	name = 'string constant.character.escape';
+			scope = 'string constant.character.escape';
+			settings = {
+				foreground = '#5ff042';
+			};
+		},
+		{	name = 'string.interpolated';
+			scope = 'string.interpolated';
+			settings = {
+				foreground = '#5ff042';
+			};
+		},
+		{	name = 'string.literal';
+			scope = 'string.literal';
+			settings = {
+				foreground = '#5ff042';
+			};
+		},
+		{	name = 'string.interpolated constant.character.escape';
+			scope = 'string.interpolated constant.character.escape';
+			settings = {
+				foreground = '#5ff042';
+			};
+		},
+	);
+}
\ No newline at end of file

out/TextMate_light.tmTheme (0) → out/TextMate_light.tmTheme (2924)

diff --git a/out/TextMate_light.tmTheme b/out/TextMate_light.tmTheme
new file mode 100644
index 0000000..ecb214b
--- /dev/null
+++ b/out/TextMate_light.tmTheme
@@ -0,0 +1,140 @@
+// Generated from palette 2477498-94fa872 at 2025-11-27T04:47:00.154723+00:00
+// See https://github.com/alexwlchan/colour-scheme
+{	settings = (
+		{	settings = {
+				foreground = '#202020';
+				background = '#fafafa';
+				caret = '#202020';
+				invisibles = '#999999';
+				selection = '#ffeb12b3';
+				lineHighlight = '#ffeb12b3';
+			};
+		},
+		{	name = 'Text base';
+			scope = 'text';
+			settings = {
+				foreground = '#202020';
+				background = '#fafafa';
+			};
+		},
+		{	name = 'Source base';
+			scope = 'source - source source';
+			settings = {
+				foreground = '#202020';
+				background = '#fafafa';
+			};
+		},
+		{	name = 'Text base';
+			scope = 'text';
+			settings = {
+				foreground = '#202020';
+				background = '#fafafa';
+			};
+		},
+		{	name = 'Source base';
+			scope = 'source - source source';
+			settings = {
+				foreground = '#202020';
+				background = '#fafafa';
+			};
+		},
+		{	name = 'Embedded source (text)';
+			scope = 'text meta.embedded';
+			settings = {
+				foreground = '#202020';
+				background = '#fafafa';
+			};
+		},
+		{	name = 'Embedded source (source)';
+			scope = 'source meta.embedded';
+			settings = {
+				foreground = '#202020';
+				background = '#fafafa';
+			};
+		},
+		{	name = 'comment';
+			scope = 'comment';
+			settings = {
+				foreground = '#d01c11';
+			};
+		},
+		{	name = 'source comment.block';
+			scope = 'source comment.block';
+			settings = {
+				foreground = '#d01c11';
+			};
+		},
+		{	name = 'constant';
+			scope = 'constant';
+			settings = {
+				foreground = '#c311d0';
+			};
+		},
+		{	name = 'entity.name.function';
+			scope = 'entity.name.function';
+			settings = {
+				foreground = '#115bda';
+			};
+		},
+		{	name = 'variable';
+			scope = 'variable';
+			settings = {
+				foreground = '#115bda';
+			};
+		},
+		{	name = 'meta.class.ruby';
+			scope = 'meta.class.ruby';
+			settings = {
+				foreground = '#115bda';
+			};
+		},
+		{	name = 'keyword.control.class.ruby';
+			scope = 'keyword.control.class.ruby';
+			settings = {
+				foreground = '#202020';
+			};
+		},
+		{	name = 'meta.identifier.python';
+			scope = 'meta.identifier.python';
+			settings = {
+				foreground = '#115bda';
+			};
+		},
+		{	name = 'markup.heading.2.markdown';
+			scope = 'markup.heading.2.markdown';
+			settings = {
+				foreground = '#115bda';
+			};
+		},
+		{	name = 'string';
+			scope = 'string';
+			settings = {
+				foreground = '#1bad0e';
+			};
+		},
+		{	name = 'string constant.character.escape';
+			scope = 'string constant.character.escape';
+			settings = {
+				foreground = '#1bad0e';
+			};
+		},
+		{	name = 'string.interpolated';
+			scope = 'string.interpolated';
+			settings = {
+				foreground = '#1bad0e';
+			};
+		},
+		{	name = 'string.literal';
+			scope = 'string.literal';
+			settings = {
+				foreground = '#1bad0e';
+			};
+		},
+		{	name = 'string.interpolated constant.character.escape';
+			scope = 'string.interpolated constant.character.escape';
+			settings = {
+				foreground = '#1bad0e';
+			};
+		},
+	);
+}
\ No newline at end of file

palette.json (362) → palette.json (526)

diff --git a/palette.json b/palette.json
index 6b7693e..e5c17cf 100644
--- a/palette.json
+++ b/palette.json
@@ -1,6 +1,9 @@
 {
   "id": "2477498-94fa872",
   "light": {
+    "background": "#fafafa",
+    "text": "#202020",
+    "accent_grey": "#999999",
     "red": "#d01c11",
     "green": "#1bad0e",
     "blue": "#115bda",
@@ -9,11 +12,14 @@
     "highlight": "#ffeb12b3"
   },
   "dark": {
+    "background": "#0d0d0d",
+    "text": "#c7c7c7",
+    "accent_grey": "#9a9a9a",
     "red": "#f45858",
     "green": "#5ff042",
     "blue": "#40c3ff",
     "magenta": "#ff42fc",
     "yellow": "#fffc42",
-    "highlight": "#fffc42cc"
+    "highlight": "#fffc4244"
   }
 }
\ No newline at end of file

palette.py (0) → palette.py (861)

diff --git a/palette.py b/palette.py
new file mode 100644
index 0000000..3f9ceff
--- /dev/null
+++ b/palette.py
@@ -0,0 +1,48 @@
+from typing import TypedDict
+
+
+class BaseColours(TypedDict):
+    background: str
+    text: str
+    accent_grey: str
+    red: str
+    green: str
+    blue: str
+    magenta: str
+    yellow: str
+    highlight: str
+
+
+class Colours(TypedDict):
+    background: str
+    text: str
+    comment: str
+    literal: str
+    string: str
+    name: str
+    punctuation: str
+    highlight: str
+
+
+def enrich_colours(c: BaseColours) -> Colours:
+    return {
+        "background": c["background"],
+        "text": c["text"],
+        "comment": c["red"],
+        "literal": c["magenta"],
+        "string": c["green"],
+        "name": c["blue"],
+        "punctuation": c["accent_grey"],
+        "highlight": c["highlight"],
+    }
+
+
+class BasePalette(TypedDict):
+    id: str
+    light: BaseColours
+    dark: BaseColours
+
+
+class Palette(TypedDict):
+    light: Colours
+    dark: Colours

requirements.in (0) → requirements.in (7)

diff --git a/requirements.in b/requirements.in
new file mode 100644
index 0000000..7f7afbf
--- /dev/null
+++ b/requirements.in
@@ -0,0 +1 @@
+jinja2

requirements.txt (0) → requirements.txt (208)

diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..398b0bf
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+# This file was autogenerated by uv via the following command:
+#    uv pip compile requirements.in --output-file requirements.txt
+jinja2==3.1.6
+    # via -r requirements.in
+markupsafe==3.0.3
+    # via jinja2

scripts/palette.py (228) → scripts/palette.py (0)

diff --git a/scripts/palette.py b/scripts/palette.py
deleted file mode 100644
index 7985b7c..0000000
--- a/scripts/palette.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from typing import TypedDict
-
-
-class Colours(TypedDict):
-    red: str
-    green: str
-    blue: str
-    magenta: str
-    yellow: str
-    highlight: str
-
-
-class Palette(TypedDict):
-    id: str
-    light: Colours
-    dark: Colours

scripts/test_vendor_css_files.py (572) → test_vendor_css_files.py (654)

diff --git a/scripts/test_vendor_css_files.py b/test_vendor_css_files.py
similarity index 94%
rename from scripts/test_vendor_css_files.py
rename to test_vendor_css_files.py
index 6f73e54..b99808c 100644
--- a/scripts/test_vendor_css_files.py
+++ b/test_vendor_css_files.py
@@ -12,6 +12,8 @@ from vendor_css_files import get_colour_variable
         ("--red:   #ff0000;", "red", "#ff0000"),
         # Alpha channel
         ("--red: #ff0000ff;", "red", "#ff0000ff"),
+        # Three-digit hex in source
+        ("--grey: #999;", "grey", "#999999"),
     ],
 )
 def test_get_colour_variable(css: str, name: str, colour: str) -> None:

scripts/vendor_css_files.py (3379) → vendor_css_files.py (4507)

diff --git a/scripts/vendor_css_files.py b/vendor_css_files.py
similarity index 67%
rename from scripts/vendor_css_files.py
rename to vendor_css_files.py
index 5a4669e..6ce7029 100755
--- a/scripts/vendor_css_files.py
+++ b/vendor_css_files.py
@@ -11,7 +11,7 @@ import re
 import shutil
 import subprocess
 
-from palette import Colours, Palette
+from palette import BaseColours, BasePalette
 
 
 def get_alexwlchan_net_css(css_name: str) -> tuple[str, str]:
@@ -55,40 +55,65 @@ def get_colour_variable(css: str, *, name: str) -> str:
     #     --red:   #ff0000;
     #     --red: #ff0000ff;
     #
-    m = re.search(f"--{name}:" + r"\s*(?P<colour>#[0-9a-f]{6}([0-9a-f]{2})?);", css)
+    m = re.search(f"{name}:" + r"\s*(?P<colour>#[0-9a-f]+);", css)
 
     if m is None:
         raise ValueError(f"cannot find variable --{name} in CSS")
 
-    return m.group("colour")
+    c = m.group("colour")
+
+    # 6- or 8-digit hex colour
+    if len(c) == 7 or len(c) == 9:
+        return c
+
+    # 3-digit hex colour, so double each digit
+    if len(c) == 4:
+        return f"#{c[1] * 2}{c[2] * 2}{c[3] * 2}"
+
+    raise ValueError(f"unrecognised hex string: {c}")
 
 
 if __name__ == "__main__":
     variable_id, variable_css = get_alexwlchan_net_css("variables.scss")
     syntax_id, syntax_css = get_alexwlchan_net_css("components/syntax_highlighting.css")
 
-    light_colours: Colours = {
-        "red": get_colour_variable(variable_css, name="default-primary-color-light"),
-        "green": get_colour_variable(syntax_css, name="green"),
-        "blue": get_colour_variable(syntax_css, name="blue"),
-        "magenta": get_colour_variable(syntax_css, name="magenta"),
-        "yellow": get_colour_variable(syntax_css, name="yellow"),
-        "highlight": get_colour_variable(syntax_css, name="highlight"),
+    light_colours: BaseColours = {
+        "background": get_colour_variable(
+            variable_css, name="--background-color-light"
+        ),
+        "text": get_colour_variable(variable_css, name="--body-text-light"),
+        "accent_grey": get_colour_variable(variable_css, name="--accent-grey-light"),
+        "red": get_colour_variable(variable_css, name="--default-primary-color-light"),
+        "green": get_colour_variable(syntax_css, name="--green"),
+        "blue": get_colour_variable(syntax_css, name="--blue"),
+        "magenta": get_colour_variable(syntax_css, name="--magenta"),
+        "yellow": get_colour_variable(syntax_css, name="--yellow"),
+        "highlight": get_colour_variable(syntax_css, name="--highlight"),
     }
 
     # Get the first block of dark theme colours from the syntax highlighting
     # CSS. This is a bit crude, but it works for now.
     _, dark_syntax_css = syntax_css.split("@media (prefers-color-scheme: dark) {")
-    dark_colours: Colours = {
-        "red": get_colour_variable(variable_css, name="default-primary-color-dark"),
-        "green": get_colour_variable(dark_syntax_css, name="green"),
-        "blue": get_colour_variable(dark_syntax_css, name="blue"),
-        "magenta": get_colour_variable(dark_syntax_css, name="magenta"),
-        "yellow": get_colour_variable(dark_syntax_css, name="yellow"),
-        "highlight": get_colour_variable(dark_syntax_css, name="highlight"),
+    dark_colours: BaseColours = {
+        "background": get_colour_variable(variable_css, name="--background-color-dark"),
+        "text": get_colour_variable(variable_css, name="--body-text-dark"),
+        "accent_grey": get_colour_variable(variable_css, name="--accent-grey-dark"),
+        "red": get_colour_variable(variable_css, name="--default-primary-color-dark"),
+        "green": get_colour_variable(dark_syntax_css, name="--green"),
+        "blue": get_colour_variable(dark_syntax_css, name="--blue"),
+        "magenta": get_colour_variable(dark_syntax_css, name="--magenta"),
+        "yellow": get_colour_variable(dark_syntax_css, name="--yellow"),
+        "highlight": get_colour_variable(dark_syntax_css, name="--highlight"),
     }
 
-    palette: Palette = {
+    # When I do <mark> highlights on my blog, I keep the text black in
+    # dark mode, but for my themes, use a more muted yellow.
+    if dark_colours["highlight"] == "#fffc42cc":
+        dark_colours["highlight"] = "#fffc4244"
+    else:
+        raise ValueError(f"Unrecognised dark colour: {dark_colours['highlight']}")
+
+    palette: BasePalette = {
         "id": f"{variable_id}-{syntax_id}",
         "light": light_colours,
         "dark": dark_colours,