Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ theme:
toggle:
icon: material/brightness-4
name: Switch to light mode
hooks:
# Resolve figure light/dark palettes against the site toggle, not the OS.
- scripts/svg_theme.py
plugins:
- search
- redirects:
Expand Down
116 changes: 116 additions & 0 deletions scripts/svg_theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Make figure SVGs follow the site's palette toggle, not the OS preference.

Every themed figure in ``src/images`` carries its dark palette in an
``@media (prefers-color-scheme: dark)`` block. That works when the SVG is
viewed on its own (GitHub, a README, a raw link), but the figures are embedded
with ``<img src=...>``, and an SVG loaded that way is a separate document: its
stylesheet sees the *operating system* preference and never sees the
``data-md-color-scheme`` attribute that Material's light/dark toggle flips on
the host page. A reader on a dark desktop who switches the site to light got a
dark figure on a light page, and vice versa.

This hook keeps one source file per figure and resolves the theme at build
time instead:

* ``on_post_build`` writes two derived variants next to each themed SVG --
``<name>.light.svg`` (the dark block dropped) and ``<name>.dark.svg`` (the
dark block applied unconditionally).
* ``on_post_page`` rewrites each ``<img>`` that points at a themed SVG into a
pair of ``<img>`` tags marked ``#only-light`` / ``#only-dark``; the rules in
``extra.css`` show exactly one of them per scheme.

The original SVG is still published unchanged, so external references to it
keep working and keep adapting to the OS preference.
"""

import re
from pathlib import Path

IMAGES_SUBDIR = "images"

# Tolerates both hand-authored spacing and Mermaid's minified output.
MEDIA_OPEN = re.compile(
r"@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)\s*\{", re.IGNORECASE
)

IMG_TAG = re.compile(r"<img\b[^>]*?>", re.IGNORECASE)
IMG_SRC = re.compile(r'\bsrc\s*=\s*"([^"]+?)"', re.IGNORECASE)

# Populated by on_files, consumed by on_post_page: the stems of the figures
# that actually carry a dark block.
_themed: set = set()


def _split_media_block(text):
"""Return (before, inner, after) around the dark block, or None."""
match = MEDIA_OPEN.search(text)
if not match:
return None
depth = 1
i = match.end()
while i < len(text) and depth:
if text[i] == "{":
depth += 1
elif text[i] == "}":
depth -= 1
i += 1
if depth:
raise ValueError("unbalanced braces in prefers-color-scheme block")
return text[: match.start()], text[match.end() : i - 1], text[i:]


def variants(text):
"""Return (light, dark) renderings of a themed SVG, or None if unthemed."""
split = _split_media_block(text)
if split is None:
return None
before, inner, after = split
return before + after, before + inner + after


def on_files(files, config):
_themed.clear()
for file in files:
if not file.src_uri.startswith(f"{IMAGES_SUBDIR}/"):
continue
if not file.src_uri.endswith(".svg"):
continue
if MEDIA_OPEN.search(Path(file.abs_src_path).read_text(encoding="utf-8")):
_themed.add(Path(file.src_uri).stem)
return files


def on_post_page(output, page, config):
if not _themed:
return output

def rewrite(match):
tag = match.group(0)
src = IMG_SRC.search(tag)
if not src:
return tag
path = src.group(1)
stem = Path(path.split("#")[0].split("?")[0]).stem
if not path.lower().endswith(".svg") or stem not in _themed:
return tag
base = path[: -len(".svg")]
light = IMG_SRC.sub(lambda _: f'src="{base}.light.svg#only-light"', tag, count=1)
dark = IMG_SRC.sub(lambda _: f'src="{base}.dark.svg#only-dark"', tag, count=1)
return light + dark

return IMG_TAG.sub(rewrite, output)


def on_post_build(config):
images = Path(config["site_dir"]) / IMAGES_SUBDIR
if not images.is_dir():
return
for svg in sorted(images.glob("*.svg")):
if svg.name.endswith((".light.svg", ".dark.svg")):
continue
pair = variants(svg.read_text(encoding="utf-8"))
if pair is None:
continue
light, dark = pair
svg.with_suffix(".light.svg").write_text(light, encoding="utf-8")
svg.with_suffix(".dark.svg").write_text(dark, encoding="utf-8")
19 changes: 19 additions & 0 deletions src/assets/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,22 @@
.jp-OutputArea-output code {
font-size: 0.8rem;
}

/* -------------------------------------------------------------------
Theme-aware figures

scripts/svg_theme.py emits each themed figure twice, tagged #only-light
and #only-dark, because an SVG embedded with <img> cannot see the palette
toggle on the host page. These rules pick the variant that matches the
scheme the reader actually selected.
------------------------------------------------------------------- */

.md-typeset img[src$="#only-dark"] {
display: none;
}
[data-md-color-scheme="slate"] .md-typeset img[src$="#only-light"] {
display: none;
}
[data-md-color-scheme="slate"] .md-typeset img[src$="#only-dark"] {
display: inline;
}
2 changes: 1 addition & 1 deletion src/explanation/data-pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A **scientific data pipeline** extends beyond a database with computations. It i

At the heart of every DataJoint pipeline is an **open-source core** of three components that handle schema, computation, and storage. The managed **DataJoint Platform** extends this core with services for AI access, data ingestion, exploration, security, collaboration, and visualization.

![DataJoint Platform Architecture](../images/dj-platform.svg/)
![DataJoint Platform Architecture](../images/dj-platform.svg)

| Open-Source Core | Purpose |
|------------------|---------|
Expand Down
8 changes: 7 additions & 1 deletion src/images/pipeline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading