Repository: mondeja/mkdocs-include-markdown-plugin
Pull request #252 (2025-01-19 15:45:41)

Add HTML support for relative URL rewrites
**Description of Change**

- New Feature:
  - Add raw html (`image`, `source`, `a`) support for `rewrite_relative_url`.
- Fix:
  - Support external top level domain

**Checklist**

- [x] Feature working
- [x] Updated unit test

**Related Issue**

#251 

maekawataiki: @mondeja Thank you for the review. I've added support for `a` and `source` tags.
maekawataiki: I've noticed currently this library doesn't support external top level domain (i.e. `https://example.com`) and throws error, so also adding fix and test for it.
maekawataiki: Sorry, I just pushed the change.
maekawataiki: @mondeja Thank you for the reviews. Do you have any other concern?

Patch:
diff --git a/src/mkdocs_include_markdown_plugin/process.py b/src/mkdocs_include_markdown_plugin/process.py
index 4d41abc..3c47550 100644
--- a/src/mkdocs_include_markdown_plugin/process.py
+++ b/src/mkdocs_include_markdown_plugin/process.py
@@ -112,6 +112,49 @@ MARKDOWN_LINK_DEFINITION_REGEX = re.compile(
     flags=re.VERBOSE | re.MULTILINE,
 )
 
+# Matched html image and source definition.
+# e.g. <img src="path/to/image.png" alt="alt-text">
+# e.g. <source src="path/to/image.png" alt="alt-text">
+MARKDOWN_HTML_IMAGE_REGEX = re.compile(
+    r'''
+        <(?:img|source)    # img or source
+        (?:\s+             # More than one whitespace
+            (?!src=)       # Not src=
+            [\w-]+         # attribute name
+            (?:\s*=\s*)?   # arbitrary whitespace (optional)
+            (?:
+                "[^"]*"    # Quoted value (double quote)
+                |
+                '[^']*'    # Quoted value (single quote)
+            )?
+        )*                 # Other attributes are repeated 0 or more times
+        \s+                # More than one whitespace
+        src=["'](\S+?)["'] # src = $1 (double quote or single quote)
+    ''',
+    flags=re.VERBOSE | re.MULTILINE,
+)
+
+# Matched html anchor definition.
+# e.g. <a href="https://example.com">example</a>
+MARKDOWN_HTML_ANCHOR_DEFINITION_REGEX = re.compile(
+    r'''
+        <a
+        (?:\s+             # More than one whitespace
+            (?!href=)      # Not href=
+            [\w-]+         # attribute name
+            (?:\s*=\s*)?   # arbitrary whitespace (optional)
+            (?:
+                "[^"]*"    # Quoted value (double quote)
+                |
+                '[^']*'    # Quoted value (single quote)
+            )?
+        )*                 # Other attributes are repeated 0 or more times
+        \s+                # More than one whitespace
+        href=["'](\S+?)["']# href = $1 (double quote or single quote)
+    ''',
+    flags=re.VERBOSE | re.MULTILINE,
+)
+
 
 def transform_p_by_p_skipping_codeblocks(  # noqa: PLR0912, PLR0915
         markdown: str,
@@ -247,6 +290,9 @@ def rewrite_relative_urls(
     from urllib.parse import urlparse, urlunparse
 
     def rewrite_url(url: str) -> str:
+        if is_url(url):
+            return url
+
         scheme, netloc, path, params, query, fragment = urlparse(url)
 
         # absolute or mail
@@ -296,10 +342,18 @@ def rewrite_relative_urls(
             found_href_url_group_index_3,
             paragraph,
         )
-        return MARKDOWN_LINK_DEFINITION_REGEX.sub(
+        paragraph = MARKDOWN_LINK_DEFINITION_REGEX.sub(
             functools.partial(found_href, url_group_index=2),
             paragraph,
         )
+        paragraph = MARKDOWN_HTML_IMAGE_REGEX.sub(
+            functools.partial(found_href, url_group_index=1),
+            paragraph,
+        )
+        return MARKDOWN_HTML_ANCHOR_DEFINITION_REGEX.sub(
+            functools.partial(found_href, url_group_index=1),
+            paragraph,
+        )
 
     return transform_p_by_p_skipping_codeblocks(
         markdown,


Test patch:
diff --git a/tests/test_unit/test_process.py b/tests/test_unit/test_process.py
index 62d8655..064ea86 100644
--- a/tests/test_unit/test_process.py
+++ b/tests/test_unit/test_process.py
@@ -13,6 +13,7 @@ from mkdocs_include_markdown_plugin.process import (
 @pytest.mark.parametrize(
     ('markdown', 'source_path', 'destination_path', 'expected_result'),
     (
+        # Markdown Relative Links
         pytest.param(
             "Here's a [link](CHANGELOG.md) to the changelog.",
             'README',
@@ -78,6 +79,133 @@ Check [this link](includes/feature_a/foobar.md) for more information
             'Build status: [![Build Status](../badge.png)](../build/)',
             id='image-inside-link',
         ),
+        # HTML Relative Links
+        pytest.param(
+            ('Here\'s a diagram: <img id="foo" src="assets/diagram.png"'
+             ' alt="diagram" class="bar" />'),
+            'README',
+            'docs/home.md',
+            ('Here\'s a diagram: <img id="foo" src="../assets/diagram.png"'
+             ' alt="diagram" class="bar" />'),
+            id='html-image',
+        ),
+        pytest.param(
+            ('Here\'s a diagram: <source id="foo" src="assets/diagram.png"'
+             ' class="bar" />'),
+            'README',
+            'docs/home.md',
+            ('Here\'s a diagram: <source id="foo" src="../assets/diagram.png"'
+             ' class="bar" />'),
+            id='html-source',
+        ),
+        pytest.param(
+            ('Here\'s a diagram: <a id="foo" href="badge.png"'
+             ' class="bar">example</a>'),
+            'README',
+            'docs/home.md',
+            ('Here\'s a diagram: <a id="foo" href="../badge.png"'
+             ' class="bar">example</a>'),
+            id='html-anchor',
+        ),
+        pytest.param(
+            ("Here's a diagram: <img id='foo' src='assets/diagram.png'"
+             " alt='diagram' class='bar' />"),
+            'README',
+            'docs/home.md',
+            ("Here's a diagram: <img id='foo' src='../assets/diagram.png'"
+             " alt='diagram' class='bar' />"),
+            id='html-image-single-quote',
+        ),
+        pytest.param(
+            ("Here's a diagram: <a id='foo' href='assets/diagram.png'"
+             " class='bar'>example</a>"),
+            'README',
+            'docs/home.md',
+            ("Here's a diagram: <a id='foo' href='../assets/diagram.png'"
+             " class='bar'>example</a>"),
+            id='html-anchor-single-quote',
+        ),
+        # HTML Relative Links Adverarial tests:
+        # (attribute contains >, attribute without value, multiple tag in line)
+        pytest.param(
+            ('<img id="foo" attr="3>2" attr2 src="assets/diagram.png"'
+             ' alt="diagram" class="bar" /><img id="foo" attr="3>2"'
+             ' src="assets/diagram.png" alt="diagram" class="bar" />'),
+            'README',
+            'docs/home.md',
+            ('<img id="foo" attr="3>2" attr2 src="../assets/diagram.png"'
+             ' alt="diagram" class="bar" /><img id="foo" attr="3>2"'
+             ' src="../assets/diagram.png" alt="diagram" class="bar" />'),
+            id='html-image-adverarial-test',
+        ),
+        pytest.param(
+            ('<a id="foo" attr="3>2" attr2 href="badge.png" class="bar">'
+             'foo</a><a id="foo" attr="3>2" href="badge.png" class="bar">'
+             'bar</a>'),
+            'README',
+            'docs/home.md',
+            ('<a id="foo" attr="3>2" attr2 href="../badge.png" class="bar">'
+             'foo</a><a id="foo" attr="3>2" href="../badge.png" class="bar">'
+             'bar</a>'),
+            id='html-anchor-adverarial-test',
+        ),
+        # HTML Relative Links Adversarial test: img no end slash
+        pytest.param(
+            ('Here\'s a diagram: <img id="foo" src="assets/diagram.png"'
+             ' alt="diagram" class="bar">'),
+            'README',
+            'docs/home.md',
+            ('Here\'s a diagram: <img id="foo" src="../assets/diagram.png"'
+             ' alt="diagram" class="bar">'),
+            id='html-image-no-end-slash',
+        ),
+        # Non-relative links
+        pytest.param(
+            "Here's a [link](/CHANGELOG.md) to the changelog.",
+            'README',
+            'docs/nav.md',
+            "Here's a [link](/CHANGELOG.md) to the changelog.",
+            id='absolute-link',
+        ),
+        pytest.param(
+            'A [link](https://example.com/index.html) to the changelog.',
+            'README',
+            'docs/nav.md',
+            'A [link](https://example.com/index.html) to the changelog.',
+            id='external-link',
+        ),
+        pytest.param(
+            "Here's a [link](https://example.com) to the changelog.",
+            'README',
+            'docs/nav.md',
+            "Here's a [link](https://example.com) to the changelog.",
+            id='external-top-level-link',
+        ),
+        pytest.param(
+            ('<img id="foo" attr="3>2" src="https://example.com/image.png"'
+             ' class="bar" />'),
+            'README',
+            'docs/home.md',
+            ('<img id="foo" attr="3>2" src="https://example.com/image.png"'
+             ' class="bar" />'),
+            id='html-image-external-link',
+        ),
+        pytest.param(
+            ('<a id="foo" attr="3>2" href="https://example.com/index.html"'
+             ' class="bar" />'),
+            'README',
+            'docs/home.md',
+            ('<a id="foo" attr="3>2" href="https://example.com/index.html"'
+             ' class="bar" />'),
+            id='html-anchor-external-link',
+        ),
+        pytest.param(
+            '<a id="ab" attr="3>2" href="https://example.com" class="cd" />',
+            'README',
+            'docs/home.md',
+            '<a id="ab" attr="3>2" href="https://example.com" class="cd" />',
+            id='html-anchor-external-top-level-link',
+        ),
         pytest.param(
             '''[Homepage](/) [Github](https://github.com/user/repo)
 [Privacy policy](/privacy)''',
@@ -158,6 +286,21 @@ const auto lambda = []() { .... };
 ''',
             id='exclude-fenced-code-blocks',
         ),
+        pytest.param(
+            '''```
+<img src="assets/diagram.png" alt="diagram">
+<a href="badge.png">example</a>
+```
+''',
+            'README',
+            'docs/nav.md',
+            '''```
+<img src="assets/diagram.png" alt="diagram">
+<a href="badge.png">example</a>
+```
+''',
+            id='exclude-fenced-code-blocks-html',
+        ),
         pytest.param(
             (
                 '    \n'