refactor: migrate autorefs MkDocs plugin replacement

Signed-off-by: squidfunk <martin.donath@squidfunk.com>
This commit is contained in:
squidfunk
2026-09-01 18:35:54 +02:00
parent 3a390c7647
commit 83b0fd7541
19 changed files with 1558 additions and 522 deletions
+26
View File
@@ -174,6 +174,32 @@ Not indexed.
assert _read_index(disabled)["items"] == []
def test_search_exclusion_attribute_is_removed_from_page(
tmp_path: Path,
) -> None:
"""Search pragmas affect the index but do not leak into final HTML."""
config = _write_project(tmp_path, plugins=" - search")
(tmp_path / "docs" / "index.md").write_text(
"""\
# Landing
Visible body.
<div data-search-exclude><p>Hidden body.</p></div>
""",
encoding="utf-8",
)
zensical.build(str(config), _BUILD_OPTIONS)
index = _read_index(tmp_path)
assert "Visible body." in index["items"][0]["text"]
assert "Hidden body." not in index["items"][0]["text"]
page = (tmp_path / "site" / "index.html").read_text()
assert "Hidden body." in page
assert "data-search-exclude" not in page
def test_search_rebuild_replaces_changed_and_removed_pages(
tmp_path: Path,
) -> None:
+40
View File
@@ -396,3 +396,43 @@ custom_dir = "overrides"
captured = capfd.readouterr()
assert "No issues found" in captured.err
def test_cached_template_refreshes_when_page_autoref_changes(
tmp_path: Path,
) -> None:
"""Page-local autoref facts participate in the template cache key."""
docs = tmp_path / "docs"
docs.mkdir()
source = docs / "index.md"
source.write_text(
"# Home\n\n[First title][first-target]\n", encoding="utf-8"
)
(docs / "other.md").write_text(
"# Other\n\n## first-target\n\n## second-target\n",
encoding="utf-8",
)
config = tmp_path / "zensical.toml"
config.write_text(
"""
[project]
site_name = "Test"
[project.plugins.autorefs]
""".lstrip(),
encoding="utf-8",
)
zensical.build(str(config), {"clean": True, "strict": False})
output = (tmp_path / "site" / "index.html").read_text(encoding="utf-8")
assert 'href="other/#first-target">First title</a>' in output
# Both references occupy page-local slot zero. Only their cached facts
# distinguish the template inputs after the Markdown pass.
source.write_text(
"# Home\n\n[Second title][second-target]\n", encoding="utf-8"
)
zensical.build(str(config), {"clean": False, "strict": False})
output = (tmp_path / "site" / "index.html").read_text(encoding="utf-8")
assert 'href="other/#second-target">Second title</a>' in output
assert 'href="other/#first-target">First title</a>' not in output
+39 -1
View File
@@ -29,7 +29,13 @@ from typing import TYPE_CHECKING, Any
import pytest
from tests.unit.extensions.conftest import soup
from zensical.extensions.autorefs import get_autorefs_store, reset
from zensical.extensions.autorefs import (
get_autorefs_inventory_data,
get_autorefs_page_data,
get_autorefs_store,
reset,
)
from zensical.extensions.context import Page
if TYPE_CHECKING:
from collections.abc import Generator
@@ -80,6 +86,38 @@ def _reset_autorefs_store() -> Generator[None, None, None]:
reset()
# ---------------------------------------------------------------------------
# Store
# ---------------------------------------------------------------------------
class TestStore:
"""Tests for page-local fact extraction from the transient store."""
def test_page_data_is_taken_without_consuming_inventory(self) -> None:
"""Page registrations leave the global inventory available."""
store = get_autorefs_store()
page = Page(url="guide/", path="guide.md", meta={})
store.set_page(page)
store.register_anchor(page, "target", title="Target")
store.register_anchor(page, "alias", anchor="target", primary=False)
store.register_url("external", "https://example.com/external")
assert get_autorefs_page_data("guide/") == {
"primary": {"target": ["guide/#target"]},
"secondary": {"alias": ["guide/#target"]},
"titles": {"guide/#target": "Target"},
}
assert get_autorefs_page_data("guide/") == {
"primary": {},
"secondary": {},
"titles": {},
}
assert get_autorefs_inventory_data() == {
"external": "https://example.com/external"
}
# ---------------------------------------------------------------------------
# Inline processor
# ---------------------------------------------------------------------------
+62 -33
View File
@@ -58,9 +58,6 @@ if TYPE_CHECKING:
HTAGS = {"h1", "h2", "h3", "h4", "h5", "h6"}
AUTOREF_RE = re.compile(
r"<autoref (?P<attrs>.*?)>(?P<title>.*?)</autoref>", flags=re.DOTALL
)
# ----------------------------------------------------------------------------
@@ -88,24 +85,59 @@ class AutorefsStore:
self._secondary_url_map: dict[str, list[str]] = {}
self._abs_url_map: dict[str, str] = {}
self._title_map: dict[str, str] = {}
self._updated_pages: set[str] = set()
self._page_registrations: dict[str, set[tuple[bool, str, str]]] = {}
def set_page(self, page: Page) -> None:
"""Set the current page and discard its previous registrations."""
self.current_page = page
self._updated_pages.add(page.url)
for primary, identifier, url in self._page_registrations.pop(
page.url, set()
):
url_map = (
self._primary_url_map if primary else self._secondary_url_map
)
urls = url_map[identifier]
urls.remove(url)
if not urls:
del url_map[identifier]
self._title_map.pop(url, None)
self.pop_page(page.url)
def pop_page(self, page_url: str) -> dict[str, Any]:
"""Remove and return registrations owned by one page."""
registrations = self._page_registrations.pop(page_url, set())
primary = self._pop_urls(self._primary_url_map, registrations, True)
secondary = self._pop_urls(
self._secondary_url_map, registrations, False
)
urls = {
url
for values in (primary, secondary)
for entries in values.values()
for url in entries
}
titles = {
url: self._title_map.pop(url)
for url in urls
if url in self._title_map
}
return {
"primary": primary,
"secondary": secondary,
"titles": titles,
}
@staticmethod
def _pop_urls(
url_map: dict[str, list[str]],
registrations: set[tuple[bool, str, str]],
primary: bool,
) -> dict[str, list[str]]:
"""Remove registered URLs from one URL map, preserving their order."""
selected: dict[str, set[str]] = {}
for is_primary, identifier, url in registrations:
if is_primary == primary:
selected.setdefault(identifier, set()).add(url)
result: dict[str, list[str]] = {}
for identifier, selected_urls in selected.items():
urls = url_map.get(identifier, [])
result[identifier] = [url for url in urls if url in selected_urls]
remaining = [url for url in urls if url not in selected_urls]
if remaining:
url_map[identifier] = remaining
else:
url_map.pop(identifier, None)
return result
def register_anchor(
self,
@@ -266,7 +298,7 @@ class AutorefsInlineProcessor(ReferenceInlineProcessor):
def _make_tag(
self, identifier: str, text: str, *, slug: str | None = None
) -> Element:
"""Create a tag that can be matched by `AUTO_REF_RE`."""
"""Create a tag that can be resolved after site settlement."""
el = Element("autoref")
if self.hook:
identifier = self.hook.expand_identifier(identifier)
@@ -436,25 +468,22 @@ def get_autorefs_store() -> AutorefsStore:
return AUTOREFS
def get_autorefs_data() -> dict[str, Any]:
"""Get autorefs data.
def get_autorefs_page_data(page_url: str) -> dict[str, Any]:
"""Take page-local autorefs data.
This function is called from Rust to replace the `<autoref>`
elements written in the HTML output by both the autorefs
Markdown extension (for manual cross-references) and the
mkdocstrings extension (for automatic cross-references).
Rust combines these registrations into the settled URL registry used to
resolve `<autoref>` elements emitted by autorefs and mkdocstrings.
"""
if AUTOREFS:
updated_pages = list(AUTOREFS._updated_pages)
AUTOREFS._updated_pages.clear()
return {
"primary": AUTOREFS._primary_url_map,
"secondary": AUTOREFS._secondary_url_map,
"inventory": AUTOREFS._abs_url_map,
"titles": AUTOREFS._title_map,
"updated_pages": updated_pages,
}
return {}
return AUTOREFS.pop_page(page_url)
return {"primary": {}, "secondary": {}, "titles": {}}
def get_autorefs_inventory_data() -> dict[str, str] | None:
"""Return global inventory URLs if Markdown rendering initialized them."""
if AUTOREFS is None:
return None
return AUTOREFS._abs_url_map
def set_autorefs_page(page: Page) -> None: