Initial pass at implementing Format=Flowed formatting in JS for WK2.

May not actually be working, needs testing, may eat your replies, etc.
This commit is contained in:
Michael James Gratton 2016-12-05 10:07:23 +11:00
parent 6edc097e50
commit d4a4c5b45f
2 changed files with 143 additions and 127 deletions

View file

@ -7,10 +7,6 @@
// Regex to determine if a URL has a known protocol.
public const string PROTOCOL_REGEX = "^(aim|apt|bitcoin|cvs|ed2k|ftp|file|finger|git|gtalk|http|https|irc|ircs|irc6|lastfm|ldap|ldaps|magnet|news|nntp|rsync|sftp|skype|smb|sms|svn|telnet|tftp|ssh|webcal|xmpp):";
// Private use unicode characters are used for quote tokens
public const string QUOTE_START = "‘";
public const string QUOTE_END = "’";
namespace Util.DOM {
public WebKit.DOM.HTMLElement? select(WebKit.DOM.Node node, string selector) {
try {
@ -298,126 +294,4 @@ namespace Util.DOM {
return outtext;
}
/**
* Convert a HTML DOM tree to RFC 3676 format=flowed text.
*
* This will modify/reset the DOM.
*/
public string html_to_flowed_text(WebKit.DOM.HTMLElement el) {
string saved_doc = el.get_inner_html();
WebKit.DOM.NodeList blockquotes;
try {
blockquotes = el.query_selector_all("blockquote");
} catch (Error error) {
debug("Error selecting blockquotes: %s", error.message);
return "";
}
int nbq = (int) blockquotes.length;
string[] bqtexts = new string[nbq];
// Get text of blockquotes and pull them out of DOM. They are replaced with tokens deliminated
// with the characters QUOTE_START and QUOTE_END (from a unicode private use block). We need to
// get the text while they're still in the DOM to get newlines at appropriate places. We go
// through the list of blockquotes from the end so that we get the innermost ones first.
for (int i = nbq - 1; i >= 0; i--) {
WebKit.DOM.HTMLElement bq = (WebKit.DOM.HTMLElement) blockquotes.item(i);
bqtexts[i] = bq.get_inner_text();
if (bqtexts[i].length > 0 && bqtexts[i].substring(-1, 1) == "\n")
bqtexts[i] = bqtexts[i].slice(0, -1);
else
debug("Did not find expected newline at end of quote.");
try {
bq.set_inner_text(@"$QUOTE_START$i$QUOTE_END");
} catch (Error error) {
debug("Error manipulating DOM: %s", error.message);
}
}
// Reassemble plain text out of parts, replace non-breaking space with regular space
string doctext = resolve_nesting(el.get_inner_text(), bqtexts).replace("\xc2\xa0", " ");
// Reassemble DOM
try {
el.set_inner_html(saved_doc);
} catch (Error error) {
debug("Error resetting DOM: %s", error.message);
}
// Wrap, space stuff, quote
string[] lines = doctext.split("\n");
GLib.StringBuilder flowed = new GLib.StringBuilder.sized(doctext.length);
foreach (string line in lines) {
// Strip trailing whitespace, so it doesn't look like a flowed line. But the
// signature separator "-- " is special, so leave that alone.
if (line != "-- ")
line = line.chomp();
int quote_level = 0;
while (line[quote_level] == Geary.RFC822.Utils.QUOTE_MARKER)
quote_level += 1;
line = line[quote_level:line.length];
string prefix = quote_level > 0 ? string.nfill(quote_level, '>') + " " : "";
int max_len = 72 - prefix.length;
do {
int start_ind = 0;
if (quote_level == 0 &&
(line.has_prefix(">") || line.has_prefix("From"))) {
line = " " + line;
start_ind = 1;
}
int cut_ind = line.length;
if (cut_ind > max_len) {
string beg = line[0:max_len];
cut_ind = beg.last_index_of(" ", start_ind) + 1;
if (cut_ind == 0) {
cut_ind = line.index_of(" ", start_ind) + 1;
if (cut_ind == 0)
cut_ind = line.length;
if (cut_ind > 998 - prefix.length)
cut_ind = 998 - prefix.length;
}
}
flowed.append(prefix + line[0:cut_ind] + "\n");
line = line[cut_ind:line.length];
} while (line.length > 0);
}
return flowed.str;
}
public string quote_lines(string text) {
string[] lines = text.split("\n");
for (int i=0; i<lines.length; i++)
lines[i] = @"$(Geary.RFC822.Utils.QUOTE_MARKER)" + lines[i];
return string.joinv("\n", lines);
}
public string resolve_nesting(string text, string[] values) {
try {
GLib.Regex tokenregex = new GLib.Regex(@"(.?)$QUOTE_START([0-9]*)$QUOTE_END(?=(.?))");
return tokenregex.replace_eval(text, -1, 0, 0, (info, res) => {
int key = int.parse(info.fetch(2));
string prev_char = info.fetch(1), next_char = info.fetch(3), insert_next = "";
// Make sure there's a newline before and after the quote.
if (prev_char != "" && prev_char != "\n")
prev_char = prev_char + "\n";
if (next_char != "" && next_char != "\n")
insert_next = "\n";
if (key >= 0 && key < values.length) {
res.append(prev_char + quote_lines(resolve_nesting(values[key], values)) + insert_next);
} else {
debug("Regex error in denesting blockquotes: Invalid key");
res.append("");
}
return false;
});
} catch (Error error) {
debug("Regex error in denesting blockquotes: %s", error.message);
return "";
}
}
}

View file

@ -13,6 +13,9 @@ var ComposerPageState = function() {
this.init.apply(this, arguments);
};
ComposerPageState.BODY_ID = "message-body";
ComposerPageState.QUOTE_START = "‘";
ComposerPageState.QUOTE_END = "’";
ComposerPageState.QUOTE_MARKER = "\x7f";
ComposerPageState.prototype = {
__proto__: PageState.prototype,
@ -63,7 +66,9 @@ ComposerPageState.prototype = {
return document.getElementById(ComposerPageState.BODY_ID).innerHTML;
},
getText: function() {
return document.getElementById(ComposerPageState.BODY_ID).innerText;
return ComposerPageState.htmlToFlowedText(
document.getElementById(ComposerPageState.BODY_ID)
);
},
setRichText: function(enabled) {
if (enabled) {
@ -87,6 +92,143 @@ ComposerPageState.prototype = {
}
};
/**
* Convert a HTML DOM tree to RFC 3676 format=flowed text.
*
* This will modify/reset the DOM.
*/
ComposerPageState.htmlToFlowedText = function(root) {
var savedDoc = root.innerHTML;
var blockquotes = root.querySelectorAll("blockquote");
var nbq = blockquotes.length;
var bqtexts = new Array(nbq);
// Get text of blockquotes and pull them out of DOM. They are
// replaced with tokens deliminated with the characters
// QUOTE_START and QUOTE_END (from a unicode private use block).
// We need to get the text while they're still in the DOM to get
// newlines at appropriate places. We go through the list of
// blockquotes from the end so that we get the innermost ones
// first.
for (let i = nbq - 1; i >= 0; i--) {
let bq = blockquotes.item(i);
let text = bq.innerText;
console.log("Line: " + text);
if (text.substr(-1, 1) == "\n") {
text = text.slice(0, -1);
console.log(" found expected newline at end of quote!");
} else {
console.log(
" no newline at end of quote: " +
text.length > 0
? "0x" + text.codePointAt(text.length - 1).toString(16)
: "empty line"
);
}
bqtexts[i] = text;
bq.innerText = (
ComposerPageState.QUOTE_START
+ i.toString()
+ ComposerPageState.QUOTE_END
);
}
// Reassemble plain text out of parts, replace non-breaking
// space with regular space
var doctext = ComposerPageState.resolveNesting(
root.innerText, bqtexts
).replace("\xc2\xa0", " ");
// Reassemble DOM
root.innerHTML = savedDoc;
// Wrap, space stuff, quote
var lines = doctext.split("\n");
flowed = [];
for (let line of lines) {
// Strip trailing whitespace, so it doesn't look like a flowed
// line. But the signature separator "-- " is special, so
// leave that alone.
if (line != "-- ") {
line = line.trimRight();
}
let quoteLevel = 0;
while (line[quoteLevel] == ComposerPageState.QUOTE_MARKER) {
quoteLevel += 1;
}
line = line.substr(quoteLevel, line.length);
let prefix = quoteLevel > 0 ? '>'.repeat(quoteLevel) + " " : "";
let maxLen = 72 - prefix.length;
do {
let startInd = 0;
if (quoteLevel == 0 &&
(line.startsWith(">") || line.startsWith("From"))) {
line = " " + line;
startInd = 1;
}
let cutInd = line.length;
if (cutInd > maxLen) {
let beg = line.substr(0, maxLen);
cutInd = beg.lastIndexOf(" ", startInd) + 1;
if (cutInd == 0) {
cutInd = line.indexOf(" ", startInd) + 1;
if (cutInd == 0) {
cutInd = line.length;
}
if (cutInd > 998 - prefix.length) {
cutInd = 998 - prefix.length;
}
}
}
flowed.push(prefix + line.substr(0, cutInd) + "\n");
line = line.substr(cutInd, line.length);
} while (line.length > 0);
}
return flowed.join("");
};
ComposerPageState.resolveNesting = function(text, values) {
let tokenregex = new RegExp(
"(.?)" +
ComposerPageState.QUOTE_START +
"([0-9]*)" +
ComposerPageState.QUOTE_END +
"(?=(.?))"
);
return text.replace(tokenregex, function(match, p1, p2, p3, offset, str) {
let key = new Number(p2);
let prevChar = p1;
let nextChar = p3;
let insertNext = "";
// Make sure there's a newline before and after the quote.
if (prevChar != "" && prevChar != "\n")
prevChar = prevChar + "\n";
if (nextChar != "" && nextChar != "\n")
insertNext = "\n";
let value = "";
if (key >= 0 && key < values.length) {
let nested = ComposerPageState.resolveNesting(values[key], values);
value = prevChar + ComposerPageState.quoteLines(nested) + insertNext;
} else {
console.log("Regex error in denesting blockquotes: Invalid key");
}
return value;
});
};
ComposerPageState.quoteLines = function(text) {
let lines = text.split("\n");
for (let i = 0; i < lines.length; i++)
lines[i] = ComposerPageState.QUOTE_MARKER + lines[i];
return lines.join("\n");
};
var geary = new ComposerPageState();
window.onload = function() {
geary.loaded();