Source code for sphinx_2009scape_theme

"""A Sphinx theme with the look and feel of 2009-era RuneScape.

The visual language (palette, pixel fonts, bevelled borders, stone panels) is
ported from the ``runescapecn`` shadcn registry, which is vendored under
``vendor/runescapecn`` in this repository for reference.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any

from sphinx.errors import ExtensionError

if TYPE_CHECKING:
    from sphinx.application import Sphinx
    from sphinx.util.typing import ExtensionMetadata

__version__ = "1.0.2"
__all__ = ["__version__", "setup"]

THEME_PATH = (Path(__file__).parent / "theme" / "2009scape").resolve()

_CSS_VARIABLE_OPTIONS = ("css_variables",)

FONT_STACKS = {
    "rs": "'RuneScape Plain 12', 'RuneScape Plain 11', monospace",
    "rs-bold": "'RuneScape Bold 12', 'RuneScape Plain 12', monospace",
    "rs-quill": "'RuneScape Quill', 'RuneScape Quill 8', serif",
    "rs-quill-caps": "'RuneScape Quill Caps', 'RuneScape Quill', serif",
    "rs-surok": "'RuneScape Surok', serif",
    "rs-barbarian": "'RuneScape Barbarian Assault', sans-serif",
    "rs-fairy": "'RuneScape Fairy Large', 'RuneScape Fairy', serif",
    "system": (
        "system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', "
        "Arial, sans-serif"
    ),
}


def _resolve_font(value: str) -> str:
    """Map a font alias to a CSS font stack, passing through raw stacks."""
    value = (value or "").strip()
    if not value:
        return ""
    return FONT_STACKS.get(value, value)


def _css_variable_block(variables: dict[str, str]) -> str:
    """Render a mapping into the body of a CSS rule."""
    declarations = []
    for name, value in variables.items():
        name = name.strip()
        if not name.startswith("--"):
            name = f"--{name}"
        declarations.append(f"    {name}: {value};")
    return "\n".join(declarations)


def _edit_page_url(app: Sphinx, pagename: str, context: dict[str, Any]) -> str:
    """Build a "view/edit this page" URL from the ``source_*`` theme options."""
    options = app.builder.theme_options if app.builder else {}
    repository = str(options.get("source_repository", "") or "").rstrip("/")
    if not repository:
        return ""

    branch = str(options.get("source_branch", "") or "main")
    directory = str(options.get("source_directory", "") or "").strip("/")
    suffix = context.get("page_source_suffix") or ".rst"

    path = f"{pagename}{suffix}"
    if directory:
        path = f"{directory}/{path}"

    if "github.com" in repository:
        return f"{repository}/blob/{branch}/{path}"
    if "gitlab" in repository:
        return f"{repository}/-/blob/{branch}/{path}"
    return f"{repository}/{branch}/{path}"


def _update_context(
    app: Sphinx,
    pagename: str,
    templatename: str,
    context: dict[str, Any],
    doctree: Any,
) -> None:
    """Expose derived values (fonts, CSS overrides, edit URL) to templates."""
    options = app.builder.theme_options if app.builder else {}

    overrides: dict[str, str] = {}
    for option in _CSS_VARIABLE_OPTIONS:
        value = options.get(option) or {}
        if isinstance(value, str):
            value = {}
        if not isinstance(value, dict):
            msg = (
                f"html_theme_options[{option!r}] must be a mapping of CSS "
                f"custom properties, got {type(value).__name__}."
            )
            raise ExtensionError(msg)
        overrides.update(value)

    for option, variable in (
        ("body_font", "--rs-font-body"),
        ("heading_font", "--rs-font-heading"),
        ("mono_font", "--rs-font-mono"),
    ):
        stack = _resolve_font(str(options.get(option, "") or ""))
        if stack:
            overrides[variable] = stack

    body_font = str(options.get("body_font", "") or "").strip()
    if body_font and not body_font.startswith("rs"):
        overrides.setdefault("--rs-font-smoothing", "antialiased")

    base_size = str(options.get("base_font_size", "") or "").strip()
    if base_size:
        if base_size.isdigit():
            base_size = f"{base_size}px"
        overrides["--rs-font-size-base"] = base_size

    context["rs_css_overrides"] = _css_variable_block(overrides)
    context["rs_edit_page_url"] = _edit_page_url(app, pagename, context)
    context["rs_theme_version"] = __version__


def _add_assets(app: Sphinx) -> None:
    """Register the theme's JavaScript, but only when the theme is in use."""
    if app.config.html_theme != "2009scape":
        return
    app.add_js_file("js/2009scape.js", loading_method="defer")


[docs] def setup(app: Sphinx) -> ExtensionMetadata: app.require_sphinx("7.3") app.add_html_theme("2009scape", str(THEME_PATH)) app.connect("builder-inited", _add_assets) app.connect("html-page-context", _update_context) return { "version": __version__, "parallel_read_safe": True, "parallel_write_safe": True, }