diff --git a/src/session_view/room_history/message_row/text/inline_html.rs b/src/session_view/room_history/message_row/text/inline_html.rs
index 8e97f491..f305b75e 100644
--- a/src/session_view/room_history/message_row/text/inline_html.rs
+++ b/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 {
diff --git a/src/session_view/room_history/message_row/text/mod.rs b/src/session_view/room_history/message_row/text/mod.rs
index 7b13b289..94dc0a1d 100644
--- a/src/session_view/room_history/message_row/text/mod.rs
+++ b/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,
diff --git a/src/session_view/room_history/message_row/text/tests.rs b/src/session_view/room_history/message_row/text/tests.rs
index f14d34f2..4016f14f 100644
--- a/src/session_view/room_history/message_row/text/tests.rs
+++ b/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
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
\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...
...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 my \nfriend.");
- let (s, pills) = InlineHtmlBuilder::new(false, false).build_with_nodes(html.children());
+ let original = "Hello \nyou! \nYou are my \nfriend.";
+ 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 my friend.");
assert!(pills.is_none());
- let html = Html::parse(" Hello \nyou! \n\nYou are \n my \nfriend . ");
- 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 my \nfriend . ";
+ 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 my friend.");
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 text with markup"#,
);
- 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 this & that docs"#,
);
- 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 https://gnome.org"#);
- 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 https://gnome.org");
- 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 https://gnome.org");
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());
diff --git a/src/session_view/room_history/message_row/text/widgets.rs b/src/session_view/room_history/message_row/text/widgets.rs
index ff7e89cf..dcefb8ac 100644
--- a/src/session_view/room_history/message_row/text/widgets.rs
+++ b/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 {
- 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 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 {
let children = children.collect::>();
@@ -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::>();
+ 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::>();
- if ellipsize {
- // Present text as inline code.
- let text = format!("{}", 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());