145
// Validates a URL. Intended to be used as a RegexEvalCallback.
146
// Ensures the URL begins with a valid protocol specifier. (If not, we don't
147
// want to linkify it.)
148
public bool is_valid_url(MatchInfo match_info, StringBuilder result) {
150
string? url = match_info.fetch(0);
151
Regex r = new Regex(PROTOCOL_REGEX, RegexCompileFlags.CASELESS);
153
result.append(r.match(url) ? "<a href=\"%s\">%s</a>".printf(url, url) : url);
155
debug("URL parsing error: %s\n", e.message);
157
return false; // False to continue processing.
160
// Converts plain text emails to something safe and usable in HTML.
161
public string linkify_and_escape_plain_text(string input) throws Error {
162
// Convert < and > into non-printable characters, and change & to &.
163
string output = input.replace("<", " \01 ").replace(">", " \02 ").replace("&", "&");
165
// Converts text links into HTML hyperlinks.
166
Regex r = new Regex(URL_REGEX, RegexCompileFlags.CASELESS);
168
output = r.replace_eval(output, -1, 0, 0, is_valid_url);
169
return output.replace(" \01 ", "<").replace(" \02 ", ">");
172
public bool node_is_child_of(WebKit.DOM.Node node, string ancestor_tag) {
173
WebKit.DOM.Element? ancestor = node.get_parent_element();
174
for (; ancestor != null; ancestor = ancestor.get_parent_element()) {
175
if (ancestor.get_tag_name() == ancestor_tag) {
182
public WebKit.DOM.HTMLElement? closest_ancestor(WebKit.DOM.Element element, string selector) {
184
WebKit.DOM.Element? parent = element.get_parent_element();
185
while (parent != null && !parent.webkit_matches_selector(selector)) {
186
parent = parent.get_parent_element();
188
return parent as WebKit.DOM.HTMLElement;
189
} catch (Error error) {
190
warning("Failed to find ancestor: %s", error.message);
195
public string decorate_quotes(string text) throws Error {
198
Regex quote_leader = new Regex("^(>)* ?"); // Some > followed by optional space
200
foreach (string line in text.split("\n")) {
201
MatchInfo match_info;
202
if (quote_leader.match_all(line, 0, out match_info)) {
203
int start, end, new_level;
204
match_info.fetch_pos(0, out start, out end);
205
new_level = end / 4; // Cast to int removes 0.25 from space at end, if present
206
while (new_level > level) {
207
outtext += "<blockquote>";
210
while (new_level < level) {
211
outtext += "</blockquote>";
214
outtext += line.substring(end);
216
debug("This line didn't match the quote regex: %s", line);
220
// Close any remaining blockquotes.
222
outtext += "</blockquote>";
228
public string html_to_flowed_text(WebKit.DOM.Document doc) {
229
WebKit.DOM.NodeList blockquotes;
231
blockquotes = doc.query_selector_all("blockquote");
232
} catch (Error error) {
233
debug("Error selecting blockquotes: %s", error.message);
237
int nbq = (int) blockquotes.length;
238
WebKit.DOM.Text[] tokens = new WebKit.DOM.Text[nbq];
239
string[] bqtexts = new string[nbq];
241
// Get text of blockquotes and pull them out of DOM. We need to get the text while they're
242
// still in the DOM to get newlines at appropriate places. We go through the list of blockquotes
243
// from the end so that we get the innermost ones first.
244
for (int i = nbq - 1; i >= 0; i--) {
245
WebKit.DOM.Node bq = blockquotes.item(i);
246
WebKit.DOM.Node parent = bq.get_parent_node();
247
bqtexts[i] = ((WebKit.DOM.HTMLElement) bq).get_inner_text();
248
tokens[i] = doc.create_text_node(@"$i");
250
parent.replace_child(tokens[i], bq);
251
} catch (Error error) {
252
debug("Error manipulating DOM: %s", error.message);
256
// Reassemble plain text out of parts
257
string doctext = resolve_nesting(doc.get_body().get_inner_text(), bqtexts);
260
for (int i = 0; i < nbq; i++) {
261
WebKit.DOM.Node parent = tokens[i].get_parent_node();
263
parent.replace_child(blockquotes.item(i), tokens[i]);
264
} catch (Error error) {
265
debug("Error manipulating DOM: %s", error.message);
269
// Wrap, space stuff, quote
270
string[] lines = doctext.split("\n");
271
GLib.StringBuilder flowed = new GLib.StringBuilder.sized(doctext.length);
272
foreach (string line in lines) {
274
while (line[quote_level] == Geary.RFC822.Utils.QUOTE_MARKER)
276
line = line[quote_level:line.length];
277
string prefix = quote_level > 0 ? string.nfill(quote_level, '>') + " " : "";
278
int max_len = 72 - prefix.length;
281
if (quote_level == 0 && (line.has_prefix(">") || line.has_prefix("From")))
284
int cut_ind = line.length;
285
if (cut_ind > max_len) {
286
string beg = line[0:max_len];
287
cut_ind = beg.last_index_of(" ") + 1;
289
cut_ind = line.index_of(" ") + 1;
291
cut_ind = line.length;
292
if (cut_ind > 998 - prefix.length)
293
cut_ind = 998 - prefix.length;
296
flowed.append(prefix + line[0:cut_ind] + "\n");
297
line = line[cut_ind:line.length];
298
} while (line.length > 0);
304
public string quote_lines(string text) {
305
string[] lines = text.split("\n");
306
for (int i=0; i<lines.length; i++)
307
lines[i] = @"$(Geary.RFC822.Utils.QUOTE_MARKER)" + lines[i];
308
return string.joinv("\n", lines);
311
public string resolve_nesting(string text, string[] values) {
313
GLib.Regex tokenregex = new GLib.Regex("([0-9]*)(.?)");
314
return tokenregex.replace_eval(text, -1, 0, 0, (info, res) => {
315
int key = int.parse(info.fetch(1));
316
string next_char = info.fetch(2);
317
// If there is a next character, and it's not a newline, insert a newline
318
// before it. Otherwise, that text will become part of the inserted quote.
319
if (next_char != "" && next_char != "\n")
320
next_char = "\n" + next_char;
321
if (key >= 0 && key < values.length) {
322
res.append(quote_lines(resolve_nesting(values[key], values)) + next_char);
324
debug("Regex error in denesting blockquotes: Invalid key");
329
} catch (Error error) {
330
debug("Regex error in denesting blockquotes: %s", error.message);