mirror of
https://github.com/zensical/zensical.git
synced 2026-08-27 17:46:32 +00:00
fix: ignore [TOC] marker during link validation (#686)
Signed-off-by: squidfunk <martin.donath@squidfunk.com>
This commit is contained in:
@@ -449,6 +449,44 @@ class TestLinkReferences:
|
||||
assert text(md, links[0].text) == b"id"
|
||||
assert text(md, links[0].href) == b"id"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"md",
|
||||
[
|
||||
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:
|
||||
refs = collect(md)
|
||||
assert len(refs) == 1
|
||||
|
||||
link_refs = link_refs_only(refs)
|
||||
assert len(link_refs) == 1
|
||||
assert text(md, link_refs[0].text) == b"TOC"
|
||||
assert text(md, link_refs[0].id) == b"TOC"
|
||||
|
||||
def test_link_ref_collapsed_toc_id(self) -> None:
|
||||
md = b"[TOC][]"
|
||||
refs = collect(md)
|
||||
assert len(refs) == 1
|
||||
|
||||
link_refs = link_refs_only(refs)
|
||||
assert len(link_refs) == 1
|
||||
assert text(md, link_refs[0].text) == b"TOC"
|
||||
assert text(md, link_refs[0].id) == b"TOC"
|
||||
|
||||
def test_link_ref_explicit_toc_id(self) -> None:
|
||||
md = b"[TOC][id]"
|
||||
refs = collect(md)
|
||||
assert len(refs) == 1
|
||||
|
||||
link_refs = link_refs_only(refs)
|
||||
assert len(link_refs) == 1
|
||||
assert text(md, link_refs[0].text) == b"TOC"
|
||||
assert text(md, link_refs[0].id) == b"id"
|
||||
|
||||
# --- negative cases ---
|
||||
|
||||
def test_no_link_ref_escaped_brackets(self) -> None:
|
||||
@@ -476,6 +514,21 @@ class TestLinkReferences:
|
||||
refs = collect(md)
|
||||
assert len(refs) == 0
|
||||
|
||||
def test_no_link_ref_toc_marker(self) -> None:
|
||||
md = b"[TOC]"
|
||||
refs = collect(md)
|
||||
assert len(refs) == 0
|
||||
|
||||
def test_no_link_ref_toc_marker_with_shift(self) -> None:
|
||||
md = b"[TOC]"
|
||||
refs = collect(md, shift=10)
|
||||
assert len(refs) == 0
|
||||
|
||||
def test_no_link_ref_toc_marker_block(self) -> None:
|
||||
md = b"before\n\n[TOC]\n\nafter"
|
||||
refs = collect(md)
|
||||
assert len(refs) == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -599,9 +652,7 @@ class TestLinkDefinitions:
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_link_def_angle_brackets_with_link_after(
|
||||
self, md: bytes
|
||||
) -> None:
|
||||
def test_link_def_angle_brackets_with_link_after(self, md: bytes) -> None:
|
||||
refs = collect(md)
|
||||
assert len(refs) == 2
|
||||
|
||||
|
||||
@@ -422,12 +422,17 @@ def _scan_link_or_link_ref(cursor: Cursor) -> Link | LinkReference | None:
|
||||
)
|
||||
|
||||
# Consume link id
|
||||
after_text = end
|
||||
id, end = _scan_link_id(cursor, end)
|
||||
|
||||
# Ignore empty shortcut references like `[]` or `[][]`.
|
||||
if id is None and text.start == text.end:
|
||||
return None
|
||||
|
||||
# Ignore Python Markdown's table-of-contents marker.
|
||||
if id is None and _is_toc_marker(cursor, text, after_text):
|
||||
return None
|
||||
|
||||
# Advance cursor and return link reference
|
||||
cursor.advance(end - start)
|
||||
return LinkReference(
|
||||
@@ -693,6 +698,32 @@ def _scan_link_id_identifier(
|
||||
return None
|
||||
|
||||
|
||||
def _is_toc_marker(cursor: Cursor, text: Span, end: int) -> bool:
|
||||
"""Return whether a shortcut reference is a TOC marker block."""
|
||||
start = text.start - cursor.shift - 1
|
||||
if cursor.data[start + 1 : end - 1] != b"TOC":
|
||||
return False
|
||||
|
||||
# Python Markdown treats the marker as a block, not inline text. A block can
|
||||
# be indented up to three spaces before it becomes a code block.
|
||||
if not cursor.at_line_start() or cursor.col > 3: # noqa: PLR2004
|
||||
return False
|
||||
|
||||
# The marker must be on a line by itself
|
||||
line = _find_line_start(cursor, start)
|
||||
if not _is_previous_line_blank(cursor, line):
|
||||
return False
|
||||
|
||||
# Skip whitespace after the marker and ensure there's nothing else
|
||||
pos = _skip_whitespace(cursor, end)
|
||||
if pos < cursor.end and cursor.data[pos] not in (_CR, _NL):
|
||||
return False
|
||||
|
||||
# The next line must be blank or non-existent
|
||||
pos = _skip_line(cursor, pos)
|
||||
return pos >= cursor.end or _is_blank_line(cursor, pos)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1492,6 +1523,27 @@ def _skip_line(cursor: Cursor, pos: int) -> int:
|
||||
return pos
|
||||
|
||||
|
||||
def _is_blank_line(cursor: Cursor, pos: int) -> bool:
|
||||
"""Return whether the line at the given position is blank."""
|
||||
end = _find_line_end(cursor, pos)
|
||||
return all(char in _WHITESPACE for char in cursor.data[pos:end])
|
||||
|
||||
|
||||
def _is_previous_line_blank(cursor: Cursor, pos: int) -> bool:
|
||||
"""Return whether the line before the given position is blank."""
|
||||
if pos == 0:
|
||||
return True
|
||||
|
||||
# Find the end of the previous line, skipping any trailing newlines
|
||||
end = pos - 1
|
||||
if end > 0 and cursor.data[end - 1] == _CR:
|
||||
end -= 1
|
||||
|
||||
# Find the start of the previous line and check if it's blank
|
||||
start = _find_line_start(cursor, end)
|
||||
return all(char in _WHITESPACE for char in cursor.data[start:end])
|
||||
|
||||
|
||||
def _find_bracket(cursor: Cursor, pos: int) -> int:
|
||||
"""Find the next `]` or newline."""
|
||||
while (
|
||||
|
||||
Reference in New Issue
Block a user