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.
This commit is contained in:
Michael James Gratton 2016-11-27 21:03:52 +11:00
parent 7c5e5ae2cd
commit 431ebcb35f
7 changed files with 169 additions and 216 deletions

View file

@ -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);
}

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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"

View file

@ -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;
};

View file

@ -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;
}

136
ui/conversation-web-view.js Normal file
View file

@ -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();
};