Skip to content

Commit

Permalink
feat: Support [identifier][] with pymdownx.inlinehilite enabled
Browse files Browse the repository at this point in the history
Issue-#34: #34
PR-#40: #40
Co-authored-by: Timothée Mazzucotelli <dev@pawamoy.fr>
  • Loading branch information
oprypin and pawamoy authored Feb 23, 2024
1 parent 143d768 commit e7f2228
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 12 deletions.
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

0 comments on commit e7f2228

Please sign in to comment.