Skip to content

Commit e351645

Browse files
authored
feat: render parsed HTML as rich text
1 parent 6d05f2e commit e351645

4 files changed

Lines changed: 96 additions & 4 deletions

File tree

cosmic-notifications-util/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = fa
1515
serde = { version = "1.0", features = ["derive"] }
1616
zbus = { version = "5.11.0", optional = true }
1717
fast_image_resize = { version = "5.1.4", optional = true }
18+
tl = { version = "0.7.8" }
1819
tracing = "0.1.41"
1920
url = "2.5.7"

cosmic-notifications-util/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ pub mod image;
33
#[cfg(feature = "image")]
44
pub use image::*;
55

6-
use cosmic::widget::{Icon, icon};
6+
pub mod markup;
7+
8+
use cosmic::widget::{icon, Icon};
79
use serde::{Deserialize, Serialize};
810
use std::{
911
collections::HashMap, convert::Infallible, fmt, path::PathBuf, str::FromStr, time::SystemTime,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use cosmic::{
2+
cosmic_theme,
3+
iced::{
4+
Font,
5+
font::{Style, Weight},
6+
},
7+
iced_core::text::Span,
8+
};
9+
10+
// Handle break lines, etc. in the future
11+
// Used only in `parse_html` function
12+
fn _prepare_html(text: &str) -> String {
13+
let text = text
14+
// handle break lines
15+
.replace("<br>", "\n")
16+
.replace("<br/>", "\n")
17+
.replace("<br />", "\n");
18+
19+
text.to_owned()
20+
}
21+
22+
// Sanitize only tags allowed by Freedesktop Notification Specifications
23+
// https://specifications.freedesktop.org/notification/1.2/markup.html
24+
// TODO: impl <img> tag handling
25+
fn sanitize_html(tags: &[String], content: &str) -> Span<'static> {
26+
let mut font = Font::default();
27+
let mut span = Span::new(content.to_owned());
28+
29+
for tag in tags {
30+
match tag.as_str() {
31+
"b" => font.weight = Weight::Bold,
32+
"i" => font.style = Style::Italic,
33+
"u" => span = span.underline(true),
34+
"a" => {
35+
let theme = cosmic_theme::Theme::preferred_theme();
36+
span = span.underline(true).color(theme.accent_text_color());
37+
}
38+
_ => {}
39+
}
40+
}
41+
42+
span.font(font)
43+
}
44+
45+
fn _handle_recursive(
46+
handle: &tl::NodeHandle,
47+
parser: &tl::Parser,
48+
tags: &mut Vec<String>,
49+
buffer: &mut Vec<Span<'static>>,
50+
) {
51+
if let Some(node) = handle.get(parser) {
52+
match node {
53+
tl::Node::Tag(tag) => {
54+
let tag_name = tag.name().as_utf8_str();
55+
tags.push(tag_name.into_owned());
56+
57+
tag.children().top().iter().for_each(|t| {
58+
_handle_recursive(t, parser, tags, buffer);
59+
});
60+
61+
tags.pop();
62+
}
63+
tl::Node::Raw(bytes) => {
64+
buffer.push(sanitize_html(tags, &bytes.as_utf8_str()));
65+
}
66+
_ => {}
67+
}
68+
}
69+
}
70+
71+
pub fn html_to_spans(text: &str) -> Vec<Span<'static>> {
72+
let mut buffer = Vec::new();
73+
let html = _prepare_html(text);
74+
let dom = tl::parse(&html, tl::ParserOptions::default());
75+
76+
if let Ok(vdom) = dom {
77+
let parser = vdom.parser();
78+
let elements = vdom.children();
79+
let mut tags = Vec::new();
80+
81+
for node_handle in elements {
82+
_handle_recursive(node_handle, parser, &mut tags, &mut buffer);
83+
}
84+
}
85+
86+
buffer
87+
}

src/app.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ use cosmic::iced::platform_specific::shell::wayland::commands::{
1010
};
1111
use cosmic::iced::{self, Length, Limits, Subscription};
1212
use cosmic::iced_runtime::core::window::Id as SurfaceId;
13-
use cosmic::iced_widget::{column, row, vertical_space};
13+
use cosmic::iced_widget::{column, rich_text, row, vertical_space};
1414
use cosmic::surface;
1515
use cosmic::widget::{autosize, button, container, icon, text};
1616
use cosmic::{Application, Element, app::Task};
1717
use cosmic_notifications_config::NotificationsConfig;
18+
use cosmic_notifications_util::markup::html_to_spans;
1819
use cosmic_notifications_util::{ActionId, CloseReason, Notification};
1920
use cosmic_panel_config::{CosmicPanelConfig, CosmicPanelOuput, PanelAnchor};
2021
use cosmic_time::{Instant, Timeline, anim, id};
@@ -602,6 +603,7 @@ impl cosmic::Application for CosmicNotifications {
602603
)
603604
.on_press(Message::Dismissed(n.id))
604605
.class(cosmic::theme::Button::Text);
606+
605607
let e = Element::from(
606608
column!(
607609
if let Some(icon) = n.notification_icon() {
@@ -616,8 +618,8 @@ impl cosmic::Application for CosmicNotifications {
616618
column![
617619
text::body(n.summary.lines().next().unwrap_or_default())
618620
.width(Length::Fill),
619-
text::caption(n.body.lines().next().unwrap_or_default())
620-
.width(Length::Fill)
621+
Element::from(rich_text(html_to_spans(&n.body)).size(12.0))
622+
.map(|_: ()| Message::Ignore)
621623
]
622624
)
623625
.width(Length::Fill),

0 commit comments

Comments
 (0)