mirror of
https://github.com/zensical/zensical.git
synced 2026-09-24 07:15:34 +00:00
refactor: migrate autorefs MkDocs plugin replacement
Signed-off-by: squidfunk <martin.donath@squidfunk.com>
This commit is contained in:
+26
@@ -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
@@ -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
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user