diff --git a/mkdocs.yml b/mkdocs.yml index 45195d7..044fe14 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,6 +36,9 @@ plugins: execute: true include_source: True +hooks: + - scripts/docs/hooks.py + extra: analytics: provider: google diff --git a/scripts/docs/hooks.py b/scripts/docs/hooks.py new file mode 100644 index 0000000..1b97abe --- /dev/null +++ b/scripts/docs/hooks.py @@ -0,0 +1,19 @@ +"""MkDocs lifecycle hooks.""" + +import shutil +import tempfile +from pathlib import Path + +BUILD_TEMP_DIR_CONFIG_KEY = "build_temp_dir" + + +def on_pre_build(config): + config[BUILD_TEMP_DIR_CONFIG_KEY] = Path(tempfile.mkdtemp(prefix="ravnar_docs_")) + + +def on_post_build(config): + build_temp_dir: Path | None = config.pop(BUILD_TEMP_DIR_CONFIG_KEY, None) + if build_temp_dir is None or not build_temp_dir.exists(): + return + + shutil.rmtree(build_temp_dir, ignore_errors=True) diff --git a/scripts/docs/macros.py b/scripts/docs/macros.py index 329570e..a56dc49 100644 --- a/scripts/docs/macros.py +++ b/scripts/docs/macros.py @@ -1,4 +1,5 @@ import json +import re import textwrap from pathlib import Path from typing import Any @@ -8,6 +9,7 @@ import pygments.util import yaml from markupsafe import Markup +from mkdocs_macros.plugin import MacrosPlugin from pydantic_settings import BaseSettings, PydanticBaseSettingsSource from _ravnar.config import Config @@ -91,7 +93,7 @@ def render(self, attrs: list[str], *values: Any) -> str: ) -def define_env(env): +def define_env(env: MacrosPlugin): config_options_renderer = ConfigOptionsRenderer() env.macro(config_options_renderer.render, name="config_options") @@ -108,3 +110,39 @@ def include_file(rel_path, language="") -> str: language = lexer_cls.aliases[0] return Markup(code(content, language=language)) + + +def on_pre_page_macros(env: MacrosPlugin) -> None: + """Preprocess tutorial notebook files to resolve cross-reference links. + + mkdocs-jupyter converts notebooks directly to HTML via nbconvert, bypassing the markdown pipeline where + autorefs/mkdocstrings would normally resolve [target][] and [display][target] cross-references. This hook patches + the page to point at a temp copy of the notebook with those links already resolved, so mkdocs-jupyter picks up the + processed version without touching the original source files. + """ + src_path = env.page.file.src_path + if not (src_path.startswith("tutorials/") and src_path.endswith((".py", ".ipynb"))): + return + + original = Path(env.page.file.abs_src_path).read_text() + + # Compute a relative path to references/python_api/ so links work regardless of + # whether the site is hosted at the root or a sub-path (e.g. /latest/). + depth = len(env.page.url.rstrip("/").split("/")) + python_api_reference = "../" * depth + "references/python_api/" + + def replace_crossref(match: re.Match) -> str: + display, target = match.group(1), match.group(2) + if not target: + display = f"`{display}`" + target = match.group(1) + return f"[{display}]({python_api_reference}#{target})" + + processed = re.sub(r"\[([^]]+)\]\[([^]]*)\]", replace_crossref, original) + + temp_dir: Path = env.conf["build_temp_dir"] / "mkdocs-jupyter-crossref" + temp_dir.mkdir(exist_ok=True) + + temp_file = temp_dir / Path(src_path).name + temp_file.write_text(processed) + env.page.file.abs_src_path = str(temp_file)