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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning][].

## [Unreleased]

### Changed

- brand accents are synced from the website’s `accents.json`.

### Fixed

- header shadow now only extends downward (#70)
Expand Down
1 change: 1 addition & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"includes": [
"**",
"!src/scverse_doc/registry.json",
"!src/scverse_doc/_accents.json",
"!src/scverse_doc/theme/scverse/static/_tokens.css",
],
},
Expand Down
2 changes: 1 addition & 1 deletion docs/kitchen-sink.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ third = "plain"
```pycon
>>> from scverse_doc import registry
>>> registry.packages["scanpy"].accent
'#de367b'
'oklch(0.608 0.209 1)'
```

## Table
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dev = [
"twine>=4.0.2",
]
test = [
"coloraide", # to check the accent lightness bounds
"coverage>=7.10",
"defusedxml", # for sphinx.testing’s HTML assertions
"pytest",
Expand Down
73 changes: 30 additions & 43 deletions scripts/sync_brand_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@
would create exactly the kind of drift this package exists to remove.

The website takes its neutral colours from Bootstrap’s tokens (``--bs-fg-*``, ``--bs-bg-*``, …),
which already carry dark values via ``light-dark()``, and defines only the brand hues itself.
This script looks up the tokens the theme needs in the website’s vendored Bootstrap and ``assets/main.scss``,
which already carry dark values via ``light-dark()``, and defines only the brand hues itself,
as OKLCH triples in ``assets/accents.json``.
This script looks up the tokens the theme needs in the website’s vendored Bootstrap and ``accents.json``,
resolves every ``var()`` down to literals, and writes them into ``_tokens.css`` as ``--scverse-color-x``.
The per-package accents go to ``_accents.json``, because the registry needs their values in Python
to derive readable shades from them.

Only the region between the marker comments is touched; the rest of the file is hand-authored.

The scverse logo used for the navbar link back to the website is copied verbatim for the same reason.
The scverse logo used for the navbar link back to the website is copied verbatim for the same reason,
as is ``accents.json``, which the registry reads the package accents from.

Usage
-----
Expand All @@ -37,16 +36,16 @@
HERE = Path(__file__).parent
STATIC = HERE.parent / "src" / "scverse_doc" / "theme" / "scverse" / "static"
TARGET = STATIC / "_tokens.css"
ACCENTS_TARGET = HERE.parent / "src" / "scverse_doc" / "_accents.json"

#: Website file -> theme static file, copied verbatim.
ASSETS = {Path("static/img/logo/scverse-fa.svg"): STATIC / "scverse-fa.svg"}
#: Website file -> package file, copied verbatim.
ASSETS = {
Path("static/img/logo/scverse-fa.svg"): STATIC / "scverse-fa.svg",
Path("assets/accents.json"): HERE.parent / "src" / "scverse_doc" / "_accents.json",
}

#: CSS custom property emitted here -> website custom property it is resolved from.
#: The pairing mirrors the one scverse/scverse.github.io#329 used when replacing its SCSS variables.
TOKEN_MAP = {
"--scverse-color-gradient-start": "--scverse-deep-blue",
"--scverse-color-gradient-end": "--scverse-sky-blue",
"--scverse-color-background": "--bs-bg-body",
"--scverse-color-text": "--bs-fg-body",
"--scverse-color-heading": "--bs-fg-1",
Expand All @@ -61,18 +60,10 @@
"--scverse-color-footer-bg": "--bs-bg-2",
}

#: Package -> website custom property holding its accent; ``default`` is for packages without one.
ACCENT_MAP = {
"default": "--scverse-deep-blue",
"anndata": "--anndata-orange",
"mudata": "--mudata-green",
"muon": "--muon-aquamarine",
"pertpy": "--pertpy-blue",
"scanpy": "--scanpy-cerise",
"scirpy": "--scirpy-purple",
"scvi-tools": "--scvi-yellow",
"spatialdata": "--spatialdata-blue",
"squidpy": "--squidpy-violet",
#: CSS custom property emitted here -> (package, colour name) in the website’s ``accents.json``.
ACCENT_TOKEN_MAP = {
"--scverse-color-gradient-start": ("scverse", "deep-blue"),
"--scverse-color-gradient-end": ("scverse", "sky-blue"),
}

BEGIN = "/* BEGIN GENERATED BRAND TOKENS – DO NOT EDIT; regenerate with scripts/sync_brand_tokens.py */"
Expand All @@ -89,36 +80,34 @@ def parse_bootstrap(css: str) -> dict[str, str]:
return dict(DECL_RE.findall(";".join(blocks) + ";"))


def parse_scss(scss: str) -> dict[str, str]:
"""Extract custom property declarations from the website’s SCSS."""
return dict(DECL_RE.findall(scss))


def resolve(name: str, props: dict[str, str]) -> str:
"""Return the value of `name` with every ``var()`` in it recursively substituted."""
return re.sub(r"var\((--[\w-]+)\)", lambda m: resolve(m[1], props), props[name])


def render_region(props: dict[str, str], indent: str) -> str:
def render_region(props: dict[str, str], accents: dict[str, list[tuple[str, list[float]]]], indent: str) -> str:
"""Render the generated region – marker comments included – indented by `indent`."""
if missing := sorted(set(TOKEN_MAP.values()) - set(props)):
msg = f"custom properties vanished from the website: {', '.join(missing)}"
raise KeyError(msg)
lines = [BEGIN, *(f"{prop}: {resolve(src, props)};" for prop, src in TOKEN_MAP.items()), END]
lines = [
BEGIN,
*(
f"{prop}: oklch({' '.join(map(str, dict(accents[pkg])[name]))});"
for prop, (pkg, name) in ACCENT_TOKEN_MAP.items()
),
*(f"{prop}: {resolve(src, props)};" for prop, src in TOKEN_MAP.items()),
END,
]
return "\n".join(indent + line for line in lines)


def render_accents(props: dict[str, str]) -> str:
"""Render the accent JSON read by ``scverse_doc.registry``."""
return json.dumps({pkg: resolve(src, props) for pkg, src in ACCENT_MAP.items()}, indent=4) + "\n"


def render(props: dict[str, str], current: str) -> str:
"""Return `current` with its generated region replaced by one rendered from `props`."""
def render(props: dict[str, str], accents: dict[str, list[tuple[str, list[float]]]], current: str) -> str:
"""Return `current` with its generated region replaced by one rendered from `props` and `accents`."""
if (region := REGION_RE.search(current)) is None:
msg = f"{TARGET} has no “{BEGIN}” … “{END}” region"
raise LookupError(msg)
return current[: region.start()] + render_region(props, region[1]) + current[region.end() :]
return current[: region.start()] + render_region(props, accents, region[1]) + current[region.end() :]


def main() -> int:
Expand All @@ -128,12 +117,10 @@ def main() -> int:
parser.add_argument("--check", action="store_true", help="fail instead of writing if the output would change")
args = parser.parse_args()

props = parse_bootstrap(
(args.website / "static" / "bootstrap" / "css" / "bootstrap.min.css").read_text()
) | parse_scss((args.website / "assets" / "main.scss").read_text())
props = parse_bootstrap((args.website / "static" / "bootstrap" / "css" / "bootstrap.min.css").read_text())
accents = json.loads((args.website / "assets" / "accents.json").read_text())
updates = {
TARGET: render(props, TARGET.read_text()).encode(),
ACCENTS_TARGET: render_accents(props).encode(),
TARGET: render(props, accents, TARGET.read_text()).encode(),
**{dst: (args.website / src).read_bytes() for src, dst in ASSETS.items()},
}
if args.check:
Expand Down
33 changes: 23 additions & 10 deletions src/scverse_doc/_accents.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
{
"default": "#262fb5",
"anndata": "#e5864b",
"mudata": "#4ab274",
"muon": "#6cf1a1",
"pertpy": "#30679a",
"scanpy": "#de367b",
"scirpy": "#da347f",
"scvi-tools": "#fbb822",
"spatialdata": "#40a9ff",
"squidpy": "#969dea"
"anndata": [["orange", [0.711, 0.138, 51]]],
"scanpy": [
["cerise", [0.608, 0.209, 1]],
["mandy", [0.634, 0.1737, 22]]
],
"mudata": [["green", [0.686, 0.134, 154.4]]],
"muon": [["aquamarine", [0.863, 0.163, 154.5]]],
"spatialdata": [["blue", [0.714, 0.158, 247.3]]],
"squidpy": [["violet", [0.72, 0.112, 279]]],
"scirpy": [
["purple", [0.601, 0.209, 358.5]],
["orange", [0.7824, 0.156, 69]]
],
"pertpy": [
["blue", [0.5, 0.1, 249]],
["red", [0.595, 0.177, 14]]
],
"scvi": [["yellow", [0.824, 0.1634, 81]]],
"scverse": [
["deep-blue", [0.405, 0.205, 270]],
["sky-blue", [0.799, 0.108, 236]],
["gold", [0.758, 0.2, 92]]
]
}
146 changes: 0 additions & 146 deletions src/scverse_doc/_color.py

This file was deleted.

10 changes: 7 additions & 3 deletions src/scverse_doc/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,15 @@ def _build_cache(app: Sphinx) -> Path:
return _cache_dir


#: Brand accents from the website, generated by ``scripts/sync_brand_tokens.py``.
_ACCENTS: dict[str, str] = json.loads(files(__package__).joinpath("_accents.json").read_text())
#: Brand accents from the website, copied by ``scripts/sync_brand_tokens.py``:
#: each key lists ``[name, [L, C, H]]`` pairs, primary first.
_ACCENTS: dict[str, str] = {
{"scvi": "scvi-tools"}.get(pkg, pkg): "oklch({} {} {})".format(*colors[0][1])
for pkg, colors in json.loads(files(__package__).joinpath("_accents.json").read_text()).items()
}

#: The website’s ``--scverse-deep-blue``, used when a package has no accent of its own.
DEFAULT_ACCENT = _ACCENTS.pop("default")
DEFAULT_ACCENT = _ACCENTS.pop("scverse")

#: Upstream ``category`` -> the ``kind`` recorded here.
#: ``core-infrastructure`` is skipped:
Expand Down
Loading
Loading