Browse Source

message-row: Add support for preformatted HTML element

When it is not a code block.
fractal-13
Kévin Commaille 5 months ago
parent
commit
5de2080b55
No known key found for this signature in database
GPG Key ID: F26F4BE20A08255B
  1. 31
      src/session_view/room_history/message_row/text/inline_html.rs
  2. 1
      src/session_view/room_history/message_row/text/mod.rs
  3. 42
      src/session_view/room_history/message_row/text/tests.rs
  4. 81
      src/session_view/room_history/message_row/text/widgets.rs

31
src/session_view/room_history/message_row/text/inline_html.rs

@ -23,6 +23,8 @@ pub(super) struct InlineHtmlBuilder<'a> {
single_line: bool,
/// Whether to append an ellipsis at the end of the string.
ellipsis: bool,
/// Whether whitespace should be preserved.
preserve_whitespace: bool,
/// The mentions detection setting and results.
mentions: MentionsMode<'a>,
/// The inner string.
@ -39,12 +41,16 @@ impl<'a> InlineHtmlBuilder<'a> {
/// If `single_line` is set to `true`, the string will be ellipsized at the
/// first line break.
///
/// If `ellipsis` is set to `true`, and ellipsis will be added at the end of
/// If `ellipsis` is set to `true`, an ellipsis will be added at the end of
/// the string.
pub(super) fn new(single_line: bool, ellipsis: bool) -> Self {
///
/// If `preserve_whitespace` is set to `true`, all whitespace will be
/// preserved, otherwise it will be collapsed according to the HTML spec.
pub(super) fn new(single_line: bool, ellipsis: bool, preserve_whitespace: bool) -> Self {
Self {
single_line,
ellipsis,
preserve_whitespace,
mentions: MentionsMode::default(),
inner: String::new(),
truncated: false,
@ -82,7 +88,7 @@ impl<'a> InlineHtmlBuilder<'a> {
if ellipsis {
inner.append_ellipsis();
} else {
} else if !self.preserve_whitespace {
inner.truncate_end_whitespaces();
}
@ -207,8 +213,11 @@ impl<'a> InlineHtmlBuilder<'a> {
if self.single_line {
self.truncated = true;
} else {
// Remove whitespaces before the newline.
self.inner.truncate_end_whitespaces();
if !self.preserve_whitespace {
// Remove whitespaces before the newline.
self.inner.truncate_end_whitespaces();
}
self.inner.push('\n');
}
}
@ -226,10 +235,14 @@ impl<'a> InlineHtmlBuilder<'a> {
fn append_text_node(&mut self, text: &str, context: NodeContext) {
// Collapse whitespaces and remove them at the beginning and end of an HTML
// element, and after a newline.
let text = text.collapse_whitespaces(
context.is_first_child || self.inner.ends_with('\n'),
context.is_last_child,
);
let text = if self.preserve_whitespace {
text.to_owned()
} else {
text.collapse_whitespaces(
context.is_first_child || self.inner.ends_with('\n'),
context.is_last_child,
)
};
if context.should_linkify {
if let MentionsMode::WithMentions {

1
src/session_view/room_history/message_row/text/mod.rs

@ -322,6 +322,7 @@ mod imp {
room,
detect_at_room,
ellipsize,
is_preformatted: false,
},
false,
&mut sender_name,

42
src/session_view/room_history/message_row/text/tests.rs

@ -5,7 +5,7 @@ use super::inline_html::InlineHtmlBuilder;
#[test]
fn text_with_no_markup() {
let html = Html::parse("A simple text");
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(s, "A simple text");
assert!(pills.is_none());
@ -14,13 +14,13 @@ fn text_with_no_markup() {
#[test]
fn single_line() {
let html = Html::parse("A simple text<br>on several lines");
let (s, pills) = InlineHtmlBuilder::new(true, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(true, false, false).build_with_nodes(html.children());
assert_eq!(s, "A simple text…");
assert!(pills.is_none());
let html = Html::parse("\nThis is a paragraph<br />\n\nThis is another paragraph\n");
let (s, pills) = InlineHtmlBuilder::new(true, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(true, false, false).build_with_nodes(html.children());
assert_eq!(s, "This is a paragraph…");
assert!(pills.is_none());
@ -29,7 +29,7 @@ fn single_line() {
#[test]
fn add_ellipsis() {
let html = Html::parse("A simple text");
let (s, pills) = InlineHtmlBuilder::new(false, true).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, true, false).build_with_nodes(html.children());
assert_eq!(s, "A simple text…");
assert!(pills.is_none());
@ -38,7 +38,7 @@ fn add_ellipsis() {
#[test]
fn no_duplicate_ellipsis() {
let html = Html::parse("A simple text...<br>...on several lines");
let (s, pills) = InlineHtmlBuilder::new(true, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(true, false, false).build_with_nodes(html.children());
assert_eq!(s, "A simple text...");
assert!(pills.is_none());
@ -47,7 +47,7 @@ fn no_duplicate_ellipsis() {
#[test]
fn trim_end_spaces() {
let html = Html::parse("A high-altitude text 🗻 ");
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(s, "A high-altitude text 🗻");
assert!(pills.is_none());
@ -55,17 +55,27 @@ fn trim_end_spaces() {
#[test]
fn collapse_whitespace() {
let html = Html::parse("Hello \nyou! \nYou are <b>my \nfriend</b>.");
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let original = "Hello \nyou! \nYou are <b>my \nfriend</b>.";
let html = Html::parse(original);
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(s, "Hello you! You are <b>my friend</b>.");
assert!(pills.is_none());
let html = Html::parse(" Hello \nyou! \n\nYou are \n<b> my \nfriend </b>. ");
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, false, true).build_with_nodes(html.children());
assert_eq!(s, original);
assert!(pills.is_none());
let original = " Hello \nyou! \n\nYou are \n<b> my \nfriend </b>. ";
let html = Html::parse(original);
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(s, "Hello you! You are <b>my friend</b>.");
assert!(pills.is_none());
let (s, pills) = InlineHtmlBuilder::new(false, false, true).build_with_nodes(html.children());
assert_eq!(s, original);
assert!(pills.is_none());
}
#[test]
@ -73,7 +83,7 @@ fn sanitize_inline_html() {
let html = Html::parse(
r#"A <strong>text</strong> with <a href="https://docs.local/markup"><i>markup</i></a>"#,
);
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(
s,
@ -87,7 +97,7 @@ fn escape_markup() {
let html = Html::parse(
r#"Go to <a href="https://docs.local?this=this&that=that">this &amp; that docs</a>"#,
);
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(
s,
@ -101,7 +111,7 @@ fn linkify() {
let html = Html::parse(
"The homepage is https://gnome.org, and you can contact me at contact@me.local",
);
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(
s,
@ -113,7 +123,7 @@ fn linkify() {
#[test]
fn do_not_linkify_inside_anchor() {
let html = Html::parse(r#"The homepage is <a href="https://gnome.org">https://gnome.org</a>"#);
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(
s,
@ -125,7 +135,7 @@ fn do_not_linkify_inside_anchor() {
#[test]
fn do_not_linkify_inside_code() {
let html = Html::parse("The homepage is <code>https://gnome.org</code>");
let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
let (s, pills) = InlineHtmlBuilder::new(false, false, false).build_with_nodes(html.children());
assert_eq!(s, "The homepage is <tt>https://gnome.org</tt>");
assert!(pills.is_none());
@ -134,7 +144,7 @@ fn do_not_linkify_inside_code() {
#[test]
fn emote_name() {
let html = Html::parse("sent a beautiful picture.");
let (s, pills) = InlineHtmlBuilder::new(false, false)
let (s, pills) = InlineHtmlBuilder::new(false, false, false)
.append_emote_with_name(&mut Some("Jun"))
.build_with_nodes(html.children());

81
src/session_view/room_history/message_row/text/widgets.rs

@ -19,9 +19,19 @@ use crate::{
/// The immutable config fields to build a HTML widget tree.
#[derive(Debug, Clone, Copy)]
pub(super) struct HtmlWidgetConfig<'a> {
/// The room where the message constructed by this widget was sent.
///
/// Used for generating mentions.
pub(super) room: &'a Room,
/// Whether we should try to detect an `@room` mention in the HTML to
/// render.
pub(super) detect_at_room: bool,
/// Whether to ellipsize the message.
pub(super) ellipsize: bool,
/// Whether this is preformatted text.
///
/// Whitespaces are untouched in preformatted text.
pub(super) is_preformatted: bool,
}
/// Construct a new label for displaying a message's content.
@ -85,7 +95,7 @@ pub(super) fn widget_for_html_nodes(
// Include sender name before, if the child widget did not handle it.
if let Some(sender_name) = sender_name.take() {
let label = new_message_label();
let (text, _) = InlineHtmlBuilder::new(false, false)
let (text, _) = InlineHtmlBuilder::new(false, false, config.is_preformatted)
.append_emote_with_name(&mut Some(sender_name))
.build();
label.set_label(&text);
@ -174,10 +184,11 @@ fn label_for_inline_html(
add_ellipsis: bool,
sender_name: &mut Option<&str>,
) -> Option<gtk::Widget> {
let (text, widgets) = InlineHtmlBuilder::new(config.ellipsize, add_ellipsis)
.detect_mentions(config.room, config.detect_at_room)
.append_emote_with_name(sender_name)
.build_with_nodes(nodes);
let (text, widgets) =
InlineHtmlBuilder::new(config.ellipsize, add_ellipsis, config.is_preformatted)
.detect_mentions(config.room, config.detect_at_room)
.append_emote_with_name(sender_name)
.build_with_nodes(nodes);
if text.is_empty() {
return None;
@ -242,7 +253,7 @@ fn widget_for_html_block(
}
MatrixElement::Hr => gtk::Separator::new(gtk::Orientation::Horizontal).upcast(),
MatrixElement::Pre => {
widget_for_preformatted_text(node.children(), config.ellipsize, add_ellipsis)?
widget_for_preformatted_text(node.children(), config, add_ellipsis, sender_name)?
}
MatrixElement::Details => widget_for_details(node.children(), config, add_ellipsis)?,
element => {
@ -350,8 +361,9 @@ impl From<OrderedListData> for ListType {
/// Create a widget for preformatted text.
fn widget_for_preformatted_text(
children: Children,
ellipsize: bool,
config: HtmlWidgetConfig<'_>,
add_ellipsis: bool,
sender_name: &mut Option<&str>,
) -> Option<gtk::Widget> {
let children = children.collect::<Vec<_>>();
@ -361,37 +373,45 @@ fn widget_for_preformatted_text(
let unique_code_child = (children.len() == 1)
.then_some(&children[0])
.and_then(|child| child.as_element())
.and_then(|element| match element.to_matrix().element {
MatrixElement::Code(code) => Some(code),
_ => None,
.and_then(|child| {
match child
.as_element()
.map(|element| element.to_matrix().element)
{
Some(MatrixElement::Code(code)) => Some((child, code)),
_ => None,
}
});
let (children, code_language) = if let Some(code) = unique_code_child {
let children = children[0].children().collect::<Vec<_>>();
let Some((child, code)) = unique_code_child else {
// This is just preformatted text, we need to construct the children hierarchy.
let config = HtmlWidgetConfig {
is_preformatted: true,
..config
};
let widget = widget_for_html_nodes(children, config, add_ellipsis, sender_name)?;
if children.is_empty() {
return None;
}
// We use the monospace font for preformatted text.
widget.add_css_class("monospace");
(children, code.language)
} else {
(children, None)
return Some(widget);
};
let text = InlineHtmlBuilder::new(ellipsize, add_ellipsis).build_with_nodes_text(children);
let children = child.children().collect::<Vec<_>>();
if ellipsize {
// Present text as inline code.
let text = format!("<tt>{}</tt>", text.escape_markup());
if children.is_empty() {
return None;
}
let text = InlineHtmlBuilder::new(config.ellipsize, add_ellipsis, config.is_preformatted)
.build_with_nodes_text(children);
if config.ellipsize {
// Present text as inline code.
let label = new_message_label();
label.set_ellipsize(if ellipsize {
pango::EllipsizeMode::End
} else {
pango::EllipsizeMode::None
});
label.set_label(&text);
label.set_ellipsize(pango::EllipsizeMode::End);
label.add_css_class("monospace");
label.set_label(&text.escape_markup());
return Some(label.upcast());
}
@ -402,7 +422,8 @@ fn widget_for_preformatted_text(
.build();
crate::utils::sourceview::setup_style_scheme(&buffer);
let language = code_language
let language = code
.language
.and_then(|lang| sourceview::LanguageManager::default().language(lang.as_ref()));
buffer.set_language(language.as_ref());

Loading…
Cancel
Save