From 431ebcb35f9eb1a295f02221fdd6127d6748e964 Mon Sep 17 00:00:00 2001 From: Michael James Gratton Date: Sun, 27 Nov 2016 21:03:52 +1100 Subject: [PATCH] Re-implement message HTML cleaning in JS in the web extension for WK2. * ui/conversation-web-view.js: New script, port old HTML cleaning code in vala to Javascript as new subclass of PageState. Instantiate that on page load. * src/client/conversation-viewer/conversation-web-view.vala (ConversationWebView): Load and add new JS script for conversations. * src/client/web-process/util-conversation.vala (Util.Conversation): Remove migrated and obsolete code. * ui/client-web-view.js (PageState): Allow on-load behaviour to be overridden in subclasses. * ui/CMakeLists.txt: Include new JS script. * ui/conversation-web-view.css: Chase CSS class name changes. --- src/client/application/geary-controller.vala | 2 +- .../conversation-web-view.vala | 13 +- src/client/web-process/util-conversation.vala | 186 ------------------ ui/CMakeLists.txt | 1 + ui/client-web-view.js | 12 +- ui/conversation-web-view.css | 35 ++-- ui/conversation-web-view.js | 136 +++++++++++++ 7 files changed, 169 insertions(+), 216 deletions(-) create mode 100644 ui/conversation-web-view.js diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala index d4e36eb0..6ec675e3 100644 --- a/src/client/application/geary-controller.vala +++ b/src/client/application/geary-controller.vala @@ -210,7 +210,7 @@ public class GearyController : Geary.BaseObject { // Load web view resources try { ClientWebView.load_scripts(this.application); - ConversationWebView.load_stylehseets(this.application); + ConversationWebView.load_resources(this.application); } catch (Error err) { error("Error loading web resources: %s", err.message); } diff --git a/src/client/conversation-viewer/conversation-web-view.vala b/src/client/conversation-viewer/conversation-web-view.vala index f1124449..bbfc9514 100644 --- a/src/client/conversation-viewer/conversation-web-view.vala +++ b/src/client/conversation-viewer/conversation-web-view.vala @@ -12,9 +12,12 @@ public class ConversationWebView : ClientWebView { private static WebKit.UserStyleSheet? user_stylesheet = null; private static WebKit.UserStyleSheet? app_stylesheet = null; + private static WebKit.UserScript? app_script = null; - public static void load_stylehseets(GearyApplication app) + public static void load_resources(GearyApplication app) throws Error { + ConversationWebView.app_script = + ClientWebView.load_app_script(app, "conversation-web-view.js"); ConversationWebView.app_stylesheet = ClientWebView.load_app_stylesheet(app, "conversation-web-view.css"); ConversationWebView.user_stylesheet = @@ -23,12 +26,12 @@ public class ConversationWebView : ClientWebView { public ConversationWebView() { - WebKit.UserContentManager manager = new WebKit.UserContentManager(); - manager.add_style_sheet(ConversationWebView.app_stylesheet); + base(); + this.user_content_manager.add_script(ConversationWebView.app_script); + this.user_content_manager.add_style_sheet(ConversationWebView.app_stylesheet); if (ConversationWebView.user_stylesheet != null) { - manager.add_style_sheet(ConversationWebView.user_stylesheet); + this.user_content_manager.add_style_sheet(ConversationWebView.user_stylesheet); } - base(manager); } public void clean_and_load(string html) { diff --git a/src/client/web-process/util-conversation.vala b/src/client/web-process/util-conversation.vala index 474d515e..04c95e83 100644 --- a/src/client/web-process/util-conversation.vala +++ b/src/client/web-process/util-conversation.vala @@ -8,143 +8,12 @@ namespace Util.Conversation { - private const string SIGNATURE_CONTAINER_CLASS = "geary_signature"; - private const string QUOTE_CONTAINER_CLASS = "geary_quote_container"; private const string QUOTE_CONTROLLABLE_CLASS = "controllable"; private const string QUOTE_HIDE_CLASS = "hide"; private const float QUOTE_SIZE_THRESHOLD = 2.0f; - public double get_preferred_height(WebKit.WebPage page) { - WebKit.DOM.Element html = page.get_dom_document().get_document_element(); - double offset_height = html.offset_height; - double offset_width = html.offset_width; - double px = offset_width * offset_height; - - const double MAX_LEN = 15.0 * 1000; - const double MAX_PX = 10.0 * 1000 * 1000; - - // If the offset_width is very small, the offset_height will - // likely be bogus, so just pretend we have no height for the - // moment. WebKitGTK seems to report an offset width of 1 in - // these cases. - if (offset_width > 1) { - if (offset_height > MAX_LEN || px > MAX_PX) { - double new_height = double.min(MAX_LEN, MAX_PX / offset_width); - debug("Clamping window height to: %f, current size: %fx%f (%fpx)", - new_height, offset_width, offset_height, px); - offset_height = new_height; - } - } else { - offset_height = 0; - } - - return offset_height; - } - - public string clean_html_markup(WebKit.WebPage page, string text, Geary.RFC822.Message message) { - try { - WebKit.DOM.HTMLElement html = (WebKit.DOM.HTMLElement) - page.get_dom_document().document_element; - - // If the message has a HTML element, get its inner - // markup. We can't just set this on a temp container div - // (the old approach) using set_inner_html() will refuse - // to parse any HTML, HEAD and BODY elements that are out - // of place in the structure. We can't use - // set_outer_html() on the document element since it - // throws an error. - GLib.Regex html_regex = new GLib.Regex("]*)>(.*)", - GLib.RegexCompileFlags.DOTALL); - GLib.MatchInfo matches; - if (html_regex.match(text, 0, out matches)) { - // Set the existing HTML element's content. Here, HEAD - // and BODY elements will be parsed fine. - html.set_inner_html(matches.fetch(2)); - // Copy email HTML element attrs across to the - // existing HTML element - string attrs = matches.fetch(1); - if (attrs != "") { - WebKit.DOM.HTMLElement container = create(page, "div"); - container.set_inner_html(@""); - WebKit.DOM.HTMLElement? attr_element = - Util.DOM.select(container, "div"); - WebKit.DOM.NamedNodeMap html_attrs = - attr_element.get_attributes(); - for (int i = 0; i < html_attrs.get_length(); i++) { - WebKit.DOM.Node attr = html_attrs.item(i); - html.set_attribute(attr.node_name, attr.text_content); - } - } - } else { - html.set_inner_html(text); - } - - // Set dir="auto" if not already set possibly get a - // slightly better RTL experience. - string? dir = html.get_dir(); - if (dir == null || dir.length == 0) { - html.set_dir("auto"); - } - - // Get all the top level block quotes and stick them into a hide/show controller. - WebKit.DOM.NodeList blockquote_list = html.query_selector_all("blockquote"); - for (int i = 0; i < blockquote_list.length; ++i) { - // Get the nodes we need. - WebKit.DOM.Node blockquote_node = blockquote_list.item(i); - WebKit.DOM.Node? next_sibling = blockquote_node.get_next_sibling(); - WebKit.DOM.Node parent = blockquote_node.get_parent_node(); - - // Make sure this is a top level blockquote. - if (Util.DOM.node_is_child_of(blockquote_node, "BLOCKQUOTE")) { - continue; - } - - WebKit.DOM.Element quote_container = create_quote_container(page); - Util.DOM.select(quote_container, ".quote").append_child(blockquote_node); - if (next_sibling == null) { - parent.append_child(quote_container); - } else { - parent.insert_before(quote_container, next_sibling); - } - } - - // Now look for the signature. - wrap_html_signature(page, ref html); - - // Now return the whole message. - return html.get_outer_html(); - } catch (Error e) { - debug("Error modifying HTML message: %s", e.message); - return text; - } - } - - public void unset_controllable_quotes(WebKit.WebPage page) - throws Error { - WebKit.DOM.HTMLElement html = - page.get_dom_document().document_element as WebKit.DOM.HTMLElement; - if (html != null) { - WebKit.DOM.NodeList quote_list = html.query_selector_all( - ".%s.%s".printf(QUOTE_CONTAINER_CLASS, QUOTE_CONTROLLABLE_CLASS) - ); - for (int i = 0; i < quote_list.length; ++i) { - WebKit.DOM.Element quote_container = quote_list.item(i) as WebKit.DOM.Element; - double outer_client_height = quote_container.client_height; - long scroll_height = quote_container.query_selector(".quote").scroll_height; - // If the message is hidden, scroll_height will be - // 0. Otherwise, unhide the full quote if there is not a - // substantial amount hidden. - if (scroll_height > 0 && - scroll_height <= outer_client_height * QUOTE_SIZE_THRESHOLD) { - //quote_container.class_list.remove(QUOTE_CONTROLLABLE_CLASS); - //quote_container.class_list.remove(QUOTE_HIDE_CLASS); - } - } - } - } - public string? get_selection_for_quoting(WebKit.WebPage page) { string? quote = null; // WebKit.DOM.Document document = page.get_dom_document(); @@ -206,59 +75,4 @@ namespace Util.Conversation { return value; } - private WebKit.DOM.HTMLElement create(WebKit.WebPage page, string name) - throws Error { - return page.get_dom_document().create_element(name) as WebKit.DOM.HTMLElement; - } - - private WebKit.DOM.HTMLElement create_quote_container(WebKit.WebPage page) throws Error { - WebKit.DOM.HTMLElement quote_container = create(page, "div"); - // quote_container.class_list.add(QUOTE_CONTAINER_CLASS); - // quote_container.class_list.add(QUOTE_CONTROLLABLE_CLASS); - // quote_container.class_list.add(QUOTE_HIDE_CLASS); - // New lines are preserved within blockquotes, so this string - // needs to be new-line free. - quote_container.set_inner_html("""
"""); - return quote_container; - } - - private void wrap_html_signature(WebKit.WebPage page, ref WebKit.DOM.HTMLElement container) throws Error { - // Most HTML signatures fall into one of these designs which are handled by this method: - // - // 1. GMail:
--
$SIGNATURE - // 2. GMail Alternate:
--
$SIGNATURE - // 3. Thunderbird:
--
$SIGNATURE
- // - WebKit.DOM.NodeList div_list = container.query_selector_all("div,span,p"); - int i = 0; - Regex sig_regex = new Regex("^--\\s*$"); - Regex alternate_sig_regex = new Regex("^--\\s*(?: .quote { + .geary-quote-container > .geary-quote { position: relative; padding: 0; border: 0; @@ -114,18 +114,18 @@ pre { overflow: hidden; z-index: 0; } - .geary_quote_container.controllable.hide > .quote { + .geary-quote-container.geary-controllable.geary-hide > .geary-quote { /* Use a fraction value to cut the last visible line off half way. */ max-height: 7.75em; } - .geary_quote_container.controllable > .quote > blockquote { + .geary-quote-container.geary-controllable > .geary-quote > blockquote { /* Add space between the quote and the hider button */ margin-bottom: 18px; } - .geary_quote_container > .shower, - .geary_quote_container > .hider { + .geary-quote-container > .geary-shower, + .geary-quote-container > .geary-hider { position: absolute; display: none; left: 0; @@ -136,24 +136,25 @@ pre { -webkit-user-drag: none; } - .geary_quote_container > .shower > input, - .geary_quote_container > .hider > input { + .geary-quote-container > .geary-shower > input, + .geary-quote-container > .geary-hider > input { + display: block; width: 100%; height: 16px; padding: 0; font-size: 8px; /* Absolute size in pixels for graphics */ color: #888; } - .geary_quote_container > .shower:hover > input, - .geary_quote_container > .hider:hover > input { + .geary-quote-container > .geary-shower:hover > input, + .geary-quote-container > .geary-hider:hover > input { color: #000; } - .geary_quote_container.controllable.hide > .hider { + .geary-quote-container.geary-controllable.geary-hide > .geary-hider { display: none; } - .geary_quote_container.controllable.hide > .shower, - .geary_quote_container.controllable > .hider { + .geary-quote-container.geary-controllable.geary-hide > .geary-shower, + .geary-quote-container.geary-controllable > .geary-hider { display: block; } diff --git a/ui/conversation-web-view.js b/ui/conversation-web-view.js new file mode 100644 index 00000000..15db7fbc --- /dev/null +++ b/ui/conversation-web-view.js @@ -0,0 +1,136 @@ +/* + * Copyright 2016 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Application logic for ConversationWebView. + */ +var ConversationPageState = function() { + this.init.apply(this, arguments); +}; +ConversationPageState.prototype = { + __proto__: PageState.prototype, + init: function() { + PageState.prototype.init.apply(this, []); + }, + loaded: function() { + this.updateDirection(); + this.createControllableQuotes(); + this.wrapSignature(); + // Call after so we continue to a preferred size update after + // munging the HTML above. + PageState.prototype.loaded.apply(this, []); + }, + /** + * Set dir="auto" if not already set. + * + * This should provide a slightly better RTL experience. + */ + updateDirection: function() { + var dir = document.documentElement.dir; + if (dir == null || dir.trim() == "") { + document.documentElement.dir = "auto"; + } + }, + /** + * Add top level blockquotes to hide/show container. + */ + createControllableQuotes: function() { + var blockquoteList = document.documentElement.querySelectorAll("blockquote"); + for (var i = 0; i < blockquoteList.length; ++i) { + var blockquote = blockquoteList.item(i); + var nextSibling = blockquote.nextSibling; + var parent = blockquote.parentNode; + + // Only insert into a quote container if the element is a + // top level blockquote + if (!ConversationPageState.isDescendantOf(blockquote, "BLOCKQUOTE")) { + var quoteContainer = document.createElement("DIV"); + quoteContainer.classList.add("geary-quote-container"); + + // Only make it controllable if the quote is tall enough + if (blockquote.offsetHeight > 50) { + quoteContainer.classList.add("geary-controllable"); + quoteContainer.classList.add("geary-hide"); + } + // New lines are preserved within blockquotes, so this + // string needs to be new-line free. + quoteContainer.innerHTML = + "
" + + "" + + "
" + + "
" + + "" + + "
"; + + var quoteDiv = document.createElement("DIV"); + quoteDiv.classList.add("geary-quote"); + quoteDiv.appendChild(blockquote); + + quoteContainer.appendChild(quoteDiv); + parent.insertBefore(quoteContainer, nextSibling); + } + } + }, + /** + * Look for and wrap a signature. + * + * Most HTML signatures fall into one + * of these designs which are handled by this method: + * + * 1. GMail:
--
$SIGNATURE + * 2. GMail Alternate:
--
$SIGNATURE + * 3. Thunderbird:
--
$SIGNATURE
+ * + */ + wrapSignature: function() { + var possibleSigs = document.documentElement.querySelectorAll("div,span,p"); + var i = 0; + var sigRegex = new RegExp("^--\\s*$"); + var alternateSigRegex = new RegExp("^--\\s*(?: