diff --git a/scripts/check_links.py b/scripts/check_links.py
index ba2907ee..d65a91b5 100644
--- a/scripts/check_links.py
+++ b/scripts/check_links.py
@@ -8,8 +8,13 @@
the browser verbatim and 404s.
This checker works on the built output instead, so it sees exactly what a
-reader's browser would request: every ```` must resolve to a real file
-in the build, and any fragment must match an ``id`` on the target page.
+reader's browser would request: every ```` and ``
`` must
+resolve to a real file in the build, and any fragment on an HTML target must
+match an ``id`` on that page.
+
+Checking ``
`` also keeps ``svg_theme.py`` honest: that hook rewrites
+figure tags to point at variants it generates separately, and nothing else
+notices when the two halves disagree.
Usage:
python scripts/check_links.py [site_dir]
@@ -31,11 +36,12 @@
class PageParser(HTMLParser):
- """Collect outgoing hrefs and available anchor ids from one page."""
+ """Collect outgoing hrefs, image srcs, and available anchor ids from one page."""
def __init__(self):
super().__init__(convert_charrefs=True)
self.hrefs: list[str] = []
+ self.srcs: list[str] = []
self.ids: set[str] = set()
def handle_starttag(self, tag, attrs):
@@ -47,6 +53,8 @@ def handle_starttag(self, tag, attrs):
self.ids.add(value)
if tag == "a" and (href := attrs.get("href")):
self.hrefs.append(href)
+ if tag == "img" and (src := attrs.get("src")):
+ self.srcs.append(src)
def parse(path: Path) -> PageParser:
@@ -83,7 +91,8 @@ def main() -> int:
rel_page = page.relative_to(site)
if str(rel_page).startswith(EXCLUDED_PREFIXES):
continue
- for href in doc.hrefs:
+ references = (*(("page", h) for h in doc.hrefs), *(("image", s) for s in doc.srcs))
+ for kind, href in references:
if EXTERNAL.match(href):
continue
path, fragment = urldefrag(href)
@@ -95,15 +104,18 @@ def main() -> int:
):
continue
if not target.is_file():
- failures.append(f"{rel_page}: '{href}' -> no such page")
+ failures.append(f"{rel_page}: '{href}' -> no such {kind}")
continue
- if fragment:
+ # A fragment names an anchor only on an HTML page. On anything else
+ # it is a selector hook -- svg_theme.py marks figure variants with
+ # #only-light / #only-dark -- and there is no document to look it up in.
+ if fragment and target.suffix == ".html":
target_doc = parsed.get(target) or parse(target)
if fragment not in target_doc.ids:
failures.append(f"{rel_page}: '{href}' -> no anchor '#{fragment}'")
if failures:
- print(f"{len(failures)} broken internal link(s) in the built site:\n")
+ print(f"{len(failures)} broken internal reference(s) in the built site:\n")
for failure in failures:
print(f" {failure}")
print(
@@ -113,7 +125,7 @@ def main() -> int:
)
return 1
- print(f"OK: internal links resolve across {len(pages)} built pages")
+ print(f"OK: internal links and images resolve across {len(pages)} built pages")
return 0
diff --git a/scripts/svg_theme.py b/scripts/svg_theme.py
index 998f19cb..8422a49c 100644
--- a/scripts/svg_theme.py
+++ b/scripts/svg_theme.py
@@ -34,13 +34,23 @@
)
IMG_TAG = re.compile(r"
]*?>", re.IGNORECASE)
-IMG_SRC = re.compile(r'\bsrc\s*=\s*"([^"]+?)"', re.IGNORECASE)
+IMG_SRC = re.compile(r"""\bsrc\s*=\s*(["'])(.+?)\1""", re.IGNORECASE)
-# Populated by on_files, consumed by on_post_page: the stems of the figures
+# Figures are keyed by their path under ``images/`` rather than by bare stem, so
+# that two figures of the same name in different subdirectories cannot collide.
+IMAGES_PREFIX = re.compile(rf"(?:^|/){re.escape(IMAGES_SUBDIR)}/")
+
+# Populated by on_files, consumed by on_post_page: the keys of the figures
# that actually carry a dark block.
_themed: set = set()
+def _image_key(path):
+ """Return a figure's path relative to the images directory, or None."""
+ match = IMAGES_PREFIX.search(path)
+ return path[match.end() :] if match else None
+
+
def _split_media_block(text):
"""Return (before, inner, after) around the dark block, or None."""
match = MEDIA_OPEN.search(text)
@@ -76,7 +86,7 @@ def on_files(files, config):
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)
+ _themed.add(file.src_uri[len(IMAGES_SUBDIR) + 1 :])
return files
@@ -89,11 +99,17 @@ def rewrite(match):
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:
+ path = src.group(2)
+ # Strip any query or fragment before testing and slicing. The variants
+ # carry their own #only-light / #only-dark marker, which the CSS keys on,
+ # so whatever the author wrote there cannot survive the rewrite anyway.
+ bare = path.split("#", 1)[0].split("?", 1)[0]
+ if not bare.lower().endswith(".svg"):
+ return tag
+ key = _image_key(bare)
+ if key is None or key not in _themed:
return tag
- base = path[: -len(".svg")]
+ base = bare[: -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
@@ -105,7 +121,7 @@ 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")):
+ for svg in sorted(images.rglob("*.svg")):
if svg.name.endswith((".light.svg", ".dark.svg")):
continue
pair = variants(svg.read_text(encoding="utf-8"))
diff --git a/src/images/pipeline.svg b/src/images/pipeline.svg
index e4ef0c90..6dcd26dc 100644
--- a/src/images/pipeline.svg
+++ b/src/images/pipeline.svg
@@ -6,13 +6,7 @@
prefers-color-scheme: dark. Colors are unique per role, so attribute
selectors override the inline presentation attributes unambiguously;
the three whites (page, code panel, part fill) are disambiguated by
- class since they map to different darks.
-
- On docs.datajoint.com the media query is resolved at build time by
- scripts/svg_theme.py, which splits this file into a light and a dark
- variant so the figure follows the site's palette toggle rather than the
- reader's OS setting. The media query still governs everywhere else the
- file is viewed directly (GitHub, a raw link, a README). -->
+ class since they map to different darks. -->