Repository: Textualize/textual

Issue:
Rendered Markdown links are broken into chunks when source text is multi-line
## Current behaviour

When a link in Markdown is split over two or more lines, the rendered link will be "broken" and not continue through the whitespace created for each of those broken lines.

Given the Markdown document:

```
My site [has
this
URL](https://example.com)
```

When we run that on [markdown-it](https://markdown-it.github.io/) it renders as a "single link" (sorry mouse disappeared when triggering screenshot):

![Screenshot 2023-06-19 at 15 04 15](https://github.com/Textualize/textual/assets/781059/bd9816db-7f35-49b5-a49d-6de5a37cd773)

However, when embedding that document into the [Markdown widget `markdown.py` example](https://textual.textualize.io/widgets/markdown/#__tabbed_1_2) from the docs as follows:

```py
from textual.app import App, ComposeResult
from textual.widgets import Markdown

EXAMPLE_MARKDOWN = """\
My site [has
this
URL](https://example.com)
"""


class MarkdownExampleApp(App):
    def compose(self) -> ComposeResult:
        yield Markdown(EXAMPLE_MARKDOWN)


if __name__ == "__main__":
    app = MarkdownExampleApp()
    app.run()
```

Then running with `python markdown.py` gives the following render - where you can see the link is "broken" to appear as 3 items:

![Screenshot from 2023-06-19 15-07-27](https://github.com/Textualize/textual/assets/781059/0b765e80-cc45-400e-b72f-22f75f60c68e)

And on hover:

![Screenshot from 2023-06-19 15-07-39](https://github.com/Textualize/textual/assets/781059/58109b6b-fb30-4c2a-93a6-ef61b4ca7b2c)

One subtle issue I've found with this bug, is when I hover over the white space gaps, the link does _not_ highlight 😞 

## Expected behaviour

Link is expected to render the same as if it was on a single line in the Markdown file:

![Screenshot from 2023-06-19 15-08-44](https://github.com/Textualize/textual/assets/781059/fb8d2b28-c69f-425d-afd8-890763dff7e4)

## Debug info...

<!-- This is valid Markdown, do not quote! -->
# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.28.0 |
| Rich    | 13.4.2 |

## Python

| Name           | Value                                  |
|----------------|----------------------------------------|
| Version        | 3.11.0                                 |
| Implementation | CPython                                |
| Compiler       | GCC 9.4.0                              |
| Executable     | /tmp/tmp.E7iYfjTaVi/venv311/bin/python |

## Operating System

| Name    | Value                                              |
|---------|----------------------------------------------------|
| System  | Linux                                              |
| Release | 5.15.0-75-generic                                  |
| Version | #82~20.04.1-Ubuntu SMP Wed Jun 7 19:37:37 UTC 2023 |

## Terminal

| Name                 | Value           |
|----------------------|-----------------|
| Terminal Application | *Unknown*       |
| TERM                 | screen-256color |
| COLORTERM            | truecolor       |
| FORCE_COLOR          | *Not set*       |
| NO_COLOR             | *Not set*       |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=105, height=58 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 105                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 58                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |

Sorry no kittens in this bug report 🙏🏻 



Discussion:
We found the following entries in the [FAQ](https://github.com/textualize/textual/blob/main/FAQ.md) which you may find helpful:


- [Does Textual support images?](https://github.com/textualize/textual/blob/main/FAQ.md#does-textual-support-images)
- [How can I select and copy text in a Textual app?](https://github.com/textualize/textual/blob/main/FAQ.md#how-can-i-select-and-copy-text-in-a-textual-app)

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)

Resolving patch:
diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py
--- a/src/textual/widgets/_markdown.py
+++ b/src/textual/widgets/_markdown.py
@@ -737,7 +737,7 @@ def update(self, markdown: str) -> None:
                         if child.type == "hardbreak":
                             content.append("\n")
                         if child.type == "softbreak":
-                            content.append(" ")
+                            content.append(" ", style_stack[-1])
                         elif child.type == "code_inline":
                             content.append(
                                 child.content,


Test patch:
diff --git a/tests/test_markdown.py b/tests/test_markdown.py
--- a/tests/test_markdown.py
+++ b/tests/test_markdown.py
@@ -6,6 +6,8 @@
 
 import pytest
 from markdown_it.token import Token
+from rich.style import Style
+from rich.text import Span
 
 import textual.widgets._markdown as MD
 from textual.app import App, ComposeResult
@@ -89,3 +91,27 @@ def markdown_nodes(root: Widget) -> Iterator[MarkdownBlock]:
         assert [
             node.__class__ for node in markdown_nodes(pilot.app.query_one(Markdown))
         ] == expected_nodes
+
+
+async def test_softbreak_split_links_rendered_correctly() -> None:
+    """Test for https://github.com/Textualize/textual/issues/2805"""
+
+    document = """\
+My site [has
+this
+URL](https://example.com)\
+"""
+    async with MarkdownApp(document).run_test() as pilot:
+        markdown = pilot.app.query_one(Markdown)
+        paragraph = markdown.children[0]
+        assert isinstance(paragraph, MD.MarkdownParagraph)
+        assert paragraph._text.plain == "My site has this URL"
+        expected_spans = [
+            Span(0, 8, Style()),
+            Span(8, 11, Style(meta={"@click": "link('https://example.com')"})),
+            Span(11, 12, Style(meta={"@click": "link('https://example.com')"})),
+            Span(12, 16, Style(meta={"@click": "link('https://example.com')"})),
+            Span(16, 17, Style(meta={"@click": "link('https://example.com')"})),
+            Span(17, 20, Style(meta={"@click": "link('https://example.com')"})),
+        ]
+        assert paragraph._text.spans == expected_spans