[geary/wip/728002-webkit2: 41/96] Re-implement message HTML cleaning in JS in the web extension for WK2.



commit caf126a15aadbe2ab6617e9251c185ccc989367f
Author: Michael James Gratton <mike vee net>
Date:   Sun Nov 27 21:03:52 2016 +1100

    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-viewer/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(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index d4e36eb..6ec675e 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 f112444..bbfc951 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 474d515..04c95e8 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("<html([^>]*)>(.*)</html>",
-                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(@"<div$attrs></div>");
-                    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("""<div class="shower"><input type="button" value="▼        ▼        
▼" /></div><div class="hider"><input type="button" value="▲        ▲        ▲" /></div><div 
class="quote"></div>""");
-        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:            <div>-- </div>$SIGNATURE
-        // 2. GMail Alternate:  <div><span>-- </span></div>$SIGNATURE
-        // 3. Thunderbird:      <div>-- <br>$SIGNATURE</div>
-        //
-        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*(?:<br|\\R)");
-        for (; i < div_list.length; ++i) {
-            // Get the div and check that it starts a signature block and is not inside a quote.
-            WebKit.DOM.HTMLElement div = div_list.item(i) as WebKit.DOM.HTMLElement;
-            string inner_html = div.get_inner_html();
-            if ((sig_regex.match(inner_html) || alternate_sig_regex.match(inner_html)) &&
-                !Util.DOM.node_is_child_of(div, "BLOCKQUOTE")) {
-                break;
-            }
-        }
-
-        // If we have a signature, move it and all of its following siblings that are not quotes
-        // inside a signature div.
-        if (i == div_list.length) {
-            return;
-        }
-        WebKit.DOM.Node elem = div_list.item(i) as WebKit.DOM.Node;
-        WebKit.DOM.Element parent = elem.get_parent_element();
-        WebKit.DOM.HTMLElement signature_container = create(page, "div");
-        //signature_container.class_list.add(SIGNATURE_CONTAINER_CLASS);
-        do {
-            // Get its sibling _before_ we move it into the signature div.
-            WebKit.DOM.Node? sibling = elem.get_next_sibling();
-            signature_container.append_child(elem);
-            elem = sibling;
-        } while (elem != null);
-        parent.append_child(signature_container);
-    }
-
 }
diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt
index 12058ff..53e6653 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -17,6 +17,7 @@ set(RESOURCE_LIST
   STRIPBLANKS "conversation-message-menus.ui"
   STRIPBLANKS "conversation-viewer.ui"
               "conversation-web-view.css"
+              "conversation-web-view.js"
   STRIPBLANKS "edit_alternate_emails.glade"
   STRIPBLANKS "empty-placeholder.ui"
   STRIPBLANKS "find_bar.glade"
diff --git a/ui/client-web-view.js b/ui/client-web-view.js
index f4105f9..13000cb 100644
--- a/ui/client-web-view.js
+++ b/ui/client-web-view.js
@@ -15,16 +15,19 @@ var PageState = function() {
 PageState.prototype = {
     init: function() {
         this.allowRemoteImages = false;
-        this.loaded = false;
+        this.is_loaded = false;
 
         var state = this;
         var timeoutId = window.setInterval(function() {
             state.preferredHeightChanged();
-            if (state.loaded) {
+            if (state.is_loaded) {
                 window.clearTimeout(timeoutId);
             }
         }, 50);
     },
+    loaded: function() {
+        this.is_loaded = true;
+    },
     loadRemoteImages: function() {
         this.allowRemoteImages = true;
         var images = document.getElementsByTagName("IMG");
@@ -51,8 +54,3 @@ PageState.prototype = {
         window.webkit.messageHandlers.selectionChanged.postMessage(has_selection);
     }
 };
-
-var geary = new PageState();
-window.onload = function() {
-    geary.loaded = true;
-};
diff --git a/ui/conversation-web-view.css b/ui/conversation-web-view.css
index 5405c46..d048993 100644
--- a/ui/conversation-web-view.css
+++ b/ui/conversation-web-view.css
@@ -70,13 +70,13 @@ pre {
  * Message chrome style.
  */
 
-.geary_signature {
+.geary-signature {
     color: #777;
     display: inline;
 }
 
-.geary_signature a,
-.geary_quote_container a {
+.geary-signature a,
+.geary-quote-container a {
     color: #5fb2e7;
 }
 
@@ -90,7 +90,7 @@ pre {
 
   /* Inline collapsable quote blocks */
 
-  .geary_quote_container {
+  .geary-quote-container {
     position: relative;
     /* Split 1em of top/bottom margin between here and the default
     blockquote style, so if a message specifies 0px margin and padding
@@ -102,11 +102,11 @@ pre {
     color: #303030;
     background-color: #e8e8e8;/* recv-quoted */
   }
-  .geary_sent .geary_quote_container {
+  .geary-sent .geary-quote-container {
     background-color: #e8e8e8;/* sent-quoted */
   }
 
-  .geary_quote_container > .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 0000000..15db7fb
--- /dev/null
+++ b/ui/conversation-web-view.js
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2016 Michael Gratton <mike vee net>
+ *
+ * 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 =
+                    "<div class=\"geary-shower\">" +
+                    "<input type=\"button\" value=\"▼        ▼        ▼\" />" +
+                    "</div>" +
+                    "<div class=\"geary-hider\">" +
+                    "<input type=\"button\" value=\"▲        ▲        ▲\" />" +
+                    "</div>";
+
+                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:            <div>-- </div>$SIGNATURE
+     * 2. GMail Alternate:  <div><span>-- </span></div>$SIGNATURE
+     * 3. Thunderbird:      <div>-- <br>$SIGNATURE</div>
+     *
+     */
+    wrapSignature: function() {
+        var possibleSigs = document.documentElement.querySelectorAll("div,span,p");
+        var i = 0;
+        var sigRegex = new RegExp("^--\\s*$");
+        var alternateSigRegex = new RegExp("^--\\s*(?:<br|\\R)");
+        for (; i < possibleSigs.length; ++i) {
+            // Get the div and check that it starts a signature block
+            // and is not inside a quote.
+            var div = possibleSigs.item(i);
+            var innerHTML = div.innerHTML;
+            if ((sigRegex.test(innerHTML) || alternateSigRegex.test(innerHTML)) &&
+                !ConversationPageState.isDescendantOf(div, "BLOCKQUOTE")) {
+                break;
+            }
+        }
+        // If we have a signature, move it and all of its following
+        // siblings that are not quotes inside a signature div.
+        if (i < possibleSigs.length) {
+            var elem = possibleSigs.item(i);
+            var parent = elem.parentNode;
+            var signatureContainer = document.createElement("DIV");
+            signatureContainer.classList.add("geary-signature");
+            do {
+                // Get its sibling _before_ we move it into the signature div.
+                var sibling = elem.nextSibling;
+                signatureContainer.appendChild(elem);
+                elem = sibling;
+            } while (elem != null);
+            parent.appendChild(signatureContainer);
+        }
+    }
+};
+
+ConversationPageState.isDescendantOf = function(node, ancestorTag) {
+    var ancestor = node.parentNode;
+    while (ancestor != null) {
+        if (ancestor.tagName == ancestorTag) {
+            return true;
+        }
+        ancestor = ancestor.parentNode;
+    }
+    return false;
+};
+
+var geary = new ConversationPageState();
+window.onload = function() {
+    geary.loaded();
+};


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]