fix: brackets in indented code blocks recognized as unresolved link references (#700)

Signed-off-by: squidfunk <martin.donath@squidfunk.com>
This commit is contained in:
squidfunk
2026-06-04 18:59:54 +02:00
parent 93cc869978
commit c40f649f7b
2 changed files with 83 additions and 5 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"
@@ -1741,6 +1737,33 @@ class TestSnippets:
# ---------------------------------------------------------------------------
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 TestInlineCode:
"""Tests for inline code."""
@@ -169,6 +169,13 @@ 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)
@@ -394,6 +401,37 @@ 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:
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:
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)
@@ -1579,6 +1617,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)