performance: iterate over raw HTML blocks only once per post-processor

Signed-off-by: Timothée Mazzucotelli <dev@pawamoy.fr>
This commit is contained in:
Timothée Mazzucotelli
2026-09-01 18:56:45 +02:00
committed by squidfunk
parent af0ca2eeb1
commit f4830e0016
4 changed files with 135 additions and 14 deletions
+51
View File
@@ -30,6 +30,8 @@ import pytest
from tests.unit.extensions.conftest import soup
if TYPE_CHECKING:
from collections.abc import Iterator
from markdown import Markdown
# ---------------------------------------------------------------------------
@@ -48,6 +50,24 @@ def _glightbox(**kwargs: object) -> dict[str, Any]:
}
class _TrackingBlocks(list[str]):
"""Track indexes read from stashed blocks."""
def __init__(self) -> None:
super().__init__()
self.visited: list[int] = []
def __iter__(self) -> Iterator[str]:
for index, value in enumerate(super().__iter__()):
self.visited.append(index)
yield value
def __getitem__(self, index: Any) -> Any:
if isinstance(index, int):
self.visited.append(index)
return super().__getitem__(index)
# ---------------------------------------------------------------------------
# Basic wrapping
# ---------------------------------------------------------------------------
@@ -388,3 +408,34 @@ class TestPostprocessor:
def test_raw_html_skip_class_not_wrapped(self, md: Markdown) -> None:
html = soup(md.convert('<img src="raw.png" class="off-glb" />'))
assert html.select_one("a.glightbox") is None
@pytest.mark.parametrize(
"md",
[pytest.param(_glightbox(), id="default")],
indirect=["md"],
)
def test_toc_does_not_rescan_stash(
self, md: Markdown, monkeypatch: pytest.MonkeyPatch
) -> None:
"""TOC rendering does not cause processed blocks to be scanned again."""
blocks = _TrackingBlocks()
md.htmlStash.rawHtmlBlocks = blocks
processor = md.postprocessors["glightbox"]
original = processor.run
visits: list[list[int]] = []
def run(text: str) -> str:
start = len(blocks.visited)
text = original(text)
visits.append(blocks.visited[start:])
return text
monkeypatch.setattr(processor, "run", run)
html = soup(
md.convert('# One\n\n## Two\n\n## Three\n\n<img src="raw.png" />')
)
assert len(visits) > 1
assert visits[0] == list(range(len(blocks)))
assert all(not visited for visited in visits[1:])
assert len(html.select("a.glightbox")) == 1
+58
View File
@@ -23,16 +23,56 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from markdown import Markdown
from zensical.extensions.links import (
LinksExtension,
LinksPostprocessor,
_is_relative,
_md_path_to_html,
_rewrite_url,
)
if TYPE_CHECKING:
from collections.abc import Iterator
from typing import Any
class _TrackingBlocks(list[str]):
"""Track indexes read from stashed blocks."""
def __init__(self, values: list[str]) -> None:
super().__init__(values)
self.visited: list[int] = []
def __iter__(self) -> Iterator[str]:
for index, value in enumerate(super().__iter__()):
self.visited.append(index)
yield value
def __getitem__(self, index: Any) -> Any:
if isinstance(index, int):
self.visited.append(index)
return super().__getitem__(index)
class _TrackingPostprocessor(LinksPostprocessor):
"""Record which stash indexes each invocation reads."""
def __init__(self, md: Markdown, blocks: _TrackingBlocks) -> None:
super().__init__(md, "guide/page.md", True)
self._blocks = blocks
self.visits: list[list[int]] = []
def run(self, text: str) -> str:
start = len(self._blocks.visited)
text = super().run(text)
self.visits.append(self._blocks.visited[start:])
return text
@pytest.mark.parametrize(
("path", "directory_urls", "expected"),
@@ -109,3 +149,21 @@ def test_rewrites_links_in_stashed_raw_html() -> None:
assert md.convert('<div><a href="guide.md">Guide</a></div>') == (
'<div><a href="guide/">Guide</a></div>'
)
def test_postprocessor_does_not_rescan_stash_for_toc() -> None:
"""TOC rendering does not cause processed blocks to be scanned again."""
md = Markdown(extensions=["toc"])
blocks = _TrackingBlocks([])
md.htmlStash.rawHtmlBlocks = blocks
processor = _TrackingPostprocessor(md, blocks)
md.postprocessors.register(processor, processor.name, 29)
html = md.convert(
'# One\n\n## Two\n\n## Three\n\n<a href="other.md">other</a>'
)
assert len(processor.visits) > 1
assert processor.visits[0] == list(range(len(blocks)))
assert all(not visited for visited in processor.visits[1:])
assert '<a href="../other/">other</a>' in html