From 65e24ccaa8cf89c340edb769bb6c420b2f74660f Mon Sep 17 00:00:00 2001 From: Aearsears <53102185+Aearsears@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:25:15 -0400 Subject: [PATCH 1/5] handle http/https links in MarkDown, handle relative MD links in MDViewer --- src/textual/widgets/_markdown.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 397b265b50..0bc4c283de 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -6,7 +6,7 @@ from functools import partial from pathlib import Path, PurePath from typing import Callable, Iterable, Optional -from urllib.parse import unquote +from urllib.parse import unquote, urlparse from markdown_it import MarkdownIt from markdown_it.token import Token @@ -1128,8 +1128,9 @@ async def stream_markdown(self) -> None: return updater def on_markdown_link_clicked(self, event: LinkClicked) -> None: - if self._open_links: + if self._open_links and is_http_url(event.href): self.app.open_url(event.href) + event.stop() @staticmethod def sanitize_location(location: str) -> tuple[Path, str]: @@ -1628,8 +1629,9 @@ async def forward(self) -> None: self.post_message(self.NavigatorUpdated()) async def _on_markdown_link_clicked(self, message: Markdown.LinkClicked) -> None: - message.stop() - await self.go(message.href) + if not is_http_url(message.href): + message.stop() + await self.go(message.href) def watch_show_table_of_contents(self, show_table_of_contents: bool) -> None: self.set_class(show_table_of_contents, "-show-table-of-contents") @@ -1657,3 +1659,9 @@ def _on_markdown_table_of_contents_selected( block = self.query_one(block_selector, MarkdownBlock) self.scroll_to_widget(block, top=True) message.stop() + + +def is_http_url(url: str) -> bool: + """Check if a URL is an HTTP or HTTPS URL.""" + parsed = urlparse(url) + return parsed.scheme in ("http", "https") From ccb44b9ddf3d97260e3fc4464c1cf9bc00539707 Mon Sep 17 00:00:00 2001 From: Aearsears <53102185+Aearsears@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:33:51 -0400 Subject: [PATCH 2/5] update mdviewer test to click on https link --- tests/test_markdownviewer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_markdownviewer.py b/tests/test_markdownviewer.py index 3c83d271d2..637d4d93e8 100644 --- a/tests/test_markdownviewer.py +++ b/tests/test_markdownviewer.py @@ -12,6 +12,7 @@ TEST_MARKDOWN = """\ * [First]({{file}}#first) * [Second](#second) +* [GitHub](https://github.com/textualize/textual/) # First @@ -37,7 +38,7 @@ async def on_mount(self) -> None: await self.query_one(MarkdownViewer).go(self.markdown_file) -@pytest.mark.parametrize("link", [0, 1]) +@pytest.mark.parametrize("link", [0, 1, 2]) async def test_markdown_file_viewer_anchor_link(tmp_path, link: int) -> None: """Test https://github.com/Textualize/textual/issues/3094""" async with MarkdownFileViewerApp(Path(tmp_path) / "test.md").run_test() as pilot: @@ -59,7 +60,7 @@ async def on_mount(self) -> None: self.query_one(MarkdownViewer).show_table_of_contents = False -@pytest.mark.parametrize("link", [0, 1]) +@pytest.mark.parametrize("link", [0, 1, 2]) async def test_markdown_string_viewer_anchor_link(link: int) -> None: """Test https://github.com/Textualize/textual/issues/3094 From 01cf0c21c709e2d2e658a41c665b06c43776a07d Mon Sep 17 00:00:00 2001 From: Aearsears <53102185+Aearsears@users.noreply.github.com> Date: Sun, 31 Aug 2025 18:08:53 -0400 Subject: [PATCH 3/5] mae markdownviewer tests click on links --- tests/test_markdownviewer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_markdownviewer.py b/tests/test_markdownviewer.py index 637d4d93e8..85075b1ae7 100644 --- a/tests/test_markdownviewer.py +++ b/tests/test_markdownviewer.py @@ -25,6 +25,7 @@ class MarkdownFileViewerApp(App[None]): + def __init__(self, markdown_file: Path) -> None: super().__init__() self.markdown_file = markdown_file @@ -45,7 +46,7 @@ async def test_markdown_file_viewer_anchor_link(tmp_path, link: int) -> None: # There's not really anything to test *for* here, but the lack of an # exception is the win (before the fix this is testing it would have # been FileNotFoundError). - await pilot.click(Markdown, Offset(2, link)) + await pilot.click(Markdown, Offset(4, link)) class MarkdownStringViewerApp(App[None]): @@ -71,7 +72,7 @@ async def test_markdown_string_viewer_anchor_link(link: int) -> None: # There's not really anything to test *for* here, but the lack of an # exception is the win (before the fix this is testing it would have # been FileNotFoundError). - await pilot.click(Markdown, Offset(2, link)) + await pilot.click(Markdown, Offset(4, link)) @pytest.mark.parametrize("text", ["Hey [[/test]]", "[i]Hey there[/i]"]) From 2b00b7bbc68308bd3a972eb128c3e8195b941fbf Mon Sep 17 00:00:00 2001 From: Aearsears <53102185+Aearsears@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:13:55 -0400 Subject: [PATCH 4/5] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e84af5e2..80e1119f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed return value for `Pilot.double_click` and `Pilot.triple_click` https://github.com/Textualize/textual/pull/6035 - Fixed sizing issue with `Pretty` widget https://github.com/Textualize/textual/pull/6040 https://github.com/Textualize/textual/pull/6041 - Fixed garbled inline app output when `inline_no_clear=True` https://github.com/Textualize/textual/pull/6080 +- Fixed `Markdown` and `MarkdownViewer` widgets both handling `Markdown.LinkClicked` messages https://github.com/Textualize/textual/pull/6093 ### Added From 9ccc7850ef136fbf325524677eab9daee84e3f93 Mon Sep 17 00:00:00 2001 From: Aearsears <53102185+Aearsears@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:16:19 -0400 Subject: [PATCH 5/5] update CHANGELOG --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e1119f00..ca0ad9520c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [6.1.0] - 2025-08-01 +### Fixed + +- Fixed `Markdown` and `MarkdownViewer` widgets both handling `Markdown.LinkClicked` messages https://github.com/Textualize/textual/pull/6093 + ### Added - Added `Button.flat` boolean to enable flat button style https://github.com/Textualize/textual/pull/6094 @@ -22,7 +26,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed return value for `Pilot.double_click` and `Pilot.triple_click` https://github.com/Textualize/textual/pull/6035 - Fixed sizing issue with `Pretty` widget https://github.com/Textualize/textual/pull/6040 https://github.com/Textualize/textual/pull/6041 - Fixed garbled inline app output when `inline_no_clear=True` https://github.com/Textualize/textual/pull/6080 -- Fixed `Markdown` and `MarkdownViewer` widgets both handling `Markdown.LinkClicked` messages https://github.com/Textualize/textual/pull/6093 ### Added