Merge pull request #731 from zensical/fix/validation

fix: snippet markers recognized as unresolved link references (#700)
This commit is contained in:
Martin Donath
2026-06-04 19:18:05 +02:00
committed by GitHub
3 changed files with 217 additions and 9 deletions
@@ -455,7 +455,6 @@ class TestLinkReferences:
pytest.param(b"text [TOC]", id="inline"),
pytest.param(b"[TOC] text", id="inline-prefix"),
pytest.param(b"[TOC]\ntext", id="paragraph"),
pytest.param(b" [TOC]", id="code-block"),
],
)
def test_link_ref_toc_text(self, md: bytes) -> None:
@@ -737,10 +736,7 @@ class TestLinkDefinitions:
def test_no_link_def_indent(self) -> None:
md = b" [id]: href"
refs = collect(md)
assert len(refs) == 1
link_refs = link_refs_only(refs)
assert len(link_refs) == 1
assert len(refs) == 0
def test_no_link_def_prefix(self) -> None:
md = b"text [id]: href"
@@ -1710,6 +1706,88 @@ class TestFencedCodeBlocks:
# ---------------------------------------------------------------------------
class TestSnippets:
"""Tests for pymdownx.snippets markers."""
def test_section_markers_are_ignored(self) -> None:
md = (
b"# --8<-- [start:reference_section]\n"
b"Text\n"
b"# --8<-- [end:reference_section]\n"
)
refs = collect(md)
assert len(refs) == 0
def test_section_markers_do_not_hide_following_links(self) -> None:
md = (
b"# --8<-- [start:reference_section]\n"
b"Text\n"
b"# --8<-- [end:reference_section]\n"
b"[after](href)\n"
)
refs = collect(md)
assert len(refs) == 1
links = links_only(refs)
assert len(links) == 1
assert text(md, links[0].text) == b"after"
assert text(md, links[0].href) == b"href"
# ---------------------------------------------------------------------------
class TestIndentedCodeBlocks:
"""Tests for indented code blocks."""
def test_indented_code_is_ignored(self) -> None:
md = b"\n [Start][]\n"
refs = collect(md)
assert len(refs) == 0
def test_indented_code_with_blank_line_continuation_is_ignored(
self,
) -> None:
md = b"\n [Start][]\n\n [End][]\n"
refs = collect(md)
assert len(refs) == 0
def test_indented_code_does_not_hide_following_link(self) -> None:
md = b"\n [Start][]\n\n[after](href)\n"
refs = collect(md)
assert len(refs) == 1
links = links_only(refs)
assert len(links) == 1
assert text(md, links[0].text) == b"after"
assert text(md, links[0].href) == b"href"
# ---------------------------------------------------------------------------
class TestMarkdownComments:
"""Tests for Markdown comments written as link-definition hacks."""
def test_markdown_comment_is_ignored(self) -> None:
md = b"[//]: # (comment)\n"
refs = collect(md)
assert len(refs) == 0
def test_markdown_comment_does_not_hide_following_link(self) -> None:
md = b"[//]: # (comment)\n[after](href)\n"
refs = collect(md)
assert len(refs) == 1
links = links_only(refs)
assert len(links) == 1
assert text(md, links[0].text) == b"after"
assert text(md, links[0].href) == b"href"
# ---------------------------------------------------------------------------
class TestInlineCode:
"""Tests for inline code."""
@@ -169,6 +169,27 @@ def _scan(cursor: Cursor) -> Iterator[Reference]:
while cursor.pos < cursor.end:
char = cursor.data[cursor.pos]
# Top-level indented code block.
if cursor.at_line_start():
end = _scan_indented_code(cursor)
if end is not None:
cursor.advance(end - cursor.pos)
continue
# Snippet directives and section markers: --8<-- ...
if cursor.at_line_start():
end = _scan_snippet(cursor)
if end is not None:
cursor.advance(end - cursor.pos)
continue
# Markdown comment hack: [//]: ...
if cursor.at_line_start():
end = _scan_markdown_comment(cursor)
if end is not None:
cursor.advance(end - cursor.pos)
continue
# Escaped character or math
if char == _BACKSLASH:
next = cursor.peek(1)
@@ -387,6 +408,98 @@ def _scan(cursor: Cursor) -> Iterator[Reference]:
# ---------------------------------------------------------------------------
def _scan_indented_code(cursor: Cursor) -> int | None:
"""Scan for a top-level indented code block."""
line = _find_line_start(cursor, cursor.pos)
if not _is_previous_line_blank(cursor, line):
return None
pos = line
indent, end = _measure_indent(cursor, pos)
if indent < 4: # noqa: PLR2004
return None
if end >= cursor.end or cursor.data[end] in (_CR, _NL):
return None
# Consume the whole chunk: indented lines plus blank continuation lines.
pos = _skip_line(cursor, end)
while pos < cursor.end:
if _is_blank_line(cursor, pos):
pos = _skip_line(cursor, pos)
continue
indent, end = _measure_indent(cursor, pos)
if indent < 4: # noqa: PLR2004
break
pos = _skip_line(cursor, end)
return pos
# ---------------------------------------------------------------------------
def _scan_snippet(cursor: Cursor) -> int | None:
"""Scan for a pymdownx.snippets directive or section marker line."""
pos = _skip_whitespace(cursor, cursor.pos)
# Markdown snippet sections are commonly wrapped in heading comments.
if pos < cursor.end and cursor.data[pos] == _HASH:
pos = _skip_whitespace(cursor, pos + 1)
# Match the scissors marker: -+8<-+
start = pos
while pos < cursor.end and cursor.data[pos] == _DASH:
pos += 1
if pos == start or pos >= cursor.end or cursor.data[pos] != ord(b"8"):
return None
pos += 1
start = pos
while pos < cursor.end and cursor.data[pos] == _LANGLE:
pos += 1
if pos == start:
return None
start = pos
while pos < cursor.end and cursor.data[pos] == _DASH:
pos += 1
if pos == start:
return None
# Snippet syntax ends at the line boundary.
if pos < cursor.end and cursor.data[pos] not in (_SPACE, _TAB, _CR, _NL):
return None
return _skip_line(cursor, pos)
# ---------------------------------------------------------------------------
def _scan_markdown_comment(cursor: Cursor) -> int | None:
"""Scan for a Markdown comment line written as `[//]: ...`."""
start = cursor.pos
# Skip if there're more than four spaces of indentation
if cursor.col > 3: # noqa: PLR2004
return None
id, end = _scan_link_id(cursor, start)
if (
id is None
or cursor.data[id.start - cursor.shift : id.end - cursor.shift] != b"//"
):
return None
if end >= cursor.end or cursor.data[end] != _COLON:
return None
return _skip_line(cursor, end)
# ---------------------------------------------------------------------------
def _scan_link_or_link_ref(cursor: Cursor) -> Link | LinkReference | None:
"""Scan for link or link reference.
@@ -1534,6 +1647,23 @@ def _skip_whitespace(cursor: Cursor, pos: int) -> int:
return pos
def _measure_indent(cursor: Cursor, pos: int) -> tuple[int, int]:
"""Return visual indentation width and first non-whitespace position."""
indent = 0
while pos < cursor.end:
char = cursor.data[pos]
if char == _SPACE:
indent += 1
pos += 1
continue
if char == _TAB:
indent += 4 - (indent % 4)
pos += 1
continue
break
return indent, pos
def _skip_whitespace_newline(cursor: Cursor, pos: int) -> int:
"""Skip trailing horizontal whitespace and at most one newline."""
pos = _skip_whitespace(cursor, pos)
Generated
+4 -4
View File
@@ -636,15 +636,15 @@ wheels = [
[[package]]
name = "pymdown-extensions"
version = "10.21.2"
version = "10.21.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" },
{ url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" },
]
[[package]]
@@ -952,7 +952,7 @@ requires-dist = [
{ name = "jinja2", specifier = ">=3.1" },
{ name = "markdown", specifier = ">=3.7" },
{ name = "pygments", specifier = ">=2.20" },
{ name = "pymdown-extensions", specifier = ">=10.21.2" },
{ name = "pymdown-extensions", specifier = ">=10.21.3" },
{ name = "pyyaml", specifier = ">=6.0.2" },
{ name = "tomli", specifier = ">=2.4.0" },
]