From e7f222894c70627c70e6a14e453a10a81e3f8957 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Fri, 23 Feb 2024 16:31:27 +0100 Subject: [PATCH] feat: Support [`identifier`][] with pymdownx.inlinehilite enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue-#34: https://github.com/mkdocstrings/autorefs/issues/34 PR-#40: https://github.com/mkdocstrings/autorefs/pull/40 Co-authored-by: Timothée Mazzucotelli --- pyproject.toml | 3 +++ src/mkdocs_autorefs/references.py | 30 +++++++++++++++++++----------- tests/test_references.py | 25 ++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eec6329..d341542 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ classifiers = [ ] dependencies = [ "Markdown>=3.3", + "markupsafe>=2.0.1", "mkdocs>=1.1", ] @@ -86,6 +87,8 @@ quality = [ "ruff>=0.0", ] tests = [ + "pygments>=2.16", + "pymdown-extensions>=10.0", "pytest>=7.4", "pytest-cov>=4.1", "pytest-randomly>=3.15", diff --git a/src/mkdocs_autorefs/references.py b/src/mkdocs_autorefs/references.py index 66b4931..a4d2ab6 100644 --- a/src/mkdocs_autorefs/references.py +++ b/src/mkdocs_autorefs/references.py @@ -4,13 +4,14 @@ import re from html import escape, unescape -from typing import TYPE_CHECKING, Any, Callable, Match, Tuple +from typing import TYPE_CHECKING, Any, Callable, Match from urllib.parse import urlsplit from xml.etree.ElementTree import Element +import markupsafe from markdown.extensions import Extension from markdown.inlinepatterns import REFERENCE_RE, ReferenceInlineProcessor -from markdown.util import INLINE_PLACEHOLDER_RE +from markdown.util import HTML_PLACEHOLDER_RE, INLINE_PLACEHOLDER_RE if TYPE_CHECKING: from markdown import Markdown @@ -24,8 +25,6 @@ in the [`on_post_page` hook][mkdocs_autorefs.plugin.AutorefsPlugin.on_post_page]. """ -EvalIDType = Tuple[Any, Any, Any] - class AutoRefInlineProcessor(ReferenceInlineProcessor): """A Markdown extension.""" @@ -36,7 +35,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 # Code based on # https://github.com/Python-Markdown/markdown/blob/8e7528fa5c98bf4652deb13206d6e6241d61630b/markdown/inlinepatterns.py#L780 - def handleMatch(self, m: Match[str], data: Any) -> Element | EvalIDType: # type: ignore[override] # noqa: N802 + def handleMatch(self, m: Match[str], data: str) -> tuple[Element | None, int | None, int | None]: # type: ignore[override] # noqa: N802 """Handle an element that matched. Arguments: @@ -51,7 +50,7 @@ def handleMatch(self, m: Match[str], data: Any) -> Element | EvalIDType: # type return None, None, None identifier, end, handled = self.evalId(data, index, text) - if not handled: + if not handled or identifier is None: return None, None, None if re.search(r"[/ \x00-\x1f]", identifier): @@ -61,9 +60,9 @@ def handleMatch(self, m: Match[str], data: Any) -> Element | EvalIDType: # type # but references with Markdown formatting are not possible anyway. return None, m.start(0), end - return self.makeTag(identifier, text), m.start(0), end + return self._make_tag(identifier, text), m.start(0), end - def evalId(self, data: str, index: int, text: str) -> EvalIDType: # noqa: N802 (parent's casing) + def evalId(self, data: str, index: int, text: str) -> tuple[str | None, int, bool]: # noqa: N802 (parent's casing) """Evaluate the id portion of `[ref][id]`. If `[ref][]` use `[ref]`. @@ -86,13 +85,22 @@ def evalId(self, data: str, index: int, text: str) -> EvalIDType: # noqa: N802 # Allow the entire content to be one placeholder, with the intent of catching things like [`Foo`][]. # It doesn't catch [*Foo*][] though, just due to the priority order. # https://github.com/Python-Markdown/markdown/blob/1858c1b601ead62ed49646ae0d99298f41b1a271/markdown/inlinepatterns.py#L78 - if INLINE_PLACEHOLDER_RE.fullmatch(identifier): - identifier = self.unescape(identifier) + if match := INLINE_PLACEHOLDER_RE.fullmatch(identifier): + stashed_nodes: dict[str, Element | str] = self.md.treeprocessors["inline"].stashed_nodes # type: ignore[attr-defined] + el = stashed_nodes.get(match[1]) + if isinstance(el, Element) and el.tag == "code": + identifier = "".join(el.itertext()) + # Special case: allow pymdownx.inlinehilite raw snippets but strip them back to unhighlighted. + if match := HTML_PLACEHOLDER_RE.fullmatch(identifier): + stash_index = int(match.group(1)) + html = self.md.htmlStash.rawHtmlBlocks[stash_index] + identifier = markupsafe.Markup(html).striptags() + self.md.htmlStash.rawHtmlBlocks[stash_index] = escape(identifier) end = m.end(0) return identifier, end, True - def makeTag(self, identifier: str, text: str) -> Element: # type: ignore[override] # noqa: N802 + def _make_tag(self, identifier: str, text: str) -> Element: """Create a tag that can be matched by `AUTO_REF_RE`. Arguments: diff --git a/tests/test_references.py b/tests/test_references.py index 5a25844..02b3f50 100644 --- a/tests/test_references.py +++ b/tests/test_references.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Mapping + import markdown import pytest @@ -44,6 +46,7 @@ def run_references_test( output: str, unmapped: list[str] | None = None, from_url: str = "page.html", + extensions: Mapping = {}, ) -> None: """Help running tests about references. @@ -54,7 +57,7 @@ def run_references_test( unmapped: The expected unmapped list. from_url: The source page URL. """ - md = markdown.Markdown(extensions=[AutorefsExtension()]) + md = markdown.Markdown(extensions=[AutorefsExtension(), *extensions], extension_configs=extensions) content = md.convert(source) def url_mapper(identifier: str) -> str: @@ -92,6 +95,26 @@ def test_reference_implicit_with_code() -> None: ) +def test_reference_implicit_with_code_inlinehilite_plain() -> None: + """Check implicit references (identifier in backticks, wrapped by inlinehilite).""" + run_references_test( + extensions={"pymdownx.inlinehilite": {}}, + url_map={"pathlib.Path": "pathlib.html#Path"}, + source="This [`pathlib.Path`][].", + output='

This pathlib.Path.

', + ) + + +def test_reference_implicit_with_code_inlinehilite_python() -> None: + """Check implicit references (identifier in backticks, syntax-highlighted by inlinehilite).""" + run_references_test( + extensions={"pymdownx.inlinehilite": {"style_plain_text": "python"}, "pymdownx.highlight": {}}, + url_map={"pathlib.Path": "pathlib.html#Path"}, + source="This [`pathlib.Path`][].", + output='

This pathlib.Path.

', + ) + + def test_reference_with_punctuation() -> None: """Check references with punctuation.""" run_references_test(