Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support [identifier][] with pymdownx.inlinehilite enabled #40

Merged
merged 3 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ classifiers = [
]
dependencies = [
"Markdown>=3.3",
"markupsafe>=2.0.1",
"mkdocs>=1.1",
]

Expand Down Expand Up @@ -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",
Expand Down
30 changes: 19 additions & 11 deletions src/mkdocs_autorefs/references.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""
Expand All @@ -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:
Expand All @@ -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):
Expand All @@ -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]`.
Expand All @@ -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 <code> 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:
Expand Down
25 changes: 24 additions & 1 deletion tests/test_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from typing import Mapping

import markdown
import pytest

Expand Down Expand Up @@ -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.

Expand All @@ -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:
Expand Down Expand Up @@ -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='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code>pathlib.Path</code></a>.</p>',
)


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='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code class="highlight">pathlib.Path</code></a>.</p>',
)


def test_reference_with_punctuation() -> None:
"""Check references with punctuation."""
run_references_test(
Expand Down