From 8650fb139a9143f04615de74ff569bce3e0c4ce3 Mon Sep 17 00:00:00 2001 From: Tomas Popela Date: Mon, 9 Jun 2014 16:32:25 +0200 Subject: Bug 540362: [webkit-composer] Use webkit for composer Merge wip/webkit-composer branch into master. --- e-util/e-html-editor-view.c | 6303 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 6303 insertions(+) create mode 100644 e-util/e-html-editor-view.c (limited to 'e-util/e-html-editor-view.c') diff --git a/e-util/e-html-editor-view.c b/e-util/e-html-editor-view.c new file mode 100644 index 0000000000..21cadb3a5e --- /dev/null +++ b/e-util/e-html-editor-view.c @@ -0,0 +1,6303 @@ +/* + * e-html-editor-view.c + * + * Copyright (C) 2012 Dan Vrátil + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "e-html-editor-view.h" +#include "e-html-editor.h" +#include "e-emoticon-chooser.h" + +#include +#include +#include +#include + +#define E_HTML_EDITOR_VIEW_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_HTML_EDITOR_VIEW, EHTMLEditorViewPrivate)) + +#define UNICODE_ZERO_WIDTH_SPACE "\xe2\x80\x8b" + +#define URL_PATTERN \ + "((([A-Za-z]{3,9}:(?:\\/\\/)?)(?:[\\-;:&=\\+\\$,\\w]+@)?" \ + "[A-Za-z0-9\\.\\-]+|(?:www\\.|[\\-;:&=\\+\\$,\\w]+@)" \ + "[A-Za-z0-9\\.\\-]+)((?:\\/[\\+~%\\/\\.\\w\\-]*)?\\?" \ + "?(?:[\\-\\+=&;%@\\.\\w]*)#?(?:[\\.\\!\\/\\\\w]*))?)" + +#define URL_PATTERN_SPACE URL_PATTERN "\\s" + +#define QUOTE_SYMBOL ">" + +/* Keep synchronized with the same value in EHTMLEditorSelection */ +#define SPACES_PER_LIST_LEVEL 8 + +/** + * EHTMLEditorView: + * + * The #EHTMLEditorView is a WebKit-based rich text editor. The view itself + * only provides means to configure global behavior of the editor. To work + * with the actual content, current cursor position or current selection, + * use #EHTMLEditorSelection object. + */ + +struct _EHTMLEditorViewPrivate { + gint changed : 1; + gint inline_spelling : 1; + gint magic_links : 1; + gint magic_smileys : 1; + gint can_copy : 1; + gint can_cut : 1; + gint can_paste : 1; + gint can_redo : 1; + gint can_undo : 1; + gint reload_in_progress : 1; + gint html_mode : 1; + + EHTMLEditorSelection *selection; + + WebKitDOMElement *element_under_mouse; + + GHashTable *inline_images; + + GSettings *font_settings; + GSettings *aliasing_settings; + + gboolean convertor_insert; + + WebKitWebView *convertor_web_view; +}; + +enum { + PROP_0, + PROP_CAN_COPY, + PROP_CAN_CUT, + PROP_CAN_PASTE, + PROP_CAN_REDO, + PROP_CAN_UNDO, + PROP_CHANGED, + PROP_HTML_MODE, + PROP_INLINE_SPELLING, + PROP_MAGIC_LINKS, + PROP_MAGIC_SMILEYS, + PROP_SPELL_CHECKER +}; + +enum { + POPUP_EVENT, + PASTE_PRIMARY_CLIPBOARD, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static CamelDataCache *emd_global_http_cache = NULL; + +G_DEFINE_TYPE_WITH_CODE ( + EHTMLEditorView, + e_html_editor_view, + WEBKIT_TYPE_WEB_VIEW, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL)) + +static WebKitDOMRange * +html_editor_view_get_dom_range (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *selection; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + selection = webkit_dom_dom_window_get_selection (window); + + if (webkit_dom_dom_selection_get_range_count (selection) < 1) { + return NULL; + } + + return webkit_dom_dom_selection_get_range_at (selection, 0, NULL); +} + +static void +html_editor_view_user_changed_contents_cb (EHTMLEditorView *view, + gpointer user_data) +{ + WebKitWebView *web_view; + gboolean can_redo, can_undo; + + web_view = WEBKIT_WEB_VIEW (view); + + e_html_editor_view_set_changed (view, TRUE); + + can_redo = webkit_web_view_can_redo (web_view); + if (view->priv->can_redo != can_redo) { + view->priv->can_redo = can_redo; + g_object_notify (G_OBJECT (view), "can-redo"); + } + + can_undo = webkit_web_view_can_undo (web_view); + if (view->priv->can_undo != can_undo) { + view->priv->can_undo = can_undo; + g_object_notify (G_OBJECT (view), "can-undo"); + } +} + +static void +html_editor_view_selection_changed_cb (EHTMLEditorView *view, + gpointer user_data) +{ + WebKitWebView *web_view; + gboolean can_copy, can_cut, can_paste; + + web_view = WEBKIT_WEB_VIEW (view); + + /* When the webview is being (re)loaded, the document is in an + * inconsistant state and there is no selection, so don't propagate + * the signal further to EHTMLEditorSelection and others and wait until + * the load is finished. */ + if (view->priv->reload_in_progress) { + g_signal_stop_emission_by_name (view, "selection-changed"); + return; + } + + can_copy = webkit_web_view_can_copy_clipboard (web_view); + if (view->priv->can_copy != can_copy) { + view->priv->can_copy = can_copy; + g_object_notify (G_OBJECT (view), "can-copy"); + } + + can_cut = webkit_web_view_can_cut_clipboard (web_view); + if (view->priv->can_cut != can_cut) { + view->priv->can_cut = can_cut; + g_object_notify (G_OBJECT (view), "can-cut"); + } + + can_paste = webkit_web_view_can_paste_clipboard (web_view); + if (view->priv->can_paste != can_paste) { + view->priv->can_paste = can_paste; + g_object_notify (G_OBJECT (view), "can-paste"); + } +} + +static gboolean +html_editor_view_should_show_delete_interface_for_element (EHTMLEditorView *view, + WebKitDOMHTMLElement *element) +{ + return FALSE; +} + +void +e_html_editor_view_force_spell_check_for_current_paragraph (EHTMLEditorView *view) +{ + EHTMLEditorSelection *selection; + WebKitDOMDocument *document; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMDOMWindow *window; + WebKitDOMElement *caret, *parent, *element; + WebKitDOMRange *end_range, *actual; + WebKitDOMText *text; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + element = webkit_dom_document_query_selector ( + document, "body[spellcheck=true]", NULL); + + if (!element) + return; + + selection = e_html_editor_view_get_selection (view); + caret = e_html_editor_selection_save_caret_position (selection); + + /* Block callbacks of selection-changed signal as we don't want to + * recount all the block format things in EHTMLEditorSelection and here as well + * when we are moving with caret */ + g_signal_handlers_block_by_func ( + view, html_editor_view_selection_changed_cb, NULL); + e_html_editor_selection_block_selection_changed (selection); + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (caret)); + element = caret; + + while (parent && !WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + element = parent; + parent = webkit_dom_node_get_parent_element ( + WEBKIT_DOM_NODE (parent)); + } + + /* Append some text on the end of the element */ + text = webkit_dom_document_create_text_node (document, "-x-evo-end"); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), WEBKIT_DOM_NODE (text), NULL); + + /* Create range that's pointing on the end of this text */ + end_range = webkit_dom_document_create_range (document); + webkit_dom_range_select_node_contents ( + end_range, WEBKIT_DOM_NODE (text), NULL); + webkit_dom_range_collapse (end_range, FALSE, NULL); + + /* Move on the beginning of the paragraph */ + actual = webkit_dom_document_create_range (document); + webkit_dom_range_select_node_contents ( + actual, WEBKIT_DOM_NODE (element), NULL); + webkit_dom_range_collapse (actual, TRUE, NULL); + webkit_dom_dom_selection_remove_all_ranges (dom_selection); + webkit_dom_dom_selection_add_range (dom_selection, actual); + + /* Go through all words to spellcheck them. To avoid this we have to wait for + * http://www.w3.org/html/wg/drafts/html/master/editing.html#dom-forcespellcheck */ + actual = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + /* We are moving forward word by word until we hit the text on the end of + * the paragraph that we previously inserted there */ + while (actual && webkit_dom_range_compare_boundary_points (end_range, 2, actual, NULL) != 0) { + webkit_dom_dom_selection_modify ( + dom_selection, "move", "forward", "word"); + actual = webkit_dom_dom_selection_get_range_at ( + dom_selection, 0, NULL); + } + + /* Remove the text that we inserted on the end of the paragraph */ + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (element), WEBKIT_DOM_NODE (text), NULL); + + /* Unblock the callbacks */ + g_signal_handlers_unblock_by_func ( + view, html_editor_view_selection_changed_cb, NULL); + e_html_editor_selection_unblock_selection_changed (selection); + + e_html_editor_selection_restore_caret_position (selection); +} + +static void +move_caret_into_element (WebKitDOMDocument *document, + WebKitDOMElement *element) +{ + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *window_selection; + WebKitDOMRange *new_range; + + if (!element) + return; + + window = webkit_dom_document_get_default_view (document); + window_selection = webkit_dom_dom_window_get_selection (window); + new_range = webkit_dom_document_create_range (document); + + webkit_dom_range_select_node_contents ( + new_range, WEBKIT_DOM_NODE (element), NULL); + webkit_dom_range_collapse (new_range, FALSE, NULL); + webkit_dom_dom_selection_remove_all_ranges (window_selection); + webkit_dom_dom_selection_add_range (window_selection, new_range); +} + +static void +refresh_spell_check (EHTMLEditorView *view, + gboolean enable_spell_check) +{ + EHTMLEditorSelection *selection; + WebKitDOMDocument *document; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMDOMWindow *window; + WebKitDOMHTMLElement *body; + WebKitDOMRange *end_range, *actual; + WebKitDOMText *text; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + /* Enable/Disable spellcheck in composer */ + body = webkit_dom_document_get_body (document); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), + "spellcheck", + enable_spell_check ? "true" : "false", + NULL); + + selection = e_html_editor_view_get_selection (view); + e_html_editor_selection_save_caret_position (selection); + + /* Sometimes the web view is not event focused, so we have to move caret + * into body */ + if (!webkit_dom_document_get_element_by_id (document, "-x-evo-caret-position")) { + move_caret_into_element ( + document, + WEBKIT_DOM_ELEMENT (webkit_dom_document_get_body (document))); + e_html_editor_selection_save_caret_position (selection); + } + + /* Block callbacks of selection-changed signal as we don't want to + * recount all the block format things in EHTMLEditorSelection and here as well + * when we are moving with caret */ + g_signal_handlers_block_by_func ( + view, html_editor_view_selection_changed_cb, NULL); + e_html_editor_selection_block_selection_changed (selection); + + /* Append some text on the end of the body */ + text = webkit_dom_document_create_text_node (document, "-x-evo-end"); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), WEBKIT_DOM_NODE (text), NULL); + + /* Create range that's pointing on the end of this text */ + end_range = webkit_dom_document_create_range (document); + webkit_dom_range_select_node_contents ( + end_range, WEBKIT_DOM_NODE (text), NULL); + webkit_dom_range_collapse (end_range, FALSE, NULL); + + /* Move on the beginning of the document */ + webkit_dom_dom_selection_modify ( + dom_selection, "move", "backward", "documentboundary"); + + /* Go through all words to spellcheck them. To avoid this we have to wait for + * http://www.w3.org/html/wg/drafts/html/master/editing.html#dom-forcespellcheck */ + actual = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + /* We are moving forward word by word until we hit the text on the end of + * the body that we previously inserted there */ + while (actual && webkit_dom_range_compare_boundary_points (end_range, 2, actual, NULL) != 0) { + webkit_dom_dom_selection_modify ( + dom_selection, "move", "forward", "word"); + actual = webkit_dom_dom_selection_get_range_at ( + dom_selection, 0, NULL); + } + + /* Remove the text that we inserted on the end of the body */ + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (body), WEBKIT_DOM_NODE (text), NULL); + + /* Unblock the callbacks */ + g_signal_handlers_unblock_by_func ( + view, html_editor_view_selection_changed_cb, NULL); + e_html_editor_selection_unblock_selection_changed (selection); + + e_html_editor_selection_restore_caret_position (selection); +} + +void +e_html_editor_view_turn_spell_check_off (EHTMLEditorView *view) +{ + refresh_spell_check (view, FALSE); +} + +void +e_html_editor_view_force_spell_check (EHTMLEditorView *view) +{ + refresh_spell_check (view, TRUE); +} + +static void +body_input_event_cb (WebKitDOMElement *element, + WebKitDOMEvent *event, + EHTMLEditorView *view) +{ + WebKitDOMNode *node; + WebKitDOMRange *range = html_editor_view_get_dom_range (view); + + e_html_editor_view_set_changed (view, TRUE); + + node = webkit_dom_range_get_end_container (range, NULL); + + /* After toggling monospaced format, we are using UNICODE_ZERO_WIDTH_SPACE + * to move caret into right space. When this callback is called it is not + * necassary anymore so remove it */ + if (e_html_editor_view_get_html_mode (view)) { + WebKitDOMElement *parent = webkit_dom_node_get_parent_element (node); + + if (parent) { + WebKitDOMNode *prev_sibling; + + prev_sibling = webkit_dom_node_get_previous_sibling ( + WEBKIT_DOM_NODE (parent)); + + if (prev_sibling && WEBKIT_DOM_IS_TEXT (prev_sibling)) { + gchar *text = webkit_dom_node_get_text_content ( + prev_sibling); + + if (g_strcmp0 (text, UNICODE_ZERO_WIDTH_SPACE) == 0) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node ( + prev_sibling), + prev_sibling, + NULL); + } + g_free (text); + } + + } + } + + /* If text before caret includes UNICODE_ZERO_WIDTH_SPACE character, remove it */ + if (WEBKIT_DOM_IS_TEXT (node)) { + gchar *text = webkit_dom_character_data_get_data (WEBKIT_DOM_CHARACTER_DATA (node)); + glong length = g_utf8_strlen (text, -1); + WebKitDOMNode *parent; + + /* We have to preserve empty paragraphs with just UNICODE_ZERO_WIDTH_SPACE + * character as when we will remove it it will collapse */ + if (length > 1) { + if (g_str_has_prefix (text, UNICODE_ZERO_WIDTH_SPACE)) + webkit_dom_character_data_replace_data ( + WEBKIT_DOM_CHARACTER_DATA (node), 0, 1, "", NULL); + else if (g_str_has_suffix (text, UNICODE_ZERO_WIDTH_SPACE)) + webkit_dom_character_data_replace_data ( + WEBKIT_DOM_CHARACTER_DATA (node), length - 1, 1, "", NULL); + } + g_free (text); + + parent = webkit_dom_node_get_parent_node (node); + if ((WEBKIT_DOM_IS_HTML_PARAGRAPH_ELEMENT (parent) || + WEBKIT_DOM_IS_HTML_DIV_ELEMENT (parent)) && + !element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-paragraph")) { + if (e_html_editor_view_get_html_mode (view)) { + element_add_class ( + WEBKIT_DOM_ELEMENT (parent), "-x-evo-paragraph"); + } else { + e_html_editor_selection_set_paragraph_style ( + e_html_editor_view_get_selection (view), + WEBKIT_DOM_ELEMENT (parent), + -1, 0, ""); + } + } + + /* When new smiley is added we have to use UNICODE_HIDDEN_SPACE to set the + * caret position to right place. It is removed when user starts typing. But + * when the user will press left arrow he will move the caret into + * smiley wrapper. If he will start to write there we have to move the written + * text out of the wrapper and move caret to right place */ + if (WEBKIT_DOM_IS_ELEMENT (parent) && + element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-smiley-wrapper")) { + WebKitDOMDocument *document; + + document = webkit_web_view_get_dom_document ( + WEBKIT_WEB_VIEW (view)); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (parent), + e_html_editor_selection_get_caret_position_node ( + document), + webkit_dom_node_get_next_sibling (parent), + NULL); + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (parent), + node, + webkit_dom_node_get_next_sibling (parent), + NULL); + e_html_editor_selection_restore_caret_position ( + e_html_editor_view_get_selection (view)); + } + } +} + +static void +set_base64_to_element_attribute (EHTMLEditorView *view, + WebKitDOMElement *element, + const gchar *attribute) +{ + gchar *attribute_value; + const gchar *base64_src; + + attribute_value = webkit_dom_element_get_attribute (element, attribute); + + if ((base64_src = g_hash_table_lookup (view->priv->inline_images, attribute_value)) != NULL) { + const gchar *base64_data = strstr (base64_src, ";") + 1; + gchar *name; + glong name_length; + + name_length = + g_utf8_strlen (base64_src, -1) - + g_utf8_strlen (base64_data, -1) - 1; + name = g_strndup (base64_src, name_length); + + webkit_dom_element_set_attribute (element, "data-inline", "", NULL); + webkit_dom_element_set_attribute (element, "data-name", name, NULL); + webkit_dom_element_set_attribute (element, attribute, base64_data, NULL); + + g_free (name); + } +} + +static void +change_cid_images_src_to_base64 (EHTMLEditorView *view) +{ + gint ii, length; + WebKitDOMDocument *document; + WebKitDOMElement *document_element; + WebKitDOMNamedNodeMap *attributes; + WebKitDOMNodeList *list; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + document_element = webkit_dom_document_get_document_element (document); + + list = webkit_dom_document_query_selector_all (document, "img[src^=\"cid:\"]", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + set_base64_to_element_attribute (view, WEBKIT_DOM_ELEMENT (node), "src"); + } + + /* Namespaces */ + attributes = webkit_dom_element_get_attributes (document_element); + length = webkit_dom_named_node_map_get_length (attributes); + for (ii = 0; ii < length; ii++) { + gchar *name; + WebKitDOMNode *node = webkit_dom_named_node_map_item (attributes, ii); + + name = webkit_dom_node_get_local_name (node); + + if (g_str_has_prefix (name, "xmlns:")) { + const gchar *ns = name + 6; + gchar *attribute_ns = g_strconcat (ns, ":src", NULL); + gchar *selector = g_strconcat ("img[", ns, "\\:src^=\"cid:\"]", NULL); + gint ns_length, jj; + + list = webkit_dom_document_query_selector_all ( + document, selector, NULL); + ns_length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < ns_length; jj++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, jj); + + set_base64_to_element_attribute ( + view, WEBKIT_DOM_ELEMENT (node), attribute_ns); + } + + g_free (attribute_ns); + g_free (selector); + } + g_free (name); + } + + list = webkit_dom_document_query_selector_all ( + document, "[background^=\"cid:\"]", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + set_base64_to_element_attribute ( + view, WEBKIT_DOM_ELEMENT (node), "background"); + } + g_hash_table_remove_all (view->priv->inline_images); +} + +/* For purpose of this function see e-mail-formatter-quote.c */ +static void +put_body_in_citation (WebKitDOMDocument *document) +{ + WebKitDOMElement *cite_body = webkit_dom_document_query_selector ( + document, "span.-x-evo-cite-body", NULL); + + if (cite_body) { + WebKitDOMHTMLElement *body = webkit_dom_document_get_body (document); + gchar *inner_html, *with_citation; + + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (cite_body), + NULL); + + inner_html = webkit_dom_html_element_get_inner_html (body); + with_citation = g_strconcat ( + "
", + inner_html, "", NULL); + webkit_dom_html_element_set_inner_html (body, with_citation, NULL); + g_free (inner_html); + g_free (with_citation); + } +} + +/* For purpose of this function see e-mail-formatter-quote.c */ +static void +move_elements_to_body (WebKitDOMDocument *document) +{ + WebKitDOMHTMLElement *body = webkit_dom_document_get_body (document); + WebKitDOMNodeList *list; + gint ii; + + list = webkit_dom_document_query_selector_all ( + document, "span.-x-evo-to-body", NULL); + for (ii = webkit_dom_node_list_get_length (list) - 1; ii >= 0; ii--) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + while (webkit_dom_node_has_child_nodes (node)) { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + webkit_dom_node_get_first_child (node), + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (body)), + NULL); + } + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (node), + NULL); + } +} + +static void +repair_gmail_blockquotes (WebKitDOMDocument *document) +{ + WebKitDOMNodeList *list; + gint ii, length; + + list = webkit_dom_document_query_selector_all ( + document, "blockquote.gmail_quote", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + + webkit_dom_element_remove_attribute (WEBKIT_DOM_ELEMENT (node), "class"); + webkit_dom_element_remove_attribute (WEBKIT_DOM_ELEMENT (node), "style"); + webkit_dom_element_set_attribute (WEBKIT_DOM_ELEMENT (node), "type", "cite", NULL); + } +} + +static void +html_editor_view_load_status_changed (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + WebKitLoadStatus status; + + status = webkit_web_view_get_load_status (WEBKIT_WEB_VIEW (view)); + if (status != WEBKIT_LOAD_FINISHED) + return; + + view->priv->reload_in_progress = FALSE; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + webkit_dom_element_remove_attribute (WEBKIT_DOM_ELEMENT (body), "style"); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-message", "", NULL); + + put_body_in_citation (document); + move_elements_to_body (document); + repair_gmail_blockquotes (document); + + /* Register on input event that is called when the content (body) is modified */ + webkit_dom_event_target_add_event_listener ( + WEBKIT_DOM_EVENT_TARGET (body), + "input", + G_CALLBACK (body_input_event_cb), + FALSE, + view); + + if (view->priv->html_mode) + change_cid_images_src_to_base64 (view); +} + +/* Based on original use_pictograms() from GtkHTML */ +static const gchar *emoticons_chars = + /* 0 */ "DO)(|/PQ*!" + /* 10 */ "S\0:-\0:\0:-\0" + /* 20 */ ":\0:;=-\"\0:;" + /* 30 */ "B\"|\0:-'\0:X" + /* 40 */ "\0:\0:-\0:\0:-" + /* 50 */ "\0:\0:-\0:\0:-" + /* 60 */ "\0:\0:\0:-\0:\0" + /* 70 */ ":-\0:\0:-\0:\0"; +static gint emoticons_states[] = { + /* 0 */ 12, 17, 22, 34, 43, 48, 53, 58, 65, 70, + /* 10 */ 75, 0, -15, 15, 0, -15, 0, -17, 20, 0, + /* 20 */ -17, 0, -14, -20, -14, 28, 63, 0, -14, -20, + /* 30 */ -3, 63, -18, 0, -12, 38, 41, 0, -12, -2, + /* 40 */ 0, -4, 0, -10, 46, 0, -10, 0, -19, 51, + /* 50 */ 0, -19, 0, -11, 56, 0, -11, 0, -13, 61, + /* 60 */ 0, -13, 0, -6, 0, 68, -7, 0, -7, 0, + /* 70 */ -16, 73, 0, -16, 0, -21, 78, 0, -21, 0 }; +static const gchar *emoticons_icon_names[] = { + "face-angel", + "face-angry", + "face-cool", + "face-crying", + "face-devilish", + "face-embarrassed", + "face-kiss", + "face-laugh", /* not used */ + "face-monkey", /* not used */ + "face-plain", + "face-raspberry", + "face-sad", + "face-sick", + "face-smile", + "face-smile-big", + "face-smirk", + "face-surprise", + "face-tired", + "face-uncertain", + "face-wink", + "face-worried" +}; + +static void +html_editor_view_check_magic_links (EHTMLEditorView *view, + WebKitDOMRange *range, + gboolean include_space_by_user, + GdkEventKey *event) +{ + gchar *node_text; + gchar **urls; + GRegex *regex = NULL; + GMatchInfo *match_info; + gint start_pos_url, end_pos_url; + WebKitDOMNode *node; + gboolean include_space = FALSE; + gboolean return_pressed = FALSE; + + if (event != NULL) { + if ((event->keyval == GDK_KEY_Return) || + (event->keyval == GDK_KEY_Linefeed) || + (event->keyval == GDK_KEY_KP_Enter)) { + + return_pressed = TRUE; + } + + if (event->keyval == GDK_KEY_space) + include_space = TRUE; + } else { + include_space = include_space_by_user; + } + + node = webkit_dom_range_get_end_container (range, NULL); + + if (return_pressed) + node = webkit_dom_node_get_previous_sibling (node); + + if (!node) + return; + + if (!WEBKIT_DOM_IS_TEXT (node)) { + if (webkit_dom_node_has_child_nodes (node)) + node = webkit_dom_node_get_first_child (node); + if (!WEBKIT_DOM_IS_TEXT (node)) + return; + } + + node_text = webkit_dom_text_get_whole_text (WEBKIT_DOM_TEXT (node)); + if (!node_text || !(*node_text) || !g_utf8_validate (node_text, -1, NULL)) + return; + + regex = g_regex_new (include_space ? URL_PATTERN_SPACE : URL_PATTERN, 0, 0, NULL); + + if (!regex) { + g_free (node_text); + return; + } + + g_regex_match_all (regex, node_text, G_REGEX_MATCH_NOTEMPTY, &match_info); + urls = g_match_info_fetch_all (match_info); + + if (urls) { + gchar *final_url, *url_end_raw; + glong url_start, url_end, url_length; + WebKitDOMDocument *document; + WebKitDOMNode *url_text_node_clone; + WebKitDOMText *url_text_node; + WebKitDOMElement *anchor; + const gchar* url_text; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + if (!return_pressed) + e_html_editor_selection_save_caret_position ( + e_html_editor_view_get_selection (view)); + + g_match_info_fetch_pos (match_info, 0, &start_pos_url, &end_pos_url); + + /* Get start and end position of url in node's text because positions + * that we get from g_match_info_fetch_pos are not UTF-8 aware */ + url_end_raw = g_strndup(node_text, end_pos_url); + url_end = g_utf8_strlen (url_end_raw, -1); + + url_length = g_utf8_strlen (urls[0], -1); + url_start = url_end - url_length; + + webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (node), + include_space ? url_end - 1 : url_end, + NULL); + + url_text_node = webkit_dom_text_split_text ( + WEBKIT_DOM_TEXT (node), url_start, NULL); + url_text_node_clone = webkit_dom_node_clone_node ( + WEBKIT_DOM_NODE (url_text_node), TRUE); + url_text = webkit_dom_text_get_whole_text ( + WEBKIT_DOM_TEXT (url_text_node_clone)); + + final_url = g_strconcat ( + g_str_has_prefix (url_text, "www") ? "http://" : "", url_text, NULL); + + /* Create and prepare new anchor element */ + anchor = webkit_dom_document_create_element (document, "A", NULL); + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (anchor), + url_text, + NULL); + + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (anchor), + final_url); + + /* Insert new anchor element into document */ + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (anchor), + WEBKIT_DOM_NODE (url_text_node), + NULL); + + if (!return_pressed) + e_html_editor_selection_restore_caret_position ( + e_html_editor_view_get_selection (view)); + + g_free (url_end_raw); + g_free (final_url); + } else { + WebKitDOMElement *parent; + WebKitDOMNode *prev_sibling; + gchar *href, *text, *url; + gint diff; + const char* text_to_append; + gboolean appending_to_link = FALSE; + + parent = webkit_dom_node_get_parent_element (node); + prev_sibling = webkit_dom_node_get_previous_sibling (node); + + /* If previous sibling is ANCHOR and actual text node is not beginning with + * space => we're appending to link */ + if (WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (prev_sibling)) { + text_to_append = webkit_dom_node_get_text_content (node); + if (g_strcmp0 (text_to_append, "") != 0 && + !g_unichar_isspace (g_utf8_get_char (text_to_append))) { + + appending_to_link = TRUE; + parent = WEBKIT_DOM_ELEMENT (prev_sibling); + } + } + + /* If parent is ANCHOR => we're editing the link */ + if (!WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (parent) && !appending_to_link) { + g_match_info_free (match_info); + g_regex_unref (regex); + g_free (node_text); + return; + } + + /* edit only if href and description are the same */ + href = webkit_dom_html_anchor_element_get_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (parent)); + + if (appending_to_link) { + gchar *inner_text; + + inner_text = + webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (parent)), + + text = g_strconcat (inner_text, text_to_append, NULL); + g_free (inner_text); + } else + text = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (parent)); + + if (strstr (href, "://") && !strstr (text, "://")) { + url = strstr (href, "://") + 3; + diff = strlen (text) - strlen (url); + + if (text [strlen (text) - 1] != '/') + diff++; + + if ((g_strcmp0 (url, text) != 0 && ABS (diff) == 1) || appending_to_link) { + gchar *inner_html, *protocol, *new_href; + + protocol = g_strndup (href, strstr (href, "://") - href + 3); + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (parent)); + new_href = g_strconcat ( + protocol, inner_html, appending_to_link ? text_to_append : "", NULL); + + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (parent), + new_href); + + if (appending_to_link) { + gchar *tmp; + + tmp = g_strconcat (inner_html, text_to_append, NULL); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (parent), + tmp, + NULL); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, NULL); + + g_free (tmp); + } + + g_free (new_href); + g_free (protocol); + g_free (inner_html); + } + } else { + diff = strlen (text) - strlen (href); + if (text [strlen (text) - 1] != '/') + diff++; + + if ((g_strcmp0 (href, text) != 0 && ABS (diff) == 1) || appending_to_link) { + gchar *inner_html; + gchar *new_href; + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (parent)); + new_href = g_strconcat ( + inner_html, + appending_to_link ? text_to_append : "", + NULL); + + webkit_dom_html_anchor_element_set_href ( + WEBKIT_DOM_HTML_ANCHOR_ELEMENT (parent), + new_href); + + if (appending_to_link) { + gchar *tmp; + + tmp = g_strconcat (inner_html, text_to_append, NULL); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (parent), + tmp, + NULL); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, NULL); + + g_free (tmp); + } + + g_free (new_href); + g_free (inner_html); + } + + } + g_free (text); + g_free (href); + } + + g_match_info_free (match_info); + g_regex_unref (regex); + g_free (node_text); +} + +typedef struct _LoadContext LoadContext; + +struct _LoadContext { + EHTMLEditorView *view; + gchar *content_type; + gchar *name; + EEmoticon *emoticon; +}; + +static LoadContext * +emoticon_load_context_new (EHTMLEditorView *view, + EEmoticon *emoticon) +{ + LoadContext *load_context; + + load_context = g_slice_new0 (LoadContext); + load_context->view = view; + load_context->emoticon = emoticon; + + return load_context; +} + +static void +emoticon_load_context_free (LoadContext *load_context) +{ + g_free (load_context->content_type); + g_free (load_context->name); + g_slice_free (LoadContext, load_context); +} + +static void +emoticon_read_async_cb (GFile *file, + GAsyncResult *result, + LoadContext *load_context) +{ + EHTMLEditorView *view = load_context->view; + EEmoticon *emoticon = load_context->emoticon; + GError *error = NULL; + gchar *html, *node_text = NULL, *mime_type; + gchar *base64_encoded, *output, *data; + const gchar *emoticon_start; + GFileInputStream *input_stream; + GOutputStream *output_stream; + gssize size; + WebKitDOMDocument *document; + WebKitDOMElement *span, *caret_position; + WebKitDOMNode *node; + WebKitDOMRange *range; + + input_stream = g_file_read_finish (file, result, &error); + g_return_if_fail (!error && input_stream); + + output_stream = g_memory_output_stream_new (NULL, 0, g_realloc, g_free); + + size = g_output_stream_splice ( + output_stream, G_INPUT_STREAM (input_stream), + G_OUTPUT_STREAM_SPLICE_NONE, NULL, &error); + + if (error || (size == -1)) + goto out; + + caret_position = e_html_editor_selection_save_caret_position ( + e_html_editor_view_get_selection (view)); + + if (caret_position) { + WebKitDOMNode *parent; + + parent = webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (caret_position)); + + /* Situation when caret is restored in body and not in paragraph */ + if (WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + caret_position = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (parent), + WEBKIT_DOM_NODE (caret_position), + NULL)); + + caret_position = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_insert_before ( + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (parent)), + WEBKIT_DOM_NODE (caret_position), + webkit_dom_node_get_first_child ( + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (parent))), + NULL)); + } + } + + mime_type = g_content_type_get_mime_type (load_context->content_type); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + range = html_editor_view_get_dom_range (view); + node = webkit_dom_range_get_end_container (range, NULL); + if (WEBKIT_DOM_IS_TEXT (node)) + node_text = webkit_dom_text_get_whole_text (WEBKIT_DOM_TEXT (node)); + span = webkit_dom_document_create_element (document, "SPAN", NULL); + + data = g_memory_output_stream_get_data (G_MEMORY_OUTPUT_STREAM (output_stream)); + + base64_encoded = g_base64_encode ((const guchar *) data, size); + output = g_strconcat ("data:", mime_type, ";base64,", base64_encoded, NULL); + + /* Insert span with image representation and another one with text + * represetation and hide/show them dependant on active composer mode */ + /* ​ == UNICODE_ZERO_WIDTH_SPACE */ + html = g_strdup_printf ( + "" + "\"%s\"" + "%s" + "​", + output, emoticon ? emoticon->text_face : "", emoticon->icon_name, + load_context->name, emoticon ? emoticon->text_face : ""); + + span = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (caret_position)), + WEBKIT_DOM_NODE (span), + WEBKIT_DOM_NODE (caret_position), + NULL)); + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (span), html, NULL); + + if (node_text) { + emoticon_start = g_utf8_strrchr ( + node_text, -1, g_utf8_get_char (emoticon->text_face)); + if (emoticon_start) { + webkit_dom_character_data_delete_data ( + WEBKIT_DOM_CHARACTER_DATA (node), + g_utf8_strlen (node_text, -1) - strlen (emoticon_start), + strlen (emoticon->text_face), + NULL); + } + } + + e_html_editor_selection_restore_caret_position ( + e_html_editor_view_get_selection (view)); + + e_html_editor_view_set_changed (view, TRUE); + + g_free (html); + g_free (node_text); + g_free (base64_encoded); + g_free (output); + g_free (mime_type); + g_object_unref (output_stream); + out: + emoticon_load_context_free (load_context); +} + +static void +emoticon_query_info_async_cb (GFile *file, + GAsyncResult *result, + LoadContext *load_context) +{ + GError *error = NULL; + GFileInfo *info; + + info = g_file_query_info_finish (file, result, &error); + g_return_if_fail (!error && info); + + load_context->content_type = g_strdup (g_file_info_get_content_type (info)); + load_context->name = g_strdup (g_file_info_get_name (info)); + + g_file_read_async ( + file, G_PRIORITY_DEFAULT, NULL, + (GAsyncReadyCallback) emoticon_read_async_cb, load_context); + + g_object_unref (info); +} + +void +e_html_editor_view_insert_smiley (EHTMLEditorView *view, + EEmoticon *emoticon) +{ + GFile *file; + gchar *filename_uri; + LoadContext *load_context; + + filename_uri = e_emoticon_get_uri (emoticon); + g_return_if_fail (filename_uri != NULL); + + load_context = emoticon_load_context_new (view, emoticon); + + file = g_file_new_for_uri (filename_uri); + g_file_query_info_async ( + file, "standard::*", G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, NULL, + (GAsyncReadyCallback) emoticon_query_info_async_cb, load_context); + + g_free (filename_uri); + g_object_unref (file); +} + +static void +html_editor_view_check_magic_smileys (EHTMLEditorView *view, + WebKitDOMRange *range) +{ + gint pos; + gint state; + gint relative; + gint start; + gchar *node_text; + gunichar uc; + WebKitDOMNode *node; + + node = webkit_dom_range_get_end_container (range, NULL); + if (!WEBKIT_DOM_IS_TEXT (node)) + return; + + node_text = webkit_dom_text_get_whole_text (WEBKIT_DOM_TEXT (node)); + if (node_text == NULL) + return; + + start = webkit_dom_range_get_end_offset (range, NULL) - 1; + pos = start; + state = 0; + while (pos >= 0) { + uc = g_utf8_get_char (g_utf8_offset_to_pointer (node_text, pos)); + relative = 0; + while (emoticons_chars[state + relative]) { + if (emoticons_chars[state + relative] == uc) + break; + relative++; + } + state = emoticons_states[state + relative]; + /* 0 .. not found, -n .. found n-th */ + if (state <= 0) + break; + pos--; + } + + /* Special case needed to recognize angel and devilish. */ + if (pos > 0 && state == -14) { + uc = g_utf8_get_char (g_utf8_offset_to_pointer (node_text, pos - 1)); + if (uc == 'O') { + state = -1; + pos--; + } else if (uc == '>') { + state = -5; + pos--; + } + } + + if (state < 0) { + const EEmoticon *emoticon; + + if (pos > 0) { + uc = g_utf8_get_char (g_utf8_offset_to_pointer (node_text, pos - 1)); + if (!g_unichar_isspace (uc)) { + g_free (node_text); + return; + } + } + + emoticon = (e_emoticon_chooser_lookup_emoticon ( + emoticons_icon_names[-state - 1])); + e_html_editor_view_insert_smiley (view, (EEmoticon *) emoticon); + } + + g_free (node_text); +} + +static void +html_editor_view_set_links_active (EHTMLEditorView *view, + gboolean active) +{ + WebKitDOMDocument *document; + WebKitDOMElement *style; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + if (active) { + style = webkit_dom_document_get_element_by_id ( + document, "--evolution-editor-style-a"); + if (style) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (style)), + WEBKIT_DOM_NODE (style), NULL); + } + } else { + WebKitDOMHTMLHeadElement *head; + head = webkit_dom_document_get_head (document); + + style = webkit_dom_document_create_element (document, "STYLE", NULL); + webkit_dom_element_set_id (style, "--evolution-editor-style-a"); + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (style), "a { cursor: text; }", NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (head), WEBKIT_DOM_NODE (style), NULL); + } +} + +static void +clipboard_text_received (GtkClipboard *clipboard, + const gchar *text, + EHTMLEditorView *view) +{ + EHTMLEditorSelection *selection; + gchar *escaped_text; + WebKitDOMDocument *document; + WebKitDOMDOMWindow *window; + WebKitDOMDOMSelection *dom_selection; + WebKitDOMElement *blockquote, *element; + WebKitDOMNode *node; + WebKitDOMRange *range; + + if (!text || !*text) + return; + + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + window = webkit_dom_document_get_default_view (document); + dom_selection = webkit_dom_dom_window_get_selection (window); + + /* This is a trick to escape any HTML characters (like <, > or &). + *