From af0ca2eeb15576af7126ec22ef2f7ac3fca90d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 31 Aug 2026 14:50:13 +0200 Subject: [PATCH] performance: faster autorefs URL membership check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Timothée Mazzucotelli --- python/tests/unit/extensions/test_autorefs.py | 40 +++++++++++++++---- python/zensical/extensions/autorefs.py | 28 ++++++------- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/python/tests/unit/extensions/test_autorefs.py b/python/tests/unit/extensions/test_autorefs.py index e8b93a0..2a07f83 100644 --- a/python/tests/unit/extensions/test_autorefs.py +++ b/python/tests/unit/extensions/test_autorefs.py @@ -117,6 +117,34 @@ class TestStore: "external": "https://example.com/external" } + def test_urls_are_ordered_and_unique(self) -> None: + """Candidate URLs preserve registration order without duplicates.""" + store = get_autorefs_store() + page = Page("page", "page.html") + + store.register_anchor(page, "identifier", "first") + store.register_anchor(page, "identifier", "second") + store.register_anchor(page, "identifier", "first") + + assert get_autorefs_page_data("page")["primary"] == { + "identifier": ["page#first", "page#second"] + } + + def test_page_registrations_remove_urls(self) -> None: + """Reprocessing a page removes its previously registered URLs.""" + store = get_autorefs_store() + first_page = Page("first", "first.html") + second_page = Page("second", "second.html") + store.register_anchor(first_page, "identifier", "anchor") + store.register_anchor(second_page, "identifier", "anchor") + + store.set_page(first_page) + + assert get_autorefs_page_data("first")["primary"] == {} + assert get_autorefs_page_data("second")["primary"] == { + "identifier": ["second#anchor"] + } + # --------------------------------------------------------------------------- # Inline processor @@ -415,8 +443,7 @@ class TestAnchorsTreeprocessor: [](){#alias10} """), ) - store = get_autorefs_store() - assert store._primary_url_map == { + assert get_autorefs_page_data("page")["primary"] == { "foo": ["page#heading-foo"], "heading-foo": ["page#heading-foo"], "bar": ["page#bar"], @@ -474,8 +501,7 @@ class TestAnchorsTreeprocessor: ## Heading baz """), ) - store = get_autorefs_store() - assert store._primary_url_map == { + assert get_autorefs_page_data("page")["primary"] == { "heading-foo": ["page#heading-foo"], "heading-bar": ["page#heading-bar"], "heading-baz": ["page#heading-baz"], @@ -510,9 +536,9 @@ class TestHeadingsTreeprocessor: def test_register_heading(self, md: Markdown) -> None: """A single heading is registered under its toc-generated slug.""" md.convert("## Foo") - store = get_autorefs_store() - assert "foo" in store._primary_url_map - assert store._primary_url_map["foo"] == ["page#foo"] + assert get_autorefs_page_data("page")["primary"]["foo"] == [ + "page#foo" + ] @pytest.mark.parametrize( "md", diff --git a/python/zensical/extensions/autorefs.py b/python/zensical/extensions/autorefs.py index 3ba0628..3414b1a 100644 --- a/python/zensical/extensions/autorefs.py +++ b/python/zensical/extensions/autorefs.py @@ -27,7 +27,7 @@ import re from abc import ABC, abstractmethod from dataclasses import dataclass from html import escape -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeAlias from xml.etree.ElementTree import Element from markdown.core import Markdown @@ -59,6 +59,11 @@ if TYPE_CHECKING: HTAGS = {"h1", "h2", "h3", "h4", "h5", "h6"} +# URL order determines which target wins when several are equally +# suitable. Dict keys give us that order plus constant-time lookups. +OrderedSet: TypeAlias = dict[str, None] +URLMap: TypeAlias = dict[str, OrderedSet] + # ---------------------------------------------------------------------------- # Globals @@ -81,8 +86,8 @@ class AutorefsStore: self.scan_toc: bool = True self.record_backlinks: bool = False - self._primary_url_map: dict[str, list[str]] = {} - self._secondary_url_map: dict[str, list[str]] = {} + self._primary_url_map: URLMap = {} + self._secondary_url_map: URLMap = {} self._abs_url_map: dict[str, str] = {} self._title_map: dict[str, str] = {} self._page_registrations: dict[str, set[tuple[bool, str, str]]] = {} @@ -118,7 +123,7 @@ class AutorefsStore: @staticmethod def _pop_urls( - url_map: dict[str, list[str]], + url_map: URLMap, registrations: set[tuple[bool, str, str]], primary: bool, ) -> dict[str, list[str]]: @@ -130,12 +135,11 @@ class AutorefsStore: result: dict[str, list[str]] = {} for identifier, selected_urls in selected.items(): - urls = url_map.get(identifier, []) + 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: + for url in selected_urls: + urls.pop(url, None) + if not urls: url_map.pop(identifier, None) return result @@ -150,11 +154,7 @@ class AutorefsStore: ) -> None: url = f"{page.url}#{anchor or identifier}" url_map = self._primary_url_map if primary else self._secondary_url_map - if identifier in url_map: - if url not in url_map[identifier]: - url_map[identifier].append(url) - else: - url_map[identifier] = [url] + url_map.setdefault(identifier, {})[url] = None self._page_registrations.setdefault(page.url, set()).add( (primary, identifier, url) )