Skip to content

Commit

Permalink
Merge pull request #415 from ihabunek/danschwarz-richtext3
Browse files Browse the repository at this point in the history
Add support for rich text
  • Loading branch information
ihabunek authored Nov 18, 2023
2 parents fe8b441 + 9b9c153 commit 317840b
Show file tree
Hide file tree
Showing 15 changed files with 604 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install -e .\[richtext\]
pip install -r requirements-test.txt
- name: Run tests
run: |
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ requests>=2.13,<3.0
beautifulsoup4>=4.5.0,<5.0
wcwidth>=0.1.7
urwid>=2.0.0,<3.0

urwidgets>=0.1,<0.2
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python :: 3',
],
packages=['toot', 'toot.tui', 'toot.utils'],
packages=['toot', 'toot.tui', 'toot.tui.richtext', 'toot.utils'],
python_requires=">=3.7",
install_requires=[
"requests>=2.13,<3.0",
Expand All @@ -40,6 +40,9 @@
"urwid>=2.0.0,<3.0",
"tomlkit>=0.10.0,<1.0"
],
extras_require={
"richtext": ['urwidgets>=0.1,<0.2'],
},
entry_points={
'console_scripts': [
'toot=toot.console:main',
Expand Down
6 changes: 6 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from toot.console import duration
from toot.wcstring import wc_wrap, trunc, pad, fit_text
from toot.utils import urlencode_url


def test_pad():
Expand Down Expand Up @@ -201,3 +202,8 @@ def test_duration():

with pytest.raises(ArgumentTypeError):
duration("banana")


def test_urlencode_url():
assert urlencode_url("https://www.example.com") == "https://www.example.com"
assert urlencode_url("https://www.example.com/url%20with%20spaces") == "https://www.example.com/url%20with%20spaces"
45 changes: 45 additions & 0 deletions tests/tui/test_rich_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from urwid import Divider, Filler, Pile
from toot.tui.richtext import url_to_widget
from urwidgets import Hyperlink, TextEmbed

from toot.tui.richtext.richtext import html_to_widgets


def test_url_to_widget():
url = "http://foo.bar"
embed_widget = url_to_widget(url)
assert isinstance(embed_widget, TextEmbed)

[(filler, length)] = embed_widget.embedded
assert length == len(url)
assert isinstance(filler, Filler)

link_widget: Hyperlink = filler.base_widget
assert isinstance(link_widget, Hyperlink)

assert link_widget.attrib == "link"
assert link_widget.text == url
assert link_widget.uri == url


def test_html_to_widgets():
html = """
<p>foo</p>
<p>foo <b>bar</b> <i>baz</i></p>
""".strip()

[foo, divider, bar] = html_to_widgets(html)

assert isinstance(foo, Pile)
assert isinstance(divider, Divider)
assert isinstance(bar, Pile)

[foo_embed] = foo.widget_list
assert foo_embed.embedded == []
assert foo_embed.attrib == []
assert foo_embed.text == "foo"

[bar_embed] = bar.widget_list
assert bar_embed.embedded == []
assert bar_embed.attrib == [(None, 4), ("b", 3), (None, 1), ("i", 3)]
assert bar_embed.text == "foo bar baz"
4 changes: 2 additions & 2 deletions toot/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from functools import lru_cache
from toot import settings
from toot.entities import Instance, Notification, Poll, Status
from toot.utils import get_text, parse_html
from toot.utils import get_text, html_to_paragraphs
from toot.wcstring import wc_wrap
from typing import List
from wcwidth import wcswidth
Expand Down Expand Up @@ -321,7 +321,7 @@ def print_status(status: Status, width: int = 80):

def print_html(text, width=80):
first = True
for paragraph in parse_html(text):
for paragraph in html_to_paragraphs(text):
if not first:
print_out("")
for line in paragraph:
Expand Down
17 changes: 0 additions & 17 deletions toot/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ def __init__(self, app, user, screen, args):
def run(self):
self.loop.set_alarm_in(0, lambda *args: self.async_load_instance())
self.loop.set_alarm_in(0, lambda *args: self.async_load_followed_accounts())
self.loop.set_alarm_in(0, lambda *args: self.async_load_followed_tags())
self.loop.set_alarm_in(0, lambda *args: self.async_load_timeline(
is_initial=True, timeline_name="home"))
self.loop.run()
Expand Down Expand Up @@ -339,22 +338,6 @@ def _done_accounts(accounts):

self.run_in_thread(_load_accounts, done_callback=_done_accounts)

def async_load_followed_tags(self):
def _load_tag_list():
try:
return api.followed_tags(self.app, self.user)
except ApiError:
# not supported by all Mastodon servers so fail silently if necessary
return []

def _done_tag_list(tags):
if len(tags) > 0:
self.followed_tags = [t["name"] for t in tags]
else:
self.followed_tags = []

self.run_in_thread(_load_tag_list, done_callback=_done_tag_list)

def refresh_footer(self, timeline):
"""Show status details in footer."""
status, index, count = timeline.get_focused_status_with_counts()
Expand Down
23 changes: 23 additions & 0 deletions toot/tui/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,29 @@
('dim', 'dark gray', ''),
('highlight', 'yellow', ''),
('success', 'dark green', ''),

# HTML tag styling
('a', ',italics', '', 'italics'),
# em tag is mapped to i
('i', ',italics', '', 'italics'),
# strong tag is mapped to b
('b', ',bold', '', 'bold'),
# special case for bold + italic nested tags
('bi', ',bold,italics', '', ',bold,italics'),
('u', ',underline', '', ',underline'),
('del', ',strikethrough', '', ',strikethrough'),
('code', 'light gray, standout', '', ',standout'),
('pre', 'light gray, standout', '', ',standout'),
('blockquote', 'light gray', '', ''),
('h1', ',bold', '', ',bold'),
('h2', ',bold', '', ',bold'),
('h3', ',bold', '', ',bold'),
('h4', ',bold', '', ',bold'),
('h5', ',bold', '', ',bold'),
('h6', ',bold', '', ',bold'),
('class_mention_hashtag', 'light cyan', '', ''),
('class_hashtag', 'light cyan', '', ''),

]

VISIBILITY_OPTIONS = [
Expand Down
19 changes: 12 additions & 7 deletions toot/tui/overlays.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import webbrowser

from toot import __version__
from toot.utils import format_content
from .utils import highlight_hashtags, highlight_keys
from .widgets import Button, EditBox, SelectableText
from toot import api
from toot.tui.utils import highlight_keys
from toot.tui.widgets import Button, EditBox, SelectableText
from toot.tui.richtext import html_to_widgets


class StatusSource(urwid.Padding):
Expand Down Expand Up @@ -279,8 +279,10 @@ def generate_contents(self, account, relationship=None, last_action=None):

if account["note"]:
yield urwid.Divider()
for line in format_content(account["note"]):
yield urwid.Text(highlight_hashtags(line, followed_tags=set()))

widgetlist = html_to_widgets(account["note"])
for line in widgetlist:
yield (line)

yield urwid.Divider()
yield urwid.Text(["ID: ", ("highlight", f"{account['id']}")])
Expand Down Expand Up @@ -312,8 +314,11 @@ def generate_contents(self, account, relationship=None, last_action=None):
name = field["name"].title()
yield urwid.Divider()
yield urwid.Text([("bold", f"{name.rstrip(':')}"), ":"])
for line in format_content(field["value"]):
yield urwid.Text(highlight_hashtags(line, followed_tags=set()))

widgetlist = html_to_widgets(field["value"])
for line in widgetlist:
yield (line)

if field["verified_at"]:
yield urwid.Text(("success", "✓ Verified"))

Expand Down
11 changes: 6 additions & 5 deletions toot/tui/poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

from toot import api
from toot.exceptions import ApiError
from toot.utils import format_content
from toot.utils.datetime import parse_datetime

from .utils import highlight_hashtags
from .widgets import Button, CheckBox, RadioButton
from .richtext import html_to_widgets


class Poll(urwid.ListBox):
Expand Down Expand Up @@ -87,8 +85,11 @@ def generate_poll_detail(self):

def generate_contents(self, status):
yield urwid.Divider()
for line in format_content(status.data["content"]):
yield urwid.Text(highlight_hashtags(line, set()))

widgetlist = html_to_widgets(status.data["content"])

for line in widgetlist:
yield (line)

yield urwid.Divider()
yield self.build_linebox(self.generate_poll_detail())
Expand Down
18 changes: 18 additions & 0 deletions toot/tui/richtext/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import urwid

from toot.tui.utils import highlight_hashtags
from toot.utils import format_content
from typing import List

try:
from .richtext import html_to_widgets, url_to_widget
except ImportError:
# Fallback if urwidgets are not available
def html_to_widgets(html: str) -> List[urwid.Widget]:
return [
urwid.Text(highlight_hashtags(line))
for line in format_content(html)
]

def url_to_widget(url: str):
return urwid.Text(("link", url))
Loading

0 comments on commit 317840b

Please sign in to comment.