diff --git a/sims/utils/markup.py b/sims/utils/markup.py
new file mode 100644
index 0000000..e9edc8d
--- /dev/null
+++ b/sims/utils/markup.py
@@ -0,0 +1,28 @@
+import re
+from html import escape as html_escape
+
+import gi
+
+gi.require_version("Pango", "1.0")
+from gi.repository import GLib, Pango
+
+# Pango cannot render
from the freedesktop notification spec; strip it.
+_IMG_RE = re.compile(r"
]*/?>", re.IGNORECASE)
+
+
+def render_body_markup(body: str) -> tuple[str, bool]:
+ """Return ``(text, is_markup)`` for a notification body.
+
+ If the body parses as Pango markup, ``text`` is the cleaned-up markup
+ string and ``is_markup`` is True. Otherwise ``text`` is the XML-escaped
+ plain text and ``is_markup`` is False.
+ """
+ if not body:
+ return "", False
+
+ candidate = _IMG_RE.sub("", body)
+ try:
+ Pango.parse_markup(candidate, -1, "\0")
+ except GLib.Error:
+ return html_escape(body, quote=False), False
+ return candidate, True
diff --git a/sims/widgets/notification.py b/sims/widgets/notification.py
index be2b90e..5fcc888 100644
--- a/sims/widgets/notification.py
+++ b/sims/widgets/notification.py
@@ -6,6 +6,8 @@ from fabric.widgets.image import Image
from fabric.widgets.label import Label
from gi.repository import GdkPixbuf
+from sims.utils.markup import render_body_markup
+
NOTIFICATION_IMAGE_SIZE = 64
@@ -76,9 +78,11 @@ class NotificationWidget(Box):
)
if body:
+ body_text, body_is_markup = render_body_markup(body)
+ body_kwargs = {"markup": body_text} if body_is_markup else {"label": body_text}
text_children.append(
Label(
- label=body,
+ **body_kwargs,
line_wrap="word-char",
v_align="start",
h_align="start",
diff --git a/sims/widgets/notification_history_entry.py b/sims/widgets/notification_history_entry.py
index 7ce475a..f24ace0 100644
--- a/sims/widgets/notification_history_entry.py
+++ b/sims/widgets/notification_history_entry.py
@@ -7,6 +7,7 @@ from fabric.widgets.image import Image
from fabric.widgets.label import Label
from sims.services.notification_history import HistoryEntry
+from sims.utils.markup import render_body_markup
def _time_ago(ts: float, now: float | None = None) -> str:
@@ -71,9 +72,11 @@ class NotificationHistoryEntryWidget(Box):
text_children.append(header)
if entry.body:
+ body_text, body_is_markup = render_body_markup(entry.body)
+ body_kwargs = {"markup": body_text} if body_is_markup else {"label": body_text}
text_children.append(
Label(
- label=entry.body,
+ **body_kwargs,
line_wrap="word-char",
h_align="start",
v_align="start",