Merge branch 'wip/29-ms-breaks-plain-sigs' into 'master'

Fix plain text signatures losing their formatting when rendered by MS products

See merge request GNOME/geary!33
This commit is contained in:
Michael Gratton 2018-09-12 11:09:34 +00:00
commit f3ad1fe00d
5 changed files with 98 additions and 44 deletions

View file

@ -603,7 +603,7 @@ public class AddEditPage : Gtk.Box {
private void on_signature_stack_changed() {
if (signature_stack.visible_child_name == "preview_window")
preview_webview.load_html(Geary.HTML.smart_escape(email_signature, true), null);
preview_webview.load_html(Geary.HTML.smart_escape(email_signature), null);
}
private uint16 get_default_smtp_port() {

View file

@ -2150,7 +2150,7 @@ public class ComposerWidget : Gtk.EventBox {
}
account_sig = (!Geary.String.is_empty_or_whitespace(account_sig))
? Geary.HTML.smart_escape(account_sig, true)
? Geary.HTML.smart_escape(account_sig)
: "";
}

View file

@ -302,12 +302,14 @@ public string quote_email_for_forward(Geary.Email email, string? quote, TextForm
return quoted;
}
private string quote_body(Geary.Email email, string? quote, bool use_quotes, TextFormat format)
private string quote_body(Geary.Email email,
string? html_quote,
bool use_quotes,
TextFormat format)
throws Error {
Message? message = email.get_message();
bool preserve_whitespace = !message.has_html_body();
string? body_text = null;
if (quote == null) {
if (String.is_empty(html_quote)) {
switch (format) {
case TextFormat.HTML:
body_text = message.has_html_body()
@ -322,7 +324,7 @@ private string quote_body(Geary.Email email, string? quote, bool use_quotes, Tex
break;
}
} else {
body_text = Geary.HTML.smart_escape(quote, preserve_whitespace);
body_text = html_quote;
}
// Wrap the whole thing in a blockquote.

View file

@ -10,6 +10,8 @@ namespace Geary.HTML {
// Originally from here: http://daringfireball.net/2010/07/improved_regex_for_matching_urls
public const string URL_REGEX = "(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))";
private Regex WHITESPACE_REGEX;
private int init_count = 0;
private Gee.HashSet<string>? breaking_elements;
private Gee.HashSet<string>? spacing_elements;
@ -27,6 +29,12 @@ public void init() {
return;
init_element_sets();
try {
WHITESPACE_REGEX = new Regex("(\\R|\\t|[ ]+)");
} catch (GLib.Error err) {
assert(true);
}
}
private void init_element_sets() {
@ -95,20 +103,58 @@ private void init_element_sets() {
});
}
/** Converts plain text to HTML with reserved characters escaped. */
public inline string escape_markup(string? plain) {
return (!String.is_empty(plain) && plain.validate()) ? Markup.escape_text(plain) : "";
return (!String.is_empty(plain) && plain.validate())
? Markup.escape_text(plain) : "";
}
/** Converts plain text to HTML with whitespace (SP, CR, LF) preserved. */
public string preserve_whitespace(string? text) {
if (String.is_empty(text))
return "";
string output = text.replace(" ", "&nbsp;");
output = output.replace("\r\n", "<br />");
output = output.replace("\n", "<br />");
output = output.replace("\r", "<br />");
string preserved = "";
if (!String.is_empty(text)) {
try {
preserved = WHITESPACE_REGEX.replace_eval(
text, -1, 0, 0, (info, result) => {
string match = info.fetch(0);
if (match[0] == ' ') {
result.append_c(' ');
for (int len = match.length - 1; len > 0; len--) {
result.append("&nbsp;");
}
} else if (match == "\t") {
result.append(" &nbsp;&nbsp;&nbsp;");
} else {
result.append("<br>");
}
return false;
});
} catch (Error err) {
debug("Error preserving whitespace: %s", err.message);
}
}
return preserved;
}
return output;
/**
* Escape reserved HTML entities and preserves whitespace, if needed.
*
* Returns a string with reserved HTML entities escaped and
* whitespace preserved if the given string does not have HTML
* tags.
*/
public string smart_escape(string? text) {
string escaped = text ?? "";
if (text != null) {
bool is_html = Regex.match_simple(
"<[A-Z]+ ?(?: [^>]*)?\\/?>", text, RegexCompileFlags.CASELESS
);
if (!is_html) {
escaped = escape_markup(escaped);
escaped = preserve_whitespace(escaped);
}
}
return escaped;
}
/**
@ -168,22 +214,4 @@ private void recurse_html_nodes_for_text(Xml.Node? node,
}
}
// Escape reserved HTML entities if the string does not have HTML
// tags. If there are no tags, or if preserve_whitespace_in_html is
// true, wrap the string a div to preserve whitespace.
public string smart_escape(string? text, bool preserve_whitespace_in_html) {
if (text == null)
return text;
string res = text;
if (!Regex.match_simple("<[A-Z]*(?: [^>]*)?\\/?>", res,
RegexCompileFlags.CASELESS)) {
res = Geary.HTML.escape_markup(res);
preserve_whitespace_in_html = true;
}
if (preserve_whitespace_in_html)
res = @"<div style='white-space: pre-wrap;'>$res</div>";
return res;
}
}

View file

@ -9,6 +9,7 @@ class Geary.HTML.UtilTest : TestCase {
public UtilTest() {
base("Geary.HTML.Util");
add_test("preserve_whitespace", preserve_whitespace);
add_test("smart_escape_div", smart_escape_div);
add_test("smart_escape_no_closing_tag", smart_escape_no_closing_tag);
add_test("smart_escape_img", smart_escape_img);
@ -19,39 +20,62 @@ class Geary.HTML.UtilTest : TestCase {
add_test("remove_html_tags", remove_html_tags);
}
public void preserve_whitespace() throws GLib.Error {
assert_string("some text", Geary.HTML.smart_escape("some text"));
assert_string("some &nbsp;text", Geary.HTML.smart_escape("some text"));
assert_string("some &nbsp;&nbsp;text", Geary.HTML.smart_escape("some text"));
assert_string("some &nbsp;&nbsp;&nbsp;text", Geary.HTML.smart_escape("some\ttext"));
assert_string("some<br>text", Geary.HTML.smart_escape("some\ntext"));
assert_string("some<br>text", Geary.HTML.smart_escape("some\rtext"));
assert_string("some<br>text", Geary.HTML.smart_escape("some\r\ntext"));
assert_string("some<br><br>text", Geary.HTML.smart_escape("some\n\ntext"));
assert_string("some<br><br>text", Geary.HTML.smart_escape("some\r\rtext"));
assert_string("some<br><br>text", Geary.HTML.smart_escape("some\n\rtext"));
assert_string("some<br><br>text", Geary.HTML.smart_escape("some\r\n\r\ntext"));
}
public void smart_escape_div() throws Error {
string html = "<div>ohhai</div>";
assert(Geary.HTML.smart_escape(html, false) == html);
assert(Geary.HTML.smart_escape(html) == html);
}
public void smart_escape_no_closing_tag() throws Error {
string html = "<div>ohhai";
assert(Geary.HTML.smart_escape(html, false) == html);
assert(Geary.HTML.smart_escape(html) == html);
}
public void smart_escape_img() throws Error {
string html = "<img src=\"http://example.com/lol.gif\">";
assert(Geary.HTML.smart_escape(html, false) == html);
assert(Geary.HTML.smart_escape(html) == html);
}
public void smart_escape_xhtml_img() throws Error {
string html = "<img src=\"http://example.com/lol.gif\"/>";
assert(Geary.HTML.smart_escape(html, false) == html);
assert(Geary.HTML.smart_escape(html) == html);
}
public void smart_escape_mixed() throws Error {
string html = "mixed <div>ohhai</div> text";
assert(Geary.HTML.smart_escape(html, false) == html);
assert(Geary.HTML.smart_escape(html) == html);
}
public void smart_escape_text() throws Error {
string text = "some text";
assert(Geary.HTML.smart_escape(text, false) == "<div style='white-space: pre-wrap;'>some text</div>");
public void smart_escape_text() throws GLib.Error {
assert_string("some text", Geary.HTML.smart_escape("some text"));
assert_string("&lt;some text", Geary.HTML.smart_escape("<some text"));
assert_string("some text&gt;", Geary.HTML.smart_escape("some text>"));
}
public void smart_escape_text_url() throws Error {
string text = "<http://example.com>";
assert(Geary.HTML.smart_escape(text, false) == "<div style='white-space: pre-wrap;'>&lt;http://example.com&gt;</div>");
public void smart_escape_text_url() throws GLib.Error {
assert_string(
"&lt;http://example.com&gt;",
Geary.HTML.smart_escape("<http://example.com>")
);
assert_string(
"&lt;http://example.com&gt;",
Geary.HTML.smart_escape("<http://example.com>")
);
}
public void remove_html_tags() throws Error {