# Copyright (c) 2025-2026 Zensical and contributors # SPDX-License-Identifier: MIT # All contributions are certified under the DCO # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. from __future__ import annotations from typing import TYPE_CHECKING import pytest from zensical.collectors import references from zensical.collectors.references import ( FootnoteDefinition, FootnoteReference, Link, LinkDefinition, LinkReference, Reference, ) if TYPE_CHECKING: from zensical.utilities.span import Span # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestLinks: """Tests for links.""" @pytest.mark.parametrize( ("md", "expected_text", "expected_href"), [ pytest.param( b"[text](href)", b"text", b"href", id="link", ), pytest.param( b'[text](href "Title")', b"text", b"href", id="link-with-title", ), pytest.param( b'[text](href more"Title")', b"text", b"href more", id="link-with-title, whitespace", ), pytest.param( b'[text](href more "Title")', b"text", b"href more", id="link-with-title, whitespace, trailing", ), pytest.param( b"[](href)", b"", b"href", id="link-with-empty-text", ), pytest.param( b"[text]()", b"text", b"", id="link-with-empty-href", ), pytest.param( b"[]()", b"", b"", id="link-with-empty-text-and-href", ), pytest.param( b"[text \\]](href)", b"text \\]", b"href", id="link-with-escaped-brackets-in-text", ), pytest.param( b"[text]((href))", b"text", b"(href)", id="link-with-parens-in-href", ), pytest.param( b"[text](href 'Title')", b"text", b"href", id="link-with-title, single quotes", ), pytest.param( b"[text](href more'Title')", b"text", b"href more", id="link-with-title, single quotes, whitespace", ), pytest.param( b"[text](href more 'Title')", b"text", b"href more", id="link-with-title, single quotes, whitespace, trailing", ), ], ) def test_link( self, md: bytes, expected_text: bytes, expected_href: bytes ) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == expected_text assert text(md, links[0].href) == expected_href @pytest.mark.parametrize( ("md", "expected_text", "expected_href"), [ pytest.param( b"[text](href){}", b"text", b"href", id="link-with-attr", ), pytest.param( b"[text](href){ .class }", b"text", b"href", id="link-with-class-attr", ), pytest.param( b"[text](href){ #id }", b"text", b"href", id="link-with-id-attr", ), pytest.param( b'[text](href){ target="_blank" }', b"text", b"href", id="link-with-target-attr", ), pytest.param( b'[text](href){ .class #id target="_blank" }', b"text", b"href", id="link-with-multiple-attrs", ), pytest.param( b'[text](href) { target="_blank" }', b"text", b"href", id="link-with-spaced-attr, invalid", ), ], ) def test_link_with_attr_list( self, md: bytes, expected_text: bytes, expected_href: bytes ) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == expected_text assert text(md, links[0].href) == expected_href @pytest.mark.parametrize( ("md", "expected_href"), [ pytest.param( b"[text]()", b"href", id="link-with-angle-brackets", ), pytest.param( b'[text]( "title")', b"href", id="link-with-angle-brackets-and-title", ), pytest.param( b'[text]( more"Title")', b" more", id="link-with-angle-brackets-and-title, whitespace", ), pytest.param( b'[text]( more "Title")', b" more", id="link-with-angle-brackets-and-title, whitespace, trailing", ), pytest.param( b"[text](<>)", b"", id="link-with-empty-angle-brackets", ), pytest.param( b"[text]( None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].href) == expected_href def test_link_text_with_newline(self) -> None: md = b"[text\nmore text](href)" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == b"text\nmore text" assert text(md, links[0].href) == b"href" def test_link_href_with_newline(self) -> None: md = b"[text](hr\nef)" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == b"text" assert text(md, links[0].href) == b"hr\nef" # --- negative cases --- def test_no_link_escaped_brackets(self) -> None: md = b"\\[text\\](href)" refs = collect(md) assert len(refs) == 0 def test_no_link_escaped_opening_bracket(self) -> None: md = b"\\[text](href)" refs = collect(md) assert len(refs) == 0 def test_no_link_escaped_closing_bracket(self) -> None: md = b"[text\\](href)" refs = collect(md) assert len(refs) == 0 # --------------------------------------------------------------------------- class TestLinkReferences: """Tests for link references.""" @pytest.mark.parametrize( ("md", "expected_text", "expected_id"), [ pytest.param( b"[text][id]", b"text", b"id", id="link-ref", ), pytest.param( b"[text][]", b"text", b"text", id="link-ref, collapsed", ), pytest.param( b"[text]", b"text", b"text", id="link-ref, shortcut", ), pytest.param( b"[][id]", b"", b"id", id="link-ref-with-empty-text", ), pytest.param( b"[text \\]][id]", b"text \\]", b"id", id="link-ref-with-escaped-brackets-in-text", ), ], ) def test_link_ref( self, md: bytes, expected_text: bytes, expected_id: bytes ) -> None: refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == expected_text assert text(md, link_refs[0].id) == expected_id @pytest.mark.parametrize( ("md", "expected_text", "expected_id"), [ pytest.param( b"[text][id]{}", b"text", b"id", id="link-ref-with-attr", ), pytest.param( b"[text][]{}", b"text", b"text", id="link-ref-with-attr, collapsed", ), pytest.param( b"[text]{}", b"text", b"text", id="link-ref-with-attr, shortcut", ), pytest.param( b"[text][id]{ .class }", b"text", b"id", id="link-ref-with-class-attr", ), pytest.param( b"[text][id]{ #id }", b"text", b"id", id="link-ref-with-id-attr", ), pytest.param( b'[text][id]{ target="_blank" }', b"text", b"id", id="link-ref-with-target-attr", ), pytest.param( b'[text][id]{ .class #id target="_blank" }', b"text", b"id", id="link-ref-with-multiple-attrs", ), pytest.param( b'[text][id] { target="_blank" }', b"text", b"id", id="link-ref-with-spaced-attr, invalid", ), ], ) def test_link_ref_with_attr_list( self, md: bytes, expected_text: bytes, expected_id: bytes ) -> None: refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == expected_text assert text(md, link_refs[0].id) == expected_id def test_link_ref_with_space(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 link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"id" assert text(md, link_refs[0].id) == b"id" def test_link_ref_with_footnote(self) -> None: md = b"[id][^note]" refs = collect(md) assert len(refs) == 2 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"id" assert text(md, link_refs[0].id) == b"id" note_refs = footnote_refs_only(refs) assert len(note_refs) == 1 assert text(md, note_refs[0].id) == b"note" def test_link_ref_text_with_newline(self) -> None: md = b"[text\nmore text][id]" refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"text\nmore text" assert text(md, link_refs[0].id) == b"id" def test_link_ref_text_brackets_in_id(self) -> None: md = b"[text][[id]]" refs = collect(md) assert len(refs) == 2 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"text" assert text(md, link_refs[0].id) == b"text" links = links_only(refs) assert len(links) == 1 assert links[0].kind == "wikilink" 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" @pytest.mark.parametrize( ("md", "expected_text"), [ pytest.param(b"[!NOTE]", b"!NOTE", id="not-blockquote"), pytest.param(b"> text [!NOTE]", b"!NOTE", id="inline"), pytest.param(b"> [!NOTE] text", b"!NOTE", id="trailing-text"), pytest.param(b"> [!note]", b"!note", id="lowercase"), ], ) def test_link_ref_callout_text( self, md: bytes, expected_text: 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) == expected_text @pytest.mark.parametrize( ("md", "expected_id"), [ pytest.param(b"> [!NOTE][]", b"!NOTE", id="collapsed"), pytest.param(b"> [!NOTE][id]", b"id", id="explicit"), ], ) def test_link_ref_callout_id(self, md: bytes, expected_id: 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"!NOTE" assert text(md, link_refs[0].id) == expected_id # --- negative cases --- def test_no_link_ref_escaped_brackets(self) -> None: md = b"\\[text\\]" refs = collect(md) assert len(refs) == 0 def test_no_link_ref_escaped_opening_bracket(self) -> None: md = b"\\[text]" refs = collect(md) assert len(refs) == 0 def test_no_link_ref_escaped_closing_bracket(self) -> None: md = b"[text\\]" refs = collect(md) assert len(refs) == 0 def test_no_link_ref_empty_shortcut(self) -> None: md = b"[]" refs = collect(md) assert len(refs) == 0 def test_no_link_ref_empty_brackets(self) -> None: md = b"[][]" 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 @pytest.mark.parametrize( "md", [ pytest.param(b"> [!NOTE]", id="note"), pytest.param(b"> [!TIP]", id="tip"), pytest.param(b"> [!IMPORTANT]", id="important"), pytest.param(b"> [!WARNING]", id="warning"), pytest.param(b"> [!CAUTION]", id="caution"), pytest.param( b"> [!NOTE]\n> This is a **note** admonition.", id="note-body", ), pytest.param( b"> [!WARNING]\n> This is a **warning** admonition.", id="warning-body", ), pytest.param(b">>[!NOTE]", id="nested-compact"), pytest.param(b"> > [!NOTE]", id="nested-spaced"), ], ) def test_no_link_ref_callout_marker(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 0 # --------------------------------------------------------------------------- class TestLinkDefinitions: """Tests for link definitions.""" @pytest.mark.parametrize( ("md", "expected_id", "expected_href"), [ pytest.param( b"[id]: href", b"id", b"href", id="link-def", ), pytest.param( b"[multi word id]: href", b"multi word id", b"href", id="link-def, multi-word", ), pytest.param( b'[id]: href "Title"', b"id", b"href", id="link-def-with-title", ), pytest.param( b"[id]: href 'Title'", b"id", b"href", id="link-def-with-title, single quotes", ), pytest.param( b" [id]: href\n", b"id", b"href", id="link-def-with-indent", ), ], ) def test_link_def( self, md: bytes, expected_id: bytes, expected_href: bytes ) -> None: refs = collect(md) assert len(refs) == 1 link_defs = link_defs_only(refs) assert len(link_defs) == 1 assert text(md, link_defs[0].id) == expected_id assert text(md, link_defs[0].href) == expected_href @pytest.mark.parametrize( ("md", "expected_id", "expected_href"), [ pytest.param( b"[id]: \nid", b"id", b"href", id="link-def", ), pytest.param( b"[multi word id]: ", b"multi word id", b"href", id="link-def, multi-word", ), pytest.param( b'[id]: "Title"', b"id", b"href", id="link-def-with-title", ), pytest.param( b" [id]: \n", b"id", b"href", id="link-def-with-indent", ), ], ) def test_link_def_with_angle_brackets( self, md: bytes, expected_id: bytes, expected_href: bytes ) -> None: refs = collect(md) assert len(refs) == 1 link_defs = link_defs_only(refs) assert len(link_defs) == 1 assert text(md, link_defs[0].id) == expected_id assert text(md, link_defs[0].href) == expected_href @pytest.mark.parametrize( "md", [ pytest.param(b'[id]: href\n "Title"', id="lf"), pytest.param(b'[id]: href\r\n "Title"', id="crlf"), pytest.param(b'[id]: href\r "Title"', id="cr"), ], ) def test_link_def_title_on_next_line(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 1 link_defs = link_defs_only(refs) assert len(link_defs) == 1 assert text(md, link_defs[0].id) == b"id" assert text(md, link_defs[0].href) == b"href" @pytest.mark.parametrize( "md", [ pytest.param( b"[id]: \r\n[after](href)", id="crlf", ), pytest.param( b"[id]: \r[after](href)", id="cr", ), ], ) def test_link_def_angle_brackets_with_link_after(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 2 link_defs = link_defs_only(refs) assert len(link_defs) == 1 assert text(md, link_defs[0].id) == b"id" assert text(md, link_defs[0].href) == b"href" links = links_only(refs) assert len(links) == 1 assert text(md, links[0].text) == b"after" # --- negative cases --- def test_no_link_def_empty_href(self) -> None: md = b"[id]:" refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 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 def test_no_link_def_prefix(self) -> None: md = b"text [id]: href" refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 # --------------------------------------------------------------------------- class TestImages: """Tests for images.""" @pytest.mark.parametrize( ("md", "expected_alt", "expected_href"), [ pytest.param( b"![alt](image.png)", b"alt", b"image.png", id="image", ), pytest.param( b'![alt](image.png "Title")', b"alt", b"image.png", id="image-with-title", ), pytest.param( b"![](image.png)", b"", b"image.png", id="image-with-empty-alt", ), pytest.param( b"![alt]()", b"alt", b"", id="image-with-empty-href", ), pytest.param( b"![]()", b"", b"", id="image-with-empty-alt-and-href", ), pytest.param( b"![alt [[]]](image.png)", b"alt [[]]", b"image.png", id="image-with-brackets-in-alt", ), pytest.param( b"![alt \\]](image.png)", b"alt \\]", b"image.png", id="image-with-escaped-brackets-in-alt", ), pytest.param( b"![alt]((href))", b"alt", b"(href)", id="image-with-parens-in-href", ), ], ) def test_image( self, md: bytes, expected_alt: bytes, expected_href: bytes ) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "image" assert text(md, links[0].text) == expected_alt assert text(md, links[0].href) == expected_href @pytest.mark.parametrize( ("md", "expected_text", "expected_href"), [ pytest.param( b"![alt](image.png){}", b"alt", b"image.png", id="image-with-attr", ), pytest.param( b"![alt](image.png){ .class }", b"alt", b"image.png", id="image-with-class-attr", ), pytest.param( b"![alt](image.png){ #id }", b"alt", b"image.png", id="image-with-id-attr", ), pytest.param( b'![alt](image.png){ loading="lazy" }', b"alt", b"image.png", id="image-with-loading-attr", ), pytest.param( b'![alt](image.png){ .class #id loading="lazy" }', b"alt", b"image.png", id="image-with-multiple-attrs", ), pytest.param( b'![alt](image.png) { loading="lazy" }', b"alt", b"image.png", id="image-with-spaced-attr, invalid", ), ], ) def test_image_with_attr_list( self, md: bytes, expected_text: bytes, expected_href: bytes ) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "image" assert text(md, links[0].text) == expected_text assert text(md, links[0].href) == expected_href def test_image_with_angle_brackets(self) -> None: md = b"![alt]()" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "image" assert text(md, links[0].text) == b"alt" assert text(md, links[0].href) == b"image.png" def test_image_ref_with_space(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 link_refs[0].kind == "image" assert text(md, link_refs[0].text) == b"id" assert text(md, link_refs[0].id) == b"id" def test_image_text_with_newline(self) -> None: md = b"![alt\nmore text](href)" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "image" assert text(md, links[0].text) == b"alt\nmore text" assert text(md, links[0].href) == b"href" # --- negative cases --- def test_no_image_escaped_bang(self) -> None: md = b"\\![alt](image.png)" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == b"alt" assert text(md, links[0].href) == b"image.png" def test_no_image_spaced_bang(self) -> None: md = b"! [alt](image.png)" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == b"alt" assert text(md, links[0].href) == b"image.png" def test_no_image_only_bang(self) -> None: md = b"!image.png" refs = collect(md) assert len(refs) == 0 def test_no_image_escaped_brackets(self) -> None: md = b"!\\[alt\\](image.png)" refs = collect(md) assert len(refs) == 0 def test_no_image_escaped_opening_bracket(self) -> None: md = b"!\\[alt](image.png)" refs = collect(md) assert len(refs) == 0 def test_no_image_escaped_closing_bracket(self) -> None: md = b"![alt\\](image.png)" refs = collect(md) assert len(refs) == 0 # --------------------------------------------------------------------------- class TestImageReferences: """Tests for image references.""" @pytest.mark.parametrize( ("md", "expected_alt", "expected_id"), [ pytest.param( b"![alt][id]", b"alt", b"id", id="image-ref", ), pytest.param( b"![alt][]", b"alt", b"alt", id="image-ref, collapsed", ), pytest.param( b"![id]", b"id", b"id", id="image-ref, shortcut", ), pytest.param( b"![][id]", b"", b"id", id="image-ref-with-empty-alt", ), pytest.param( b"![][]", b"", b"", id="image-ref-with-empty-alt-and-id", ), pytest.param( b"![alt [[]]][id]", b"alt [[]]", b"id", id="image-ref-with-brackets-in-text", ), pytest.param( b"![alt \\]][id]", b"alt \\]", b"id", id="image-ref-with-escaped-brackets-in-text", ), ], ) def test_image_ref( self, md: bytes, expected_alt: bytes, expected_id: bytes ) -> None: refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "image" assert text(md, link_refs[0].text) == expected_alt assert text(md, link_refs[0].id) == expected_id @pytest.mark.parametrize( ("md", "expected_text", "expected_id"), [ pytest.param( b"![alt][image-id]{}", b"alt", b"image-id", id="image-ref-with-attr", ), pytest.param( b"![alt][]{}", b"alt", b"alt", id="image-ref-with-attr, collapsed", ), pytest.param( b"![alt]{}", b"alt", b"alt", id="image-ref-with-attr, shortcut", ), pytest.param( b"![alt][id]{ .class }", b"alt", b"id", id="image-ref-with-class-attr", ), pytest.param( b"![alt][id]{ #id }", b"alt", b"id", id="image-ref-with-id-attr", ), pytest.param( b'![alt][id]{ loading="lazy" }', b"alt", b"id", id="image-ref-with-loading-attr", ), pytest.param( b'![alt][id]{ .class #id loading="lazy" }', b"alt", b"id", id="image-ref-with-multiple-attrs", ), pytest.param( b'![alt][id] { loading="lazy" }', b"alt", b"id", id="image-ref-with-spaced-attr, invalid", ), ], ) def test_image_ref_with_attr_list( self, md: bytes, expected_text: bytes, expected_id: bytes ) -> None: refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "image" assert text(md, link_refs[0].text) == expected_text assert text(md, link_refs[0].id) == expected_id def test_image_ref_text_with_newline(self) -> None: md = b"![alt\nmore text][id]" refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "image" assert text(md, link_refs[0].text) == b"alt\nmore text" assert text(md, link_refs[0].id) == b"id" # --- negative cases --- def test_no_image_ref_escaped_bang(self) -> None: md = b"\\![alt][id]" refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"alt" assert text(md, link_refs[0].id) == b"id" def test_no_image_ref_escaped_brackets(self) -> None: md = b"!\\[text\\]" refs = collect(md) assert len(refs) == 0 def test_no_image_ref_escaped_opening_bracket(self) -> None: md = b"!\\[text]" refs = collect(md) assert len(refs) == 0 def test_no_image_ref_escaped_closing_bracket(self) -> None: md = b"![text\\]" refs = collect(md) assert len(refs) == 0 # --------------------------------------------------------------------------- class TestInnerLinks: """Tests for links in links and images.""" def test_link_ref_in_link(self) -> None: md = b"[text[more]](href)" refs = collect(md) assert len(refs) == 2 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == b"text[more]" assert text(md, links[0].href) == b"href" link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"more" assert text(md, link_refs[0].id) == b"more" def test_link_ref_in_link_ref(self) -> None: md = b"[text[more]][id]" refs = collect(md) assert len(refs) == 2 link_refs = link_refs_only(refs) assert len(link_refs) == 2 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"text[more]" assert text(md, link_refs[0].id) == b"id" assert link_refs[1].kind == "link" assert text(md, link_refs[1].text) == b"more" assert text(md, link_refs[1].id) == b"more" def test_image_in_link(self) -> None: md = b"[![alt](image.png)](href)" refs = collect(md) assert len(refs) == 2 links = links_only(refs) assert len(links) == 2 assert links[0].kind == "link" assert text(md, links[0].href) == b"href" assert links[1].kind == "image" assert text(md, links[1].text) == b"alt" assert text(md, links[1].href) == b"image.png" def test_image_ref_in_link(self) -> None: md = b"[![alt][image-id]](href)" refs = collect(md) assert len(refs) == 2 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].href) == b"href" link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "image" assert text(md, link_refs[0].text) == b"alt" assert text(md, link_refs[0].id) == b"image-id" def test_image_in_link_ref(self) -> None: md = b"[![alt](image.png)][id]" refs = collect(md) assert len(refs) == 2 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "image" assert text(md, links[0].text) == b"alt" assert text(md, links[0].href) == b"image.png" link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].id) == b"id" def test_image_ref_in_link_ref(self) -> None: md = b"[![alt][image-id]][id]" refs = collect(md) assert len(refs) == 2 link_refs = link_refs_only(refs) assert len(link_refs) == 2 assert link_refs[0].kind == "link" assert text(md, link_refs[0].id) == b"id" assert link_refs[1].kind == "image" assert text(md, link_refs[1].text) == b"alt" assert text(md, link_refs[1].id) == b"image-id" def test_no_link_ref_in_link(self) -> None: md = b"[text\\[more\\]](href)" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == b"text\\[more\\]" assert text(md, links[0].href) == b"href" def test_no_link_ref_in_link_ref(self) -> None: md = b"[text\\[more\\]][id]" refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"text\\[more\\]" assert text(md, link_refs[0].id) == b"id" # --------------------------------------------------------------------------- class TestAutolinks: """Tests for autolinks.""" @pytest.mark.parametrize( ("md", "expected_href"), [ pytest.param( b"", b"https://example.com", id="autolink-https", ), pytest.param( b"", b"http://example.com", id="autolink-http", ), ], ) def test_autolink(self, md: bytes, expected_href: bytes) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "autolink" assert text(md, links[0].text) == expected_href assert text(md, links[0].href) == expected_href # --- negative cases --- @pytest.mark.parametrize( "md", [ pytest.param( b"", id="autolink-scheme, missing", ), pytest.param( b"", id="autolink-scheme, unsupported", ), pytest.param( b"<>", id="autolink-empty", ), ], ) def test_no_autolink(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 0 # --------------------------------------------------------------------------- class TestWikilinks: """Tests for wikilinks.""" @pytest.mark.parametrize( ("md", "expected_text"), [ pytest.param( b"[[Page]]", b"Page", id="wikilink", ), pytest.param( b"[[Page|display]]", b"Page|display", id="wikilink-with-text", ), ], ) def test_wikilink(self, md: bytes, expected_text: bytes) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert links[0].kind == "wikilink" assert text(md, links[0].text) == expected_text assert text(md, links[0].href) == expected_text # --- negative cases --- @pytest.mark.parametrize( ("md", "expected_text", "expected_id"), [ pytest.param( b"[[Page]][id]", b"[Page]", b"id", id="link-ref", ), pytest.param( b"[[Page]][]", b"[Page]", b"[Page]", id="link-ref, collapsed", ), pytest.param( b"[[Page]]\n[id]", b"[Page]", b"id", id="link-ref-with-newline", ), pytest.param( b"[[Page]]\r\n[id]", b"[Page]", b"id", id="link-ref-with-crlf", ), pytest.param( b"[[Page]]\r[id]", b"[Page]", b"id", id="link-ref-with-cr", ), ], ) def test_no_wikilink( self, md: bytes, expected_text: bytes, expected_id: bytes ) -> None: refs = collect(md) assert len(refs) == 2 link_refs = link_refs_only(refs) assert len(link_refs) == 2 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == expected_text assert text(md, link_refs[0].id) == expected_id assert link_refs[1].kind == "link" assert text(md, link_refs[1].text) == b"Page" assert text(md, link_refs[1].id) == b"Page" # --------------------------------------------------------------------------- class TestFootnoteReferences: """Tests for footnote references.""" @pytest.mark.parametrize( ("md", "expected_id"), [ pytest.param( b"[^]", b"", id="footnote-ref-empty-id", ), pytest.param( b"[^1]", b"1", id="footnote-ref", ), pytest.param( b"[^note]", b"note", id="footnote-ref-with-name", ), pytest.param( b"[^@#$%]", b"@#$%", id="footnote-ref-with-special-chars", ), ], ) def test_footnote_ref(self, md: bytes, expected_id: bytes) -> None: refs = collect(md) assert len(refs) == 1 note_refs = footnote_refs_only(refs) assert len(note_refs) == 1 assert text(md, note_refs[0].id) == expected_id # --- negative cases --- @pytest.mark.parametrize( ("md", "expected_id"), [ pytest.param( b"[^ ]", b"^ ", id="link-ref, space", ), pytest.param( b"[^multi word note]", b"^multi word note", id="link-ref, separated", ), ], ) def test_no_footnote_ref(self, md: bytes, expected_id: bytes) -> None: refs = collect(md) assert len(refs) == 1 link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == expected_id assert text(md, link_refs[0].id) == expected_id def test_no_footnote_ref_escaped_id(self) -> None: md = b"[^a\\_b]\n\n[^a_b]: note" refs = collect(md) assert len(refs) == 1 note_defs = footnote_defs_only(refs) assert len(note_defs) == 1 assert text(md, note_defs[0].id) == b"a_b" # --------------------------------------------------------------------------- class TestFootnoteDefinitions: """Tests for footnote definitions.""" @pytest.mark.parametrize( ("md", "expected_id", "expected_body"), [ pytest.param( b"[^1]: body", b"1", b"body", id="footnote-def", ), pytest.param( b"[^note]: body", b"note", b"body", id="footnote-def-with-name", ), pytest.param( b"[^@#$%]: body", b"@#$%", b"body", id="footnote-def-with-special-chars", ), pytest.param( b"[^1]: body\n continuation\n and so on.", b"1", b"body\n continuation\n and so on.", id="footnote-def, multiple lines", ), ], ) def test_footnote_def( self, md: bytes, expected_id: bytes, expected_body: bytes ) -> None: refs = collect(md) assert len(refs) == 1 note_defs = footnote_defs_only(refs) assert len(note_defs) == 1 assert text(md, note_defs[0].id) == expected_id assert text(md, note_defs[0].body) == expected_body def test_footnote_def_with_link(self) -> None: md = b"[^1]: [text](href)" refs = collect(md) assert len(refs) == 2 note_defs = footnote_defs_only(refs) assert len(note_defs) == 1 assert text(md, note_defs[0].id) == b"1" links = links_only(refs) assert len(links) == 1 assert links[0].kind == "link" assert text(md, links[0].text) == b"text" assert text(md, links[0].href) == b"href" def test_footnote_def_with_link_ref(self) -> None: md = b"[^1]: [id]" refs = collect(md) assert len(refs) == 2 note_defs = footnote_defs_only(refs) assert len(note_defs) == 1 assert text(md, note_defs[0].id) == b"1" link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "link" assert text(md, link_refs[0].text) == b"id" assert text(md, link_refs[0].id) == b"id" def test_footnote_def_with_link_def(self) -> None: md = b"[^1]: body\n[id]: href" refs = collect(md) assert len(refs) == 2 note_defs = footnote_defs_only(refs) assert len(note_defs) == 1 assert text(md, note_defs[0].id) == b"1" link_defs = link_defs_only(refs) assert len(link_defs) == 1 assert text(md, link_defs[0].id) == b"id" assert text(md, link_defs[0].href) == b"href" def test_footnote_def_with_image(self) -> None: md = b"[^1]: ![alt](href)" refs = collect(md) assert len(refs) == 2 note_defs = footnote_defs_only(refs) assert len(note_defs) == 1 assert text(md, note_defs[0].id) == b"1" links = links_only(refs) assert len(links) == 1 assert links[0].kind == "image" assert text(md, links[0].text) == b"alt" assert text(md, links[0].href) == b"href" def test_footnote_def_with_image_ref(self) -> None: md = b"[^1]: ![id]" refs = collect(md) assert len(refs) == 2 note_defs = footnote_defs_only(refs) assert len(note_defs) == 1 assert text(md, note_defs[0].id) == b"1" link_refs = link_refs_only(refs) assert len(link_refs) == 1 assert link_refs[0].kind == "image" assert text(md, link_refs[0].text) == b"id" assert text(md, link_refs[0].id) == b"id" def test_no_footnote_def_with_inline_code(self) -> None: md = b"[^1]: `[code]`" refs = collect(md) assert len(refs) == 1 note_defs = footnote_defs_only(refs) assert len(note_defs) == 1 # --- negative cases --- def test_no_footnote_def_prefix(self) -> None: md = b"text [^1]: body\n" refs = collect(md) assert len(refs) == 1 note_refs = footnote_refs_only(refs) assert len(note_refs) == 1 # --------------------------------------------------------------------------- class TestFencedCodeBlocks: """Tests for fenced code blocks.""" @pytest.mark.parametrize( "md", [ pytest.param( b"```\n[text](href)\n```", id="fenced-code-3-backticks", ), pytest.param( b"``` python\n[text](href)\n```", id="fenced-code-3-backticks-lang", ), pytest.param( b"````\n[text](href)\n````", id="fenced-code-4-backticks", ), pytest.param( b"```` py\n[text](href)\n````", id="fenced-code-4-backticks-lang", ), pytest.param( b"~~~\n[text](href)\n~~~", id="fenced-code-3-tildes", ), pytest.param( b"~~~ python\n[text](href)\n~~~", id="fenced-code-3-tildes-lang", ), pytest.param( b"~~~~\n[text](href)\n~~~~", id="fenced-code-4-tildes", ), pytest.param( b"~~~~ py\n[text](href)\n~~~~", id="fenced-code-4-tildes-lang", ), pytest.param( b"\n```\n[Start][]\n```\n", id="fenced-code-with-info", ), pytest.param( b"```\r\n[Start]\r\n```\r\n", id="fenced-code-crlf-with-shortcut-link-ref", ), pytest.param( b"```\r[Start]\r```\r", id="fenced-code-cr-with-shortcut-link-ref", ), ], ) def test_fenced_code(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 0 def test_fenced_code_with_link_after(self) -> None: md = b"```\n[text](href)\n```\n[after](href)" 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" def test_fenced_code_with_link_before(self) -> None: md = b"[before](href)\n```\n[text](href)\n```" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert text(md, links[0].text) == b"before" assert text(md, links[0].href) == b"href" # --- negative cases --- @pytest.mark.parametrize( "md", [ pytest.param( b"```\n[text](href)\n````", id="fenced-code-unbalanced-backticks", ), pytest.param( b"```\n[text](href)", id="fenced-code-missing-backticks", ), pytest.param( b"```\n[text](href)\n~~~", id="fenced-code-backticks-tildes", ), pytest.param( b"~~~\n[text](href)\n```", id="fenced-code-tildes-backticks", ), ], ) def test_no_fenced_code(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert text(md, links[0].text) == b"text" assert text(md, links[0].href) == b"href" # --------------------------------------------------------------------------- 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 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.""" @pytest.mark.parametrize( "md", [ pytest.param( b"`[text](href)`", id="inline-code", ), pytest.param( b"``[text](href)``", id="inline-code-2-backticks", ), pytest.param( b"`` [text](href) ``", id="inline-code-2-backticks, spaces", ), pytest.param( b"```[text](href)```", id="inline-code-3-backticks", ), pytest.param( b"``text`[text](href)``", id="inline-code, inner backticks", ), pytest.param( b"`` `[text](href)`", id="inline-code, empty code span", ), ], ) def test_inline_code(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 0 def test_inline_code_with_link_after(self) -> None: md = b"`code` [text](href)" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert text(md, links[0].text) == b"text" assert text(md, links[0].href) == b"href" def test_inline_code_with_link_before(self) -> None: md = b"[text](href) `code`" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert text(md, links[0].text) == b"text" assert text(md, links[0].href) == b"href" # --- negative cases --- @pytest.mark.parametrize( "md", [ pytest.param( b"`[text](href)\n``", id="inline-code-unbalanced-backticks", ), pytest.param( b"`\n[text](href)", id="inline-code-missing-backticks", ), ], ) def test_no_inline_code(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert text(md, links[0].text) == b"text" assert text(md, links[0].href) == b"href" # --------------------------------------------------------------------------- class TestMath: """Tests for math blocks and inline math.""" @pytest.mark.parametrize( "md", [ pytest.param( b"$$\n[text](href)\n$$", id="math-block", ), pytest.param( b"$$ [text](href) $$", id="math-block, single line", ), pytest.param( b"$[text](href)$", id="math-inline", ), pytest.param( b"\\[\n[text](href)\n\\]", id="match-block-brackets", ), pytest.param( b"\\[\r\n[text](href)\r\n\\]", id="match-block-brackets-crlf", ), pytest.param( b"\\[\r[text](href)\r\\]", id="match-block-brackets-cr", ), pytest.param( b"\\[ [text](href) \\]", id="match-block-brackets, single line", ), pytest.param( b"\\([text](href)\\)", id="math-inline-parens", ), ], ) def test_math(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 0 def test_math_with_link_after(self) -> None: md = b"$$\n[text](href)\n$$\n[after](href)" 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" def test_math_with_link_before(self) -> None: md = b"[before](href)\n$$\n[text](href)\n$$" refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert text(md, links[0].text) == b"before" assert text(md, links[0].href) == b"href" def test_no_math(self) -> None: md = b"1.000$" refs = collect(md) assert len(refs) == 0 def test_no_math_inline_across_code(self) -> None: md = b"| $ | `#!python 'ab$'` | `#!python '[abc]'` |" refs = collect(md) assert len(refs) == 0 @pytest.mark.parametrize( "md", [ pytest.param(b"$[text](href)\n$", id="lf"), pytest.param(b"$[text](href)\r\n$", id="crlf"), pytest.param(b"$[text](href)\r$", id="cr"), ], ) def test_no_math_inline_across_line_ending(self, md: bytes) -> None: refs = collect(md) assert len(refs) == 1 links = links_only(refs) assert len(links) == 1 assert text(md, links[0].text) == b"text" # --------------------------------------------------------------------------- class TestHtmlLinks: """Tests for HTML elements with links.""" @pytest.mark.parametrize( ("md", "expected_href"), [ pytest.param( b'text', b"href", id="html-a-href", ), pytest.param( b'
\ntext\n
', b"href", id="html-a-href, in block", ), pytest.param( b'
\r\ntext\r\n
', b"href", id="html-a-href, in block, crlf", ), pytest.param( b'
\rtext\r
', b"href", id="html-a-href, in block, cr", ), pytest.param( b'', b"image.png", id="html-img-src", ), pytest.param( b'', b"image.png", id="html-img-src, self-closing", ), pytest.param( b'
\n\n
', b"image.png", id="html-img-src, in block", ), pytest.param( b'

\n\n

', b"image.png", id="html-p-img-src, in block, multiple lines", ), pytest.param( b'', b"style.css", id="html-link-href", ), pytest.param( b'', b"script.js", id="html-script-src", ), pytest.param( b'