Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions src/textual/widgets/_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
10 changes: 6 additions & 4 deletions tests/test_markdownviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
TEST_MARKDOWN = """\
* [First]({{file}}#first)
* [Second](#second)
* [GitHub](https://github.com/textualize/textual/)

# First

Expand All @@ -24,6 +25,7 @@


class MarkdownFileViewerApp(App[None]):

def __init__(self, markdown_file: Path) -> None:
super().__init__()
self.markdown_file = markdown_file
Expand All @@ -37,14 +39,14 @@ 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:
# 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]):
Expand All @@ -59,7 +61,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

Expand All @@ -70,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]"])
Expand Down