diff options
Diffstat (limited to 'e-util/e-html-editor-view.c')
-rw-r--r-- | e-util/e-html-editor-view.c | 6303 |
1 files changed, 6303 insertions, 0 deletions
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 <dvratil@redhat.com> + * + * 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 <http://www.gnu.org/licenses/> + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-html-editor-view.h" +#include "e-html-editor.h" +#include "e-emoticon-chooser.h" + +#include <e-util/e-util.h> +#include <e-util/e-marshal.h> +#include <glib/gi18n-lib.h> +#include <gdk/gdkkeysyms.h> + +#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 ( + "<blockquote type=\"cite\" id=\"-x-evo-main-cite\">", + inner_html, "</span>", 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 ( + "<span class=\"-x-evo-smiley-wrapper -x-evo-resizable-wrapper\">" + "<img src=\"%s\" alt=\"%s\" x-evo-smiley=\"%s\" " + "class=\"-x-evo-smiley-img\" data-inline data-name=\"%s\"/>" + "<span class=\"-x-evo-smiley-text\" style=\"display: none;\">%s" + "</span></span>​", + 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 &). + * <textarea> automatically replaces all these unsafe characters + * by <, > etc. */ + element = webkit_dom_document_create_element (document, "textarea", NULL); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element), text, NULL); + escaped_text = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); + + element = webkit_dom_document_create_element (document, "pre", NULL); + + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (element), escaped_text, NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), + e_html_editor_selection_get_caret_position_node (document), + NULL); + + blockquote = webkit_dom_document_create_element (document, "blockquote", NULL); + webkit_dom_element_set_attribute (blockquote, "type", "cite", NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), WEBKIT_DOM_NODE (element), NULL); + + if (!e_html_editor_view_get_html_mode (view)) + e_html_editor_view_quote_plain_text_element (view, element); + + range = webkit_dom_dom_selection_get_range_at (dom_selection, 0, NULL); + node = webkit_dom_range_get_end_container (range, NULL); + + webkit_dom_node_append_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (blockquote), + NULL); + + e_html_editor_selection_restore_caret_position (selection); + + e_html_editor_view_force_spell_check_for_current_paragraph (view); + + g_free (escaped_text); +} + +static void +html_editor_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CHANGED: + e_html_editor_view_set_changed ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_HTML_MODE: + e_html_editor_view_set_html_mode ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_INLINE_SPELLING: + e_html_editor_view_set_inline_spelling ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_MAGIC_LINKS: + e_html_editor_view_set_magic_links ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + + case PROP_MAGIC_SMILEYS: + e_html_editor_view_set_magic_smileys ( + E_HTML_EDITOR_VIEW (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +html_editor_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CAN_COPY: + g_value_set_boolean ( + value, webkit_web_view_can_copy_clipboard ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CAN_CUT: + g_value_set_boolean ( + value, webkit_web_view_can_cut_clipboard ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CAN_PASTE: + g_value_set_boolean ( + value, webkit_web_view_can_paste_clipboard ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CAN_REDO: + g_value_set_boolean ( + value, webkit_web_view_can_redo ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CAN_UNDO: + g_value_set_boolean ( + value, webkit_web_view_can_undo ( + WEBKIT_WEB_VIEW (object))); + return; + + case PROP_CHANGED: + g_value_set_boolean ( + value, e_html_editor_view_get_changed ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_HTML_MODE: + g_value_set_boolean ( + value, e_html_editor_view_get_html_mode ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_INLINE_SPELLING: + g_value_set_boolean ( + value, e_html_editor_view_get_inline_spelling ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_MAGIC_LINKS: + g_value_set_boolean ( + value, e_html_editor_view_get_magic_links ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_MAGIC_SMILEYS: + g_value_set_boolean ( + value, e_html_editor_view_get_magic_smileys ( + E_HTML_EDITOR_VIEW (object))); + return; + + case PROP_SPELL_CHECKER: + g_value_set_object ( + value, e_html_editor_view_get_spell_checker ( + E_HTML_EDITOR_VIEW (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +html_editor_view_dispose (GObject *object) +{ + EHTMLEditorViewPrivate *priv; + + priv = E_HTML_EDITOR_VIEW_GET_PRIVATE (object); + + g_clear_object (&priv->selection); + + if (priv->convertor_web_view != NULL) { + g_object_unref (priv->convertor_web_view); + priv->convertor_web_view = NULL; + } + + if (priv->aliasing_settings != NULL) { + g_signal_handlers_disconnect_matched ( + priv->aliasing_settings, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->aliasing_settings); + priv->aliasing_settings = NULL; + } + + if (priv->font_settings != NULL) { + g_signal_handlers_disconnect_matched ( + priv->font_settings, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->font_settings); + priv->font_settings = NULL; + } + + g_hash_table_remove_all (priv->inline_images); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_html_editor_view_parent_class)->dispose (object); +} + +static void +html_editor_view_finalize (GObject *object) +{ + EHTMLEditorViewPrivate *priv; + + priv = E_HTML_EDITOR_VIEW_GET_PRIVATE (object); + + g_hash_table_destroy (priv->inline_images); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_html_editor_view_parent_class)->finalize (object); +} + +static void +html_editor_view_constructed (GObject *object) +{ + e_extensible_load_extensions (E_EXTENSIBLE (object)); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_html_editor_view_parent_class)->constructed (object); +} + +static void +html_editor_view_save_element_under_mouse_click (GtkWidget *widget) +{ + gint x, y; + GdkDeviceManager *device_manager; + GdkDevice *pointer; + EHTMLEditorView *view; + WebKitDOMDocument *document; + WebKitDOMElement *element; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (widget)); + + device_manager = gdk_display_get_device_manager ( + gtk_widget_get_display (GTK_WIDGET (widget))); + pointer = gdk_device_manager_get_client_pointer (device_manager); + gdk_window_get_device_position ( + gtk_widget_get_window (GTK_WIDGET (widget)), pointer, &x, &y, NULL); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (widget)); + element = webkit_dom_document_element_from_point (document, x, y); + + view = E_HTML_EDITOR_VIEW (widget); + view->priv->element_under_mouse = element; +} + +static gboolean +html_editor_view_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + gboolean event_handled; + + if (event->button == 2) { + /* Middle click paste */ + g_signal_emit (widget, signals[PASTE_PRIMARY_CLIPBOARD], 0); + event_handled = TRUE; + } else if (event->button == 3) { + html_editor_view_save_element_under_mouse_click (widget); + g_signal_emit ( + widget, signals[POPUP_EVENT], + 0, event, &event_handled); + } else { + event_handled = FALSE; + } + + if (event_handled) + return TRUE; + + /* Chain up to parent's button_press_event() method. */ + return GTK_WIDGET_CLASS (e_html_editor_view_parent_class)-> + button_press_event (widget, event); +} + +static gboolean +html_editor_view_button_release_event (GtkWidget *widget, + GdkEventButton *event) +{ + WebKitWebView *webview; + WebKitHitTestResult *hit_test; + WebKitHitTestResultContext context; + gchar *uri; + + webview = WEBKIT_WEB_VIEW (widget); + hit_test = webkit_web_view_get_hit_test_result (webview, event); + + g_object_get ( + hit_test, + "context", &context, + "link-uri", &uri, + NULL); + + g_object_unref (hit_test); + + /* Left click on a link */ + if ((context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) && + (event->button == 1)) { + + /* Ctrl + Left Click on link opens it, otherwise ignore the + * click completely */ + if (event->state & GDK_CONTROL_MASK) { + GtkWidget *toplevel; + GdkScreen *screen; + + toplevel = gtk_widget_get_toplevel (widget); + screen = gtk_window_get_screen (GTK_WINDOW (toplevel)); + gtk_show_uri (screen, uri, event->time, NULL); + g_free (uri); + } + + return TRUE; + } + + g_free (uri); + + /* Chain up to parent's button_release_event() method. */ + return GTK_WIDGET_CLASS (e_html_editor_view_parent_class)-> + button_release_event (widget, event); +} + +static gboolean +insert_new_line_into_citation (EHTMLEditorView *view) +{ + EHTMLEditorSelection *selection; + gboolean html_mode, ret_val; + + html_mode = e_html_editor_view_get_html_mode (view); + selection = e_html_editor_view_get_selection (view); + + ret_val = e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_NEW_LINE_IN_QUOTED_CONTENT, NULL); + + if (ret_val && !html_mode) { + WebKitDOMElement *element; + WebKitDOMDocument *document; + WebKitDOMNode *next_sibling; + + document = webkit_web_view_get_dom_document ( + WEBKIT_WEB_VIEW (view)); + + element = webkit_dom_document_query_selector ( + document, "body>br", NULL); + + next_sibling = webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (element)); + + if (WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (next_sibling)) { + /* Quote content */ + next_sibling = WEBKIT_DOM_NODE ( + e_html_editor_view_quote_plain_text_element ( + view, WEBKIT_DOM_ELEMENT (next_sibling))); + /* Renew spellcheck */ + e_html_editor_view_force_spell_check (view); + /* Insert caret node on right position */ + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (element)), + e_html_editor_selection_get_caret_position_node ( + document), + WEBKIT_DOM_NODE (element), + NULL); + /* Restore caret position */ + e_html_editor_selection_restore_caret_position ( + selection); + } + } + + return ret_val; +} + +static gboolean +prevent_from_deleting_last_element_in_body (EHTMLEditorView *view) +{ + gboolean ret_val = FALSE; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + WebKitDOMNodeList *list; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + list = webkit_dom_node_get_child_nodes (WEBKIT_DOM_NODE (body)); + + if (webkit_dom_node_list_get_length (list) <= 1) { + gchar *content; + + content = webkit_dom_node_get_text_content (WEBKIT_DOM_NODE (body)); + + if (!*content) + ret_val = TRUE; + + g_free (content); + + if (webkit_dom_element_query_selector (WEBKIT_DOM_ELEMENT (body), "img", NULL)) + ret_val = FALSE; + } + + return ret_val; +} + +static gboolean +html_editor_view_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + EHTMLEditorView *view = E_HTML_EDITOR_VIEW (widget); + + if (event->keyval == GDK_KEY_Tab) + return e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_TEXT, "\t"); + + if ((event->keyval == GDK_KEY_Control_L) || + (event->keyval == GDK_KEY_Control_R)) { + + html_editor_view_set_links_active (view, TRUE); + } + + if ((event->keyval == GDK_KEY_Return) || + (event->keyval == GDK_KEY_KP_Enter)) { + EHTMLEditorSelection *selection; + + selection = e_html_editor_view_get_selection (view); + /* When user presses ENTER in a citation block, WebKit does + * not break the citation automatically, so we need to use + * the special command to do it. */ + if (e_html_editor_selection_is_citation (selection)) + return insert_new_line_into_citation (view); + } + + /* BackSpace in indented block decrease indent level by one */ + if (event->keyval == GDK_KEY_BackSpace) { + EHTMLEditorSelection *selection; + + selection = e_html_editor_view_get_selection (view); + if (e_html_editor_selection_is_indented (selection)) { + WebKitDOMElement *caret; + + caret = e_html_editor_selection_save_caret_position (selection); + + if (!webkit_dom_node_get_previous_sibling (WEBKIT_DOM_NODE (caret))) { + e_html_editor_selection_clear_caret_position_marker (selection); + e_html_editor_selection_unindent (selection); + return TRUE; + } else + e_html_editor_selection_clear_caret_position_marker (selection); + } + + if (prevent_from_deleting_last_element_in_body (view)) + return TRUE; + } + + /* Chain up to parent's key_press_event() method. */ + return GTK_WIDGET_CLASS (e_html_editor_view_parent_class)-> + key_press_event (widget, event); +} + +static void +mark_node_as_paragraph_after_ending_list (EHTMLEditorSelection *selection, + WebKitDOMDocument *document) +{ + gint ii, length; + WebKitDOMNodeList *list; + + /* When pressing Enter on empty line in the list WebKit will end that + * list and inserts <div><br></div> so mark it for wrapping */ + list = webkit_dom_document_query_selector_all ( + document, "body > div:not(.-x-evo-paragraph) > br", NULL); + + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_get_parent_node ( + webkit_dom_node_list_item (list, ii)); + + e_html_editor_selection_set_paragraph_style ( + selection, WEBKIT_DOM_ELEMENT (node), -1, 0, ""); + } +} + +static gboolean +surround_text_with_paragraph_if_needed (EHTMLEditorSelection *selection, + WebKitDOMDocument *document, + WebKitDOMNode *node) +{ + WebKitDOMNode *next_sibling = webkit_dom_node_get_next_sibling (node); + WebKitDOMNode *prev_sibling = webkit_dom_node_get_previous_sibling (node); + WebKitDOMElement *element; + + /* All text in composer has to be written in div elements, so if + * we are writing something straight to the body, surround it with + * paragraph */ + if (WEBKIT_DOM_IS_TEXT (node) && + WEBKIT_DOM_IS_HTML_BODY_ELEMENT (webkit_dom_node_get_parent_node (node))) { + element = e_html_editor_selection_put_node_into_paragraph ( + selection, + document, + node, + e_html_editor_selection_get_caret_position_node (document)); + + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (next_sibling)) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (next_sibling), + next_sibling, + NULL); + } + + /* Tab character */ + if (WEBKIT_DOM_IS_ELEMENT (prev_sibling) && + element_has_class (WEBKIT_DOM_ELEMENT (prev_sibling), "Apple-tab-span")) { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (element), + prev_sibling, + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (element)), + NULL); + } + + return TRUE; + } + + return FALSE; +} + +static gboolean +html_editor_view_key_release_event (GtkWidget *widget, + GdkEventKey *event) +{ + WebKitDOMDocument *document; + WebKitDOMRange *range; + EHTMLEditorView *view; + EHTMLEditorSelection *selection; + + view = E_HTML_EDITOR_VIEW (widget); + range = html_editor_view_get_dom_range (view); + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (widget)); + + if (view->priv->magic_smileys && + view->priv->html_mode) { + html_editor_view_check_magic_smileys (view, range); + } + + if ((event->keyval == GDK_KEY_Return) || + (event->keyval == GDK_KEY_Linefeed) || + (event->keyval == GDK_KEY_KP_Enter) || + (event->keyval == GDK_KEY_space)) { + + html_editor_view_check_magic_links (view, range, FALSE, event); + + mark_node_as_paragraph_after_ending_list (selection, document); + } else { + WebKitDOMNode *node; + + node = webkit_dom_range_get_end_container (range, NULL); + + if (surround_text_with_paragraph_if_needed (selection, document, node)) { + e_html_editor_selection_restore_caret_position (selection); + node = webkit_dom_range_get_end_container (range, NULL); + range = html_editor_view_get_dom_range (view); + } + + if (WEBKIT_DOM_IS_TEXT (node)) { + gchar *text; + + text = webkit_dom_node_get_text_content (node); + + if (g_strcmp0 (text, "") != 0 && !g_unichar_isspace (g_utf8_get_char (text))) { + WebKitDOMNode *prev_sibling; + + prev_sibling = webkit_dom_node_get_previous_sibling (node); + + if (WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (prev_sibling)) + html_editor_view_check_magic_links (view, range, FALSE, event); + } + g_free (text); + } + } + + if ((event->keyval == GDK_KEY_Control_L) || + (event->keyval == GDK_KEY_Control_R)) { + + html_editor_view_set_links_active (view, FALSE); + } + + /* Chain up to parent's key_release_event() method. */ + return GTK_WIDGET_CLASS (e_html_editor_view_parent_class)-> + key_release_event (widget, event); +} + +static void +html_editor_view_paste_clipboard_quoted (EHTMLEditorView *view) +{ + GtkClipboard *clipboard; + + clipboard = gtk_clipboard_get_for_display ( + gdk_display_get_default (), + GDK_SELECTION_CLIPBOARD); + + gtk_clipboard_request_text ( + clipboard, + (GtkClipboardTextReceivedFunc) clipboard_text_received, + view); +} + +static gboolean +html_editor_view_image_exists_in_cache (const gchar *image_uri) +{ + gchar *filename; + gchar *hash; + gboolean exists = FALSE; + + g_return_val_if_fail (emd_global_http_cache != NULL, FALSE); + + hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, image_uri, -1); + filename = camel_data_cache_get_filename ( + emd_global_http_cache, "http", hash); + + if (filename != NULL) { + exists = g_file_test (filename, G_FILE_TEST_EXISTS); + g_free (filename); + } + + g_free (hash); + + return exists; +} + +static gchar * +html_editor_view_redirect_uri (EHTMLEditorView *view, + const gchar *uri) +{ + EImageLoadingPolicy image_policy; + GSettings *settings; + gboolean uri_is_http; + + uri_is_http = + g_str_has_prefix (uri, "http:") || + g_str_has_prefix (uri, "https:") || + g_str_has_prefix (uri, "evo-http:") || + g_str_has_prefix (uri, "evo-https:"); + + /* Redirect http(s) request to evo-http(s) protocol. + * See EMailRequest for further details about this. */ + if (uri_is_http) { + gchar *new_uri; + SoupURI *soup_uri; + gboolean image_exists; + + /* Check Evolution's cache */ + image_exists = html_editor_view_image_exists_in_cache (uri); + + settings = g_settings_new ("org.gnome.evolution.mail"); + image_policy = g_settings_get_enum (settings, "image-loading-policy"); + g_object_unref (settings); + /* If the URI is not cached and we are not allowed to load it + * then redirect to invalid URI, so that webkit would display + * a native placeholder for it. */ + if (!image_exists && (image_policy == E_IMAGE_LOADING_POLICY_NEVER)) { + return g_strdup ("about:blank"); + } + + new_uri = g_strconcat ("evo-", uri, NULL); + soup_uri = soup_uri_new (new_uri); + g_free (new_uri); + + new_uri = soup_uri_to_string (soup_uri, FALSE); + + soup_uri_free (soup_uri); + + return new_uri; + } + + return g_strdup (uri); +} + +static void +html_editor_view_resource_requested (WebKitWebView *web_view, + WebKitWebFrame *frame, + WebKitWebResource *resource, + WebKitNetworkRequest *request, + WebKitNetworkResponse *response, + gpointer user_data) +{ + const gchar *original_uri; + + original_uri = webkit_network_request_get_uri (request); + + if (original_uri != NULL) { + gchar *redirected_uri; + + redirected_uri = html_editor_view_redirect_uri ( + E_HTML_EDITOR_VIEW (web_view), original_uri); + + webkit_network_request_set_uri (request, redirected_uri); + + g_free (redirected_uri); + } +} + +static void +e_html_editor_view_class_init (EHTMLEditorViewClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + g_type_class_add_private (class, sizeof (EHTMLEditorViewPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = html_editor_view_get_property; + object_class->set_property = html_editor_view_set_property; + object_class->dispose = html_editor_view_dispose; + object_class->finalize = html_editor_view_finalize; + object_class->constructed = html_editor_view_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->button_press_event = html_editor_view_button_press_event; + widget_class->button_release_event = html_editor_view_button_release_event; + widget_class->key_press_event = html_editor_view_key_press_event; + widget_class->key_release_event = html_editor_view_key_release_event; + + class->paste_clipboard_quoted = html_editor_view_paste_clipboard_quoted; + + /** + * EHTMLEditorView:can-copy + * + * Determines whether it's possible to copy to clipboard. The action + * is usually disabled when there is no selection to copy. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_COPY, + g_param_spec_boolean ( + "can-copy", + "Can Copy", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:can-cut + * + * Determines whether it's possible to cut to clipboard. The action + * is usually disabled when there is no selection to cut. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_CUT, + g_param_spec_boolean ( + "can-cut", + "Can Cut", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:can-paste + * + * Determines whether it's possible to paste from clipboard. The action + * is usually disabled when there is no valid content in clipboard to + * paste. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_PASTE, + g_param_spec_boolean ( + "can-paste", + "Can Paste", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:can-redo + * + * Determines whether it's possible to redo previous action. The action + * is usually disabled when there is no action to redo. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_REDO, + g_param_spec_boolean ( + "can-redo", + "Can Redo", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:can-undo + * + * Determines whether it's possible to undo last action. The action + * is usually disabled when there is no previous action to undo. + */ + g_object_class_install_property ( + object_class, + PROP_CAN_UNDO, + g_param_spec_boolean ( + "can-undo", + "Can Undo", + NULL, + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:changed + * + * Determines whether document has been modified + */ + g_object_class_install_property ( + object_class, + PROP_CHANGED, + g_param_spec_boolean ( + "changed", + _("Changed property"), + _("Whether editor changed"), + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:html-mode + * + * Determines whether HTML or plain text mode is enabled. + **/ + g_object_class_install_property ( + object_class, + PROP_HTML_MODE, + g_param_spec_boolean ( + "html-mode", + "HTML Mode", + "Edit HTML or plain text", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView::inline-spelling + * + * Determines whether automatic spellchecking is enabled. + */ + g_object_class_install_property ( + object_class, + PROP_INLINE_SPELLING, + g_param_spec_boolean ( + "inline-spelling", + "Inline Spelling", + "Check your spelling as you type", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:magic-links + * + * Determines whether automatic conversion of text links into + * HTML links is enabled. + */ + g_object_class_install_property ( + object_class, + PROP_MAGIC_LINKS, + g_param_spec_boolean ( + "magic-links", + "Magic Links", + "Make URIs clickable as you type", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:magic-smileys + * + * Determines whether automatic conversion of text smileys into + * images is enabled. + */ + g_object_class_install_property ( + object_class, + PROP_MAGIC_SMILEYS, + g_param_spec_boolean ( + "magic-smileys", + "Magic Smileys", + "Convert emoticons to images as you type", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:spell-checker: + * + * The #ESpellChecker used for spell checking. + **/ + g_object_class_install_property ( + object_class, + PROP_SPELL_CHECKER, + g_param_spec_object ( + "spell-checker", + "Spell Checker", + "The spell checker", + E_TYPE_SPELL_CHECKER, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * EHTMLEditorView:popup-event + * + * Emitted whenever a context menu is requested. + */ + signals[POPUP_EVENT] = g_signal_new ( + "popup-event", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EHTMLEditorViewClass, popup_event), + g_signal_accumulator_true_handled, NULL, + e_marshal_BOOLEAN__BOXED, + G_TYPE_BOOLEAN, 1, + GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); + /** + * EHTMLEditorView:paste-primary-clipboad + * + * Emitted when user presses middle button on EHTMLEditorView + */ + signals[PASTE_PRIMARY_CLIPBOARD] = g_signal_new ( + "paste-primary-clipboard", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (EHTMLEditorViewClass, paste_primary_clipboard), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static GString * +replace_string_spaces_with_nbsp_in_prefix (const gchar *text) +{ + GString *str; + gint counter = 0; + + g_return_val_if_fail (text != NULL, NULL); + + str = g_string_new (""); + + while (g_str_has_prefix (text + counter, " ")) { + g_string_append (str, " "); + + counter++; + } + + g_string_append (str, text + counter); + + return str; +} + +/* This parses the HTML code (that contains just text, and BR elements) + * into paragraphs. + * HTML code in that format we can get by taking innerText from some element, + * setting it to another one and finally getting innerHTML from it */ +static void +parse_html_into_paragraphs (EHTMLEditorView *view, + WebKitDOMDocument *document, + WebKitDOMElement *blockquote, + const gchar *html, + gboolean use_pre) +{ + const gchar *prev_br, *next_br; + gchar *inner_html; + gint citation_level = 0; + GString *start, *end; + gboolean ignore_next_br = FALSE; + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (blockquote), "", NULL); + + prev_br = html; + next_br = strstr (prev_br, "<br>"); + + while (next_br) { + gboolean local_ignore_next_br = ignore_next_br; + const gchar *citation = NULL, *citation_end = NULL; + const gchar *rest = NULL, *with_br = NULL; + gchar *to_insert = NULL; + WebKitDOMElement *paragraph; + + to_insert = g_utf8_substring ( + prev_br, 0, g_utf8_pointer_to_offset (prev_br, next_br)); + + with_br = strstr (to_insert, "<br>"); + + ignore_next_br = FALSE; + + citation = strstr (to_insert, "##CITATION_"); + if (citation) { + if (strstr (to_insert, "##CITATION_START##")) + citation_level++; + else + citation_level--; + + citation_end = strstr (citation + 2, "##"); + if (citation_end) + rest = citation_end + 2; + } else { + rest = with_br ? + to_insert + 4 + (with_br - to_insert) : to_insert; + } + + if (use_pre) { + paragraph = webkit_dom_document_create_element ( + document, "pre", NULL); + } else { + paragraph = e_html_editor_selection_get_paragraph_element ( + e_html_editor_view_get_selection (view), + document, -1, citation_level); + } + + if (with_br && !*rest && !citation &&!local_ignore_next_br) { + WebKitDOMNode *paragraph_clone; + + paragraph_clone = webkit_dom_node_clone_node ( + WEBKIT_DOM_NODE (paragraph), TRUE); + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (paragraph_clone), + " ", + NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), + paragraph_clone, + NULL); + } + + if (citation) { + WebKitDOMText *text; + gchar *citation_mark; + + citation_mark = g_utf8_substring ( + citation, 0, + g_utf8_pointer_to_offset ( + citation, citation_end + 2)); + + text = webkit_dom_document_create_text_node ( + document, citation_mark); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), + WEBKIT_DOM_NODE (text), + NULL); + + g_free (citation_mark); + } + + if (rest && *rest){ + GString *space_to_nbsp; + + space_to_nbsp = replace_string_spaces_with_nbsp_in_prefix (rest); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (paragraph), + space_to_nbsp->str, + NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), + WEBKIT_DOM_NODE (paragraph), + NULL); + g_string_free (space_to_nbsp, TRUE); + } + + if (citation_end) + ignore_next_br = TRUE; + + prev_br = next_br; + next_br = strstr (prev_br + 4, "<br>"); + g_free (to_insert); + } + + if (g_utf8_strlen (prev_br, -1) > 0) { + WebKitDOMElement *paragraph; + + if (use_pre) { + paragraph = webkit_dom_document_create_element ( + document, "pre", NULL); + } else { + paragraph = e_html_editor_selection_get_paragraph_element ( + e_html_editor_view_get_selection (view), + document, -1, citation_level); + } + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (paragraph), + g_str_has_prefix (prev_br, "<br>") ? prev_br + 4 : prev_br, + NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (blockquote), + WEBKIT_DOM_NODE (paragraph), + NULL); + } + + /* Replace text markers with actual HTML blockquotes */ + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (blockquote)); + start = e_str_replace_string ( + inner_html, "##CITATION_START##","<blockquote type=\"cite\">"); + end = e_str_replace_string ( + start->str, "##CITATION_END##", "</blockquote>"); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (blockquote), end->str, NULL); + + g_free (inner_html); + g_string_free (start, TRUE); + g_string_free (end, TRUE); +} + +static void +mark_citation (WebKitDOMElement *citation) +{ + gchar *inner_html, *surrounded; + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (citation)); + + surrounded = g_strconcat ( + "<span>##CITATION_START##</span>", inner_html, + "<span>##CITATION_END##</span>", NULL); + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (citation), surrounded, NULL); + + element_add_class (citation, "marked"); + + g_free (inner_html); + g_free (surrounded); +} + +static gint +create_text_markers_for_citations_in_document (WebKitDOMDocument *document) +{ + gint count = 0; + WebKitDOMElement *citation; + + citation = webkit_dom_document_query_selector ( + document, "blockquote[type=cite]:not(.marked)", NULL); + + while (citation) { + mark_citation (citation); + count ++; + + citation = webkit_dom_document_query_selector ( + document, "blockquote[type=cite]:not(.marked)", NULL); + } + + return count; +} + +static gint +create_text_markers_for_citations_in_element (WebKitDOMElement *element) +{ + gint count = 0; + WebKitDOMElement *citation; + + citation = webkit_dom_element_query_selector ( + element, "blockquote[type=cite]:not(.marked)", NULL); + + while (citation) { + mark_citation (citation); + count ++; + + citation = webkit_dom_element_query_selector ( + element, "blockquote[type=cite]:not(.marked)", NULL); + } + + return count; +} + +static void +html_editor_view_process_document_from_convertor (EHTMLEditorView *view, + WebKitDOMDocument *document_convertor) +{ + EHTMLEditorSelection *selection = e_html_editor_view_get_selection (view); + gboolean start_bottom; + gchar *inner_text, *inner_html; + gint ii; + GSettings *settings; + WebKitDOMDocument *document; + WebKitDOMElement *paragraph, *new_blockquote, *top_signature; + WebKitDOMElement *cite_body, *signature; + WebKitDOMHTMLElement *body, *body_convertor; + WebKitDOMNodeList *list; + + settings = g_settings_new ("org.gnome.evolution.mail"); + start_bottom = g_settings_get_boolean (settings, "composer-reply-start-bottom"); + g_object_unref (settings); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + body_convertor = webkit_dom_document_get_body (document_convertor); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-converted", "", NULL); + + paragraph = webkit_dom_document_get_element_by_id (document, "-x-evo-input-start"); + if (!paragraph) { + paragraph = e_html_editor_selection_get_paragraph_element ( + selection, document, -1, 0); + webkit_dom_element_set_id (paragraph, "-x-evo-input-start"); + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (paragraph), UNICODE_ZERO_WIDTH_SPACE, NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)), + WEBKIT_DOM_NODE (paragraph), + NULL); + } + + list = webkit_dom_document_query_selector_all ( + document_convertor, "span.-x-evo-to-body", NULL); + for (ii = webkit_dom_node_list_get_length (list) - 1; ii >= 0; ii--) { + WebKitDOMNode *node; + + 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_clone_node ( + webkit_dom_node_get_first_child (node), TRUE), + webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (paragraph)), + NULL); + + webkit_dom_node_remove_child ( + node, webkit_dom_node_get_first_child (node), NULL); + } + + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (body_convertor), + WEBKIT_DOM_NODE (node), + NULL); + } + + repair_gmail_blockquotes (document_convertor); + + create_text_markers_for_citations_in_document (document_convertor); + + /* Get innertText from convertor */ + inner_text = webkit_dom_html_element_get_inner_text (body_convertor); + + cite_body = webkit_dom_document_query_selector ( + document_convertor, "span.-x-evo-cite-body", NULL); + + top_signature = webkit_dom_document_query_selector ( + document, ".-x-evo-top-signature", NULL); + signature = webkit_dom_document_query_selector ( + document, "span.-x-evo-signature", NULL); + + if (cite_body) { + if (!(top_signature && start_bottom)) + e_html_editor_selection_save_caret_position (selection); + } else { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (paragraph), + WEBKIT_DOM_NODE ( + e_html_editor_selection_get_caret_position_node ( + document)), + NULL); + } + + new_blockquote = webkit_dom_document_create_element ( + document, "blockquote", NULL); + webkit_dom_element_set_attribute ( + new_blockquote, "type", "cite", NULL); + + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (new_blockquote), inner_text, NULL); + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (new_blockquote)); + + if (cite_body) { + webkit_dom_element_set_attribute ( + new_blockquote, "id", "-x-evo-main-cite", NULL); + + parse_html_into_paragraphs ( + view, document, new_blockquote, inner_html, FALSE); + + if (start_bottom) { + if (signature) { + WebKitDOMNode *parent = + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)); + if (top_signature) { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (paragraph), + NULL); + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (paragraph), + e_html_editor_selection_get_caret_position_node ( + document), + NULL); + } else { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + WEBKIT_DOM_NODE (parent), + NULL); + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (paragraph), + webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (new_blockquote)), + NULL); + } + } else { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + NULL); + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (paragraph), + webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (new_blockquote)), + NULL); + } + } else { + if (signature) { + WebKitDOMNode *parent = + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)); + + if (top_signature) { + WebKitDOMElement *br; + + br = webkit_dom_document_create_element ( + document, "BR", NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + NULL); + /* Insert NL after signature */ + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (br), + webkit_dom_node_get_next_sibling ( + WEBKIT_DOM_NODE (paragraph)), + NULL); + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (parent), + WEBKIT_DOM_NODE (br), + NULL); + } else + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + WEBKIT_DOM_NODE (parent), + NULL); + } else { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (new_blockquote), + NULL); + } + } + } else { + WebKitDOMNode *signature_clone, *first_child; + + if (signature) { + signature_clone = webkit_dom_node_clone_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)), + TRUE); + } + + parse_html_into_paragraphs ( + view, document, WEBKIT_DOM_ELEMENT (body), + inner_html, FALSE); + + if (signature) { + if (!top_signature) { + signature_clone = webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + signature_clone, + NULL); + } else { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (body), + signature_clone, + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (body)), + NULL); + } + } + + first_child = webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (body)); + + webkit_dom_node_insert_before ( + first_child, + e_html_editor_selection_get_caret_position_node ( + document), + webkit_dom_node_get_first_child (first_child), + NULL); + } + + if (!e_html_editor_view_get_html_mode (view)) + e_html_editor_selection_wrap_paragraphs_in_document ( + selection, document); + if (webkit_dom_document_query_selector (document, "blockquote[type=cite]", NULL)) + body = WEBKIT_DOM_HTML_ELEMENT ( + e_html_editor_view_quote_plain_text (view)); + + e_html_editor_selection_restore_caret_position (selection); + e_html_editor_view_force_spell_check (view); + + /* 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); + + g_free (inner_html); + g_free (inner_text); +} + +static void +html_editor_view_insert_converted_html_into_selection (EHTMLEditorView *view, + WebKitDOMDocument *document_convertor) +{ + gchar *inner_text, *inner_html; + WebKitDOMDocument *document; + WebKitDOMElement *element; + WebKitDOMHTMLElement *convertor_body; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + convertor_body = webkit_dom_document_get_body (document_convertor); + + inner_text = webkit_dom_html_element_get_inner_text (convertor_body); + element = webkit_dom_document_create_element (document, "div", NULL); + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (element), inner_text, NULL); + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); + + parse_html_into_paragraphs ( + view, document, element, inner_html, FALSE); + + g_free (inner_html); + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); + + e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_INSERT_HTML, inner_html); + + e_html_editor_view_force_spell_check (view); + + g_free (inner_html); + g_free (inner_text); +} + +static void +html_plain_text_convertor_load_status_changed (WebKitWebView *web_view, + GParamSpec *pspec, + EHTMLEditorView *view) +{ + WebKitDOMDocument *document_convertor; + + if (webkit_web_view_get_load_status (web_view) != WEBKIT_LOAD_FINISHED) + return; + + document_convertor = webkit_web_view_get_dom_document (web_view); + + if (view->priv->convertor_insert) + html_editor_view_insert_converted_html_into_selection ( + view, document_convertor); + else + html_editor_view_process_document_from_convertor ( + view, document_convertor); +} + +static void +e_html_editor_view_init (EHTMLEditorView *view) +{ + WebKitWebSettings *settings; + GSettings *g_settings; + GSettingsSchema *settings_schema; + ESpellChecker *checker; + gchar **languages; + gchar *comma_separated; + const gchar *user_cache_dir; + + view->priv = E_HTML_EDITOR_VIEW_GET_PRIVATE (view); + + webkit_web_view_set_editable (WEBKIT_WEB_VIEW (view), TRUE); + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (view)); + + g_object_set ( + G_OBJECT (settings), + "enable-developer-extras", TRUE, + "enable-dom-paste", TRUE, + "enable-file-access-from-file-uris", TRUE, + "enable-plugins", FALSE, + "enable-scripts", FALSE, + "enable-spell-checking", TRUE, + "respect-image-orientation", TRUE, + NULL); + + webkit_web_view_set_settings (WEBKIT_WEB_VIEW (view), settings); + + /* Override the spell-checker, use our own */ + checker = e_spell_checker_new (); + webkit_set_text_checker (G_OBJECT (checker)); + g_object_unref (checker); + + /* Don't use CSS when possible to preserve compatibility with older + * versions of Evolution or other MUAs */ + e_html_editor_view_exec_command ( + view, E_HTML_EDITOR_VIEW_COMMAND_STYLE_WITH_CSS, "false"); + + g_signal_connect ( + view, "user-changed-contents", + G_CALLBACK (html_editor_view_user_changed_contents_cb), NULL); + g_signal_connect ( + view, "selection-changed", + G_CALLBACK (html_editor_view_selection_changed_cb), NULL); + g_signal_connect ( + view, "should-show-delete-interface-for-element", + G_CALLBACK (html_editor_view_should_show_delete_interface_for_element), NULL); + g_signal_connect ( + view, "resource-request-starting", + G_CALLBACK (html_editor_view_resource_requested), NULL); + g_signal_connect ( + view, "notify::load-status", + G_CALLBACK (html_editor_view_load_status_changed), NULL); + + view->priv->selection = g_object_new ( + E_TYPE_HTML_EDITOR_SELECTION, + "html-editor-view", view, + NULL); + + g_settings = g_settings_new ("org.gnome.desktop.interface"); + g_signal_connect_swapped ( + g_settings, "changed::font-name", + G_CALLBACK (e_html_editor_view_update_fonts), view); + g_signal_connect_swapped ( + g_settings, "changed::monospace-font-name", + G_CALLBACK (e_html_editor_view_update_fonts), view); + view->priv->font_settings = g_settings; + + /* This schema is optional. Use if available. */ + settings_schema = g_settings_schema_source_lookup ( + g_settings_schema_source_get_default (), + "org.gnome.settings-daemon.plugins.xsettings", FALSE); + if (settings_schema != NULL) { + g_settings = g_settings_new ("org.gnome.settings-daemon.plugins.xsettings"); + g_signal_connect_swapped ( + settings, "changed::antialiasing", + G_CALLBACK (e_html_editor_view_update_fonts), view); + view->priv->aliasing_settings = g_settings; + } + + view->priv->inline_images = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + + e_html_editor_view_update_fonts (view); + + /* Give spell check languages to WebKit */ + languages = e_spell_checker_list_active_languages (checker, NULL); + comma_separated = g_strjoinv (",", languages); + g_strfreev (languages); + + g_object_set ( + G_OBJECT (settings), + "spell-checking-languages", comma_separated, + NULL); + + g_free (comma_separated); + + view->priv->convertor_insert = FALSE; + + view->priv->convertor_web_view = + g_object_ref_sink (WEBKIT_WEB_VIEW (webkit_web_view_new ())); + settings = webkit_web_view_get_settings (view->priv->convertor_web_view); + + g_object_set ( + G_OBJECT (settings), + "enable-scripts", FALSE, + "enable-plugins", FALSE, + NULL); + + g_signal_connect ( + view->priv->convertor_web_view, "notify::load-status", + G_CALLBACK (html_plain_text_convertor_load_status_changed), view); + + /* Make WebKit think we are displaying a local file, so that it + * does not block loading resources from file:// protocol */ + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), "", "text/html", "UTF-8", "file://"); + + html_editor_view_set_links_active (view, FALSE); + + if (emd_global_http_cache == NULL) { + user_cache_dir = e_get_user_cache_dir (); + emd_global_http_cache = camel_data_cache_new (user_cache_dir, NULL); + + /* cache expiry - 2 hour access, 1 day max */ + camel_data_cache_set_expire_age ( + emd_global_http_cache, 24 * 60 * 60); + camel_data_cache_set_expire_access ( + emd_global_http_cache, 2 * 60 * 60); + } +} + +/** + * e_html_editor_view_new: + * + * Returns a new instance of the editor. + * + * Returns: A newly created #EHTMLEditorView. [transfer-full] + */ +EHTMLEditorView * +e_html_editor_view_new (void) +{ + return g_object_new (E_TYPE_HTML_EDITOR_VIEW, NULL); +} + +/** + * e_html_editor_view_get_selection: + * @view: an #EHTMLEditorView + * + * Returns an #EHTMLEditorSelection object which represents current selection or + * cursor position within the editor document. The #EHTMLEditorSelection allows + * programmer to manipulate with formatting, selection, styles etc. + * + * Returns: An always valid #EHTMLEditorSelection object. The object is owned by + * the @view and should never be free'd. + */ +EHTMLEditorSelection * +e_html_editor_view_get_selection (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), NULL); + + return view->priv->selection; +} + +/** + * e_html_editor_view_exec_command: + * @view: an #EHTMLEditorView + * @command: an #EHTMLEditorViewCommand to execute + * @value: value of the command (or @NULL if the command does not require value) + * + * The function will fail when @value is @NULL or empty but the current @command + * requires a value to be passed. The @value is ignored when the @command does + * not expect any value. + * + * Returns: @TRUE when the command was succesfully executed, @FALSE otherwise. + */ +gboolean +e_html_editor_view_exec_command (EHTMLEditorView *view, + EHTMLEditorViewCommand command, + const gchar *value) +{ + WebKitDOMDocument *document; + const gchar *cmd_str = 0; + gboolean has_value; + + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + +#define CHECK_COMMAND(cmd,str,val) case cmd:\ + if (val) {\ + g_return_val_if_fail (value && *value, FALSE);\ + }\ + has_value = val; \ + cmd_str = str;\ + break; + + switch (command) { + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_BACKGROUND_COLOR, "BackColor", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_BOLD, "Bold", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_COPY, "Copy", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_CREATE_LINK, "CreateLink", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_CUT, "Cut", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_DEFAULT_PARAGRAPH_SEPARATOR, "DefaultParagraphSeparator", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_DELETE, "Delete", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FIND_STRING, "FindString", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FONT_NAME, "FontName", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE, "FontSize", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FONT_SIZE_DELTA, "FontSizeDelta", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FORE_COLOR, "ForeColor", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FORMAT_BLOCK, "FormatBlock", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_FORWARD_DELETE, "ForwardDelete", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_HILITE_COLOR, "HiliteColor", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INDENT, "Indent", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_HORIZONTAL_RULE, "InsertHorizontalRule", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_HTML, "InsertHTML", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_IMAGE, "InsertImage", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_LINE_BREAK, "InsertLineBreak", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_NEW_LINE_IN_QUOTED_CONTENT, "InsertNewlineInQuotedContent", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_ORDERED_LIST, "InsertOrderedList", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_PARAGRAPH, "InsertParagraph", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_TEXT, "InsertText", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_INSERT_UNORDERED_LIST, "InsertUnorderedList", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_ITALIC, "Italic", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_CENTER, "JustifyCenter", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_FULL, "JustifyFull", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_LEFT, "JustifyLeft", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_NONE, "JustifyNone", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_JUSTIFY_RIGHT, "JustifyRight", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_OUTDENT, "Outdent", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_PASTE, "Paste", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_PASTE_AND_MATCH_STYLE, "PasteAndMatchStyle", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_PASTE_AS_PLAIN_TEXT, "PasteAsPlainText", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_PRINT, "Print", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_REDO, "Redo", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_REMOVE_FORMAT, "RemoveFormat", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_SELECT_ALL, "SelectAll", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_STRIKETHROUGH, "Strikethrough", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_STYLE_WITH_CSS, "StyleWithCSS", TRUE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_SUBSCRIPT, "Subscript", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_SUPERSCRIPT, "Superscript", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_TRANSPOSE, "Transpose", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_UNDERLINE, "Underline", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_UNDO, "Undo", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_UNLINK, "Unlink", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_UNSELECT, "Unselect", FALSE) + CHECK_COMMAND (E_HTML_EDITOR_VIEW_COMMAND_USE_CSS, "UseCSS", TRUE) + } + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + return webkit_dom_document_exec_command ( + document, cmd_str, FALSE, has_value ? value : "" ); +} + +/** + * e_html_editor_view_get_changed: + * @view: an #EHTMLEditorView + * + * Whether content of the editor has been changed. + * + * Returns: @TRUE when document was changed, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_changed (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->changed; +} + +/** + * e_html_editor_view_set_changed: + * @view: an #EHTMLEditorView + * @changed: whether document has been changed or not + * + * Sets whether document has been changed or not. The editor is tracking changes + * automatically, but sometimes it's necessary to change the dirty flag to force + * "Save changes" dialog for example. + */ +void +e_html_editor_view_set_changed (EHTMLEditorView *view, + gboolean changed) +{ + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + if (view->priv->changed == changed) + return; + + view->priv->changed = changed; + + g_object_notify (G_OBJECT (view), "changed"); +} + +static gboolean +is_citation_node (WebKitDOMNode *node) +{ + char *value; + + if (!WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (node)) + return FALSE; + + value = webkit_dom_element_get_attribute (WEBKIT_DOM_ELEMENT (node), "type"); + + /* citation == <blockquote type='cite'> */ + if (g_strcmp0 (value, "cite") == 0) { + g_free (value); + return TRUE; + } else { + g_free (value); + return FALSE; + } +} + +static gchar * +get_quotation_for_level (gint quote_level) +{ + gint ii; + GString *output = g_string_new (""); + + for (ii = 0; ii < quote_level; ii++) { + g_string_append (output, "<span class=\"quote_character\">"); + g_string_append (output, QUOTE_SYMBOL); + g_string_append (output, " "); + g_string_append (output, "</span>"); + } + + return g_string_free (output, FALSE); +} + +static void +insert_quote_symbols (WebKitDOMHTMLElement *element, + gint quote_level, + gboolean skip_first, + gboolean insert_newline) +{ + gchar *text; + gint ii; + GString *output; + gchar *quotation; + + if (!WEBKIT_DOM_IS_HTML_ELEMENT (element)) + return; + + text = webkit_dom_html_element_get_inner_html (element); + output = g_string_new (""); + quotation = get_quotation_for_level (quote_level); + + if (g_strcmp0 (text, "\n") == 0) { + g_string_append (output, "<span class=\"-x-evo-quoted\">"); + g_string_append (output, quotation); + g_string_append (output, "</span>"); + g_string_append (output, "\n"); + } else { + gchar **lines; + + lines = g_strsplit (text, "\n", 0); + + for (ii = 0; lines[ii]; ii++) { + if (ii == 0 && skip_first) { + if (g_strv_length (lines) == 1) { + g_strfreev (lines); + goto exit; + } + g_string_append (output, lines[ii]); + g_string_append (output, "\n"); + } + + g_string_append (output, "<span class=\"-x-evo-quoted\">"); + g_string_append (output, quotation); + g_string_append (output, "</span>"); + + /* Insert line of text */ + g_string_append (output, lines[ii]); + if ((ii == g_strv_length (lines) - 1) && + !g_str_has_suffix (text, "\n") && !insert_newline) { + /* If we are on last line and node's text doesn't + * end with \n, don't insert it */ + break; + } + g_string_append (output, "\n"); + } + + g_strfreev (lines); + } + + webkit_dom_html_element_set_inner_html (element, output->str, NULL); + exit: + g_free (quotation); + g_free (text); + g_string_free (output, TRUE); +} + +static void +quote_node (WebKitDOMDocument *document, + WebKitDOMNode *node, + gint quote_level) +{ + gboolean skip_first = FALSE; + gboolean insert_newline = FALSE; + gboolean is_html_node = FALSE; + WebKitDOMElement *wrapper; + WebKitDOMNode *node_clone, *prev_sibling, *next_sibling; + + /* Don't quote when we are not in citation */ + if (quote_level == 0) + return; + + if (WEBKIT_DOM_IS_COMMENT (node)) + return; + + if (WEBKIT_DOM_IS_HTML_ELEMENT (node)) { + insert_quote_symbols ( + WEBKIT_DOM_HTML_ELEMENT (node), quote_level, FALSE, FALSE); + return; + } + + prev_sibling = webkit_dom_node_get_previous_sibling (node); + next_sibling = webkit_dom_node_get_next_sibling (node); + + is_html_node = + !WEBKIT_DOM_IS_COMMENT (prev_sibling) && ( + WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (prev_sibling) || + element_has_tag (WEBKIT_DOM_ELEMENT (prev_sibling), "b") || + element_has_tag (WEBKIT_DOM_ELEMENT (prev_sibling), "i") || + element_has_tag (WEBKIT_DOM_ELEMENT (prev_sibling), "u")); + + if (prev_sibling && is_html_node) + skip_first = TRUE; + + /* Skip the BR between first blockquote and pre */ + if (quote_level == 1 && next_sibling && WEBKIT_DOM_IS_HTML_PRE_ELEMENT (next_sibling)) + return; + + if (next_sibling && WEBKIT_DOM_IS_HTMLBR_ELEMENT (next_sibling) && + WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (webkit_dom_node_get_next_sibling (next_sibling))) { + insert_newline = TRUE; + } + + /* Do temporary wrapper */ + wrapper = webkit_dom_document_create_element (document, "SPAN", NULL); + webkit_dom_element_set_class_name (wrapper, "-x-evo-temp-text-wrapper"); + + node_clone = webkit_dom_node_clone_node (node, TRUE); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (wrapper), + node_clone, + NULL); + + insert_quote_symbols ( + WEBKIT_DOM_HTML_ELEMENT (wrapper), + quote_level, + skip_first, + insert_newline); + + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (wrapper), + node, + NULL); +} + +static void +insert_quote_symbols_before_node (WebKitDOMDocument *document, + WebKitDOMNode *node, + gint quote_level, + gboolean is_html_node) +{ + gchar *quotation; + WebKitDOMElement *element; + + quotation = get_quotation_for_level (quote_level); + element = webkit_dom_document_create_element (document, "SPAN", NULL); + element_add_class (element, "-x-evo-quoted"); + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (element), quotation, NULL); + + if (is_html_node) { + WebKitDOMElement *new_br; + + new_br = webkit_dom_document_create_element (document, "br", NULL); + element_add_class (new_br, "-x-evo-temp-br"); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (new_br), + node, + NULL); + } + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (node), + WEBKIT_DOM_NODE (element), + node, + NULL); + + if (is_html_node) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, + NULL); + } + + g_free (quotation); +} + +static void +quote_plain_text_recursive (WebKitDOMDocument *document, + WebKitDOMNode *node, + WebKitDOMNode *start_node, + gint quote_level) +{ + gboolean skip_node = FALSE; + gboolean move_next = FALSE; + gboolean suppress_next = FALSE; + gboolean is_html_node = FALSE; + WebKitDOMNode *next_sibling, *prev_sibling; + + node = webkit_dom_node_get_first_child (node); + + while (node) { + skip_node = FALSE; + move_next = FALSE; + is_html_node = FALSE; + + if (WEBKIT_DOM_IS_COMMENT (node)) + goto next_node; + + prev_sibling = webkit_dom_node_get_previous_sibling (node); + next_sibling = webkit_dom_node_get_next_sibling (node); + + if (WEBKIT_DOM_IS_TEXT (node)) { + /* Start quoting after we are in blockquote */ + if (quote_level > 0 && !suppress_next) { + /* When quoting text node, we are wrappering it and + * afterwards replacing it with that wrapper, thus asking + * for next_sibling after quoting will return NULL bacause + * that node don't exist anymore */ + quote_node (document, node, quote_level); + node = next_sibling; + skip_node = TRUE; + } else + suppress_next = FALSE; + + goto next_node; + } + + if (!(WEBKIT_DOM_IS_ELEMENT (node) || WEBKIT_DOM_IS_HTML_ELEMENT (node))) + goto next_node; + + if (element_has_id (WEBKIT_DOM_ELEMENT (node), "-x-evo-caret-position")) { + if (quote_level > 0) + element_add_class ( + WEBKIT_DOM_ELEMENT (node), "-x-evo-caret-quoting"); + + move_next = TRUE; + suppress_next = TRUE; + goto next_node; + } + + if (element_has_id (WEBKIT_DOM_ELEMENT (node), "-x-evo-selection-start-marker") || + element_has_id (WEBKIT_DOM_ELEMENT (node), "-x-evo-selection-end-marker")) { + move_next = TRUE; + suppress_next = TRUE; + goto next_node; + } + + if (WEBKIT_DOM_IS_HTML_META_ELEMENT (node)) { + goto next_node; + } + if (WEBKIT_DOM_IS_HTML_STYLE_ELEMENT (node)) { + move_next = TRUE; + goto next_node; + } + if (WEBKIT_DOM_IS_HTML_IMAGE_ELEMENT (node)) { + move_next = TRUE; + goto next_node; + } + + if (webkit_dom_element_get_child_element_count (WEBKIT_DOM_ELEMENT (node)) != 0) + goto with_children; + + /* Even in plain text mode we can have some basic html element + * like anchor and others. When Forwaring e-mail as Quoted EMFormat + * generates header that contatains <b> tags (bold font). + * We have to treat these elements separately to avoid + * modifications of theirs inner texts */ + is_html_node = + WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (node) || + element_has_tag (WEBKIT_DOM_ELEMENT (node), "b") || + element_has_tag (WEBKIT_DOM_ELEMENT (node), "i") || + element_has_tag (WEBKIT_DOM_ELEMENT (node), "u"); + + if (is_html_node) { + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (prev_sibling)) + insert_quote_symbols_before_node ( + document, prev_sibling, quote_level, TRUE); + + move_next = TRUE; + goto next_node; + } + + /* If element doesn't have children, we can quote it */ + if (is_citation_node (node)) { + /* Citation with just text inside */ + quote_node (document, node, quote_level + 1); + /* Set citation as quoted */ + element_add_class ( + WEBKIT_DOM_ELEMENT (node), + "-x-evo-plaintext-quoted"); + + move_next = TRUE; + goto next_node; + } + + if (!WEBKIT_DOM_IS_HTMLBR_ELEMENT (node)) + goto not_br; + + if (!prev_sibling) { + WebKitDOMNode *parent; + + parent = webkit_dom_node_get_parent_node (node); + + /* BR in the beginning of the citation */ + if (WEBKIT_DOM_IS_HTML_PRE_ELEMENT (parent)) + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + } + + if (WEBKIT_DOM_IS_ELEMENT (prev_sibling) && + WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (next_sibling) && + element_has_class (WEBKIT_DOM_ELEMENT (prev_sibling), "-x-evo-temp-text-wrapper")) { + /* Situation when anchors are alone on line */ + gchar *text_content; + + text_content = webkit_dom_node_get_text_content (prev_sibling); + + if (g_str_has_suffix (text_content, "\n")) { + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, + NULL); + g_free (text_content); + node = next_sibling; + skip_node = TRUE; + goto next_node; + } + g_free (text_content); + } + + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (prev_sibling)) { + gchar *quotation, *content; + + quotation = get_quotation_for_level (quote_level); + + content = g_strconcat ( + "<span class=\"-x-evo-quoted\">", + quotation, + "</span><br class=\"-x-evo-temp-br\">", + NULL); + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (node), + content, + NULL); + + g_free (content); + g_free (quotation); + + node = next_sibling; + skip_node = TRUE; + goto next_node; + } + + if (!prev_sibling && !next_sibling) { + WebKitDOMNode *parent = webkit_dom_node_get_parent_node (node); + + if (WEBKIT_DOM_IS_HTML_DIV_ELEMENT (parent)) { + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + } + } + + if (WEBKIT_DOM_IS_ELEMENT (prev_sibling) && + element_has_class (WEBKIT_DOM_ELEMENT (prev_sibling), "-x-evo-temp-text-wrapper")) { + gchar *text_content; + + text_content = webkit_dom_node_get_text_content (prev_sibling); + if (g_strcmp0 (text_content, "") == 0) + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + + g_free (text_content); + } + + if (is_citation_node (prev_sibling)) { + insert_quote_symbols_before_node ( + document, node, quote_level, FALSE); + } + not_br: + if (g_strcmp0 (webkit_dom_node_get_text_content (node), "") == 0) { + move_next = TRUE; + goto next_node; + } + + quote_node (document, node, quote_level); + + move_next = TRUE; + goto next_node; + + with_children: + if (is_citation_node (node)) { + /* Go deeper and increase level */ + quote_plain_text_recursive ( + document, node, start_node, quote_level + 1); + /* set citation as quoted */ + element_add_class ( + WEBKIT_DOM_ELEMENT (node), + "-x-evo-plaintext-quoted"); + move_next = TRUE; + } else { + quote_plain_text_recursive ( + document, node, start_node, quote_level); + move_next = TRUE; + } + next_node: + if (!skip_node) { + /* Move to next node */ + if (!move_next && webkit_dom_node_has_child_nodes (node)) { + node = webkit_dom_node_get_first_child (node); + } else if (webkit_dom_node_get_next_sibling (node)) { + node = webkit_dom_node_get_next_sibling (node); + } else { + return; + } + } + } +} + +static gint +get_citation_level (WebKitDOMNode *node, + gboolean set_plaintext_quoted) +{ + WebKitDOMNode *parent = node; + gint level = 0; + + while (parent && !WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + if (WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (parent) && + webkit_dom_element_has_attribute (WEBKIT_DOM_ELEMENT (parent), "type")) { + level++; + + if (set_plaintext_quoted) { + element_add_class ( + WEBKIT_DOM_ELEMENT (parent), + "-x-evo-plaintext-quoted"); + } + } + + parent = webkit_dom_node_get_parent_node (parent); + } + + return level; +} + +WebKitDOMElement * +e_html_editor_view_quote_plain_text_element (EHTMLEditorView *view, + WebKitDOMElement *element) +{ + WebKitDOMDocument *document; + WebKitDOMNode *element_clone; + WebKitDOMNodeList *list; + gint ii, length, level; + + document = webkit_dom_node_get_owner_document (WEBKIT_DOM_NODE (element)); + + element_clone = webkit_dom_node_clone_node (WEBKIT_DOM_NODE (element), TRUE); + level = get_citation_level (WEBKIT_DOM_NODE (element), TRUE); + + /* Remove old quote characters if the exists */ + list = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (element_clone), "span.-x-evo-quoted", 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_node_remove_child ( + webkit_dom_node_get_parent_node (node), node, NULL); + } + + quote_plain_text_recursive ( + document, element_clone, element_clone, level); + + /* Replace old element with one, that is quoted */ + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (element)), + element_clone, + WEBKIT_DOM_NODE (element), + NULL); + + return WEBKIT_DOM_ELEMENT (element_clone); +} + +/** + * e_html_editor_view_quote_plain_text: + * @view: an #EHTMLEditorView + * + * Quote text inside citation blockquotes in plain text mode. + * + * As this function is cloning and replacing all citation blockquotes keep on + * mind that any pointers to nodes inside these blockquotes will be invalidated. + */ +WebKitDOMElement * +e_html_editor_view_quote_plain_text (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + WebKitDOMNode *body_clone; + WebKitDOMNamedNodeMap *attributes; + WebKitDOMNodeList *list; + WebKitDOMElement *element; + gint ii, length; + gulong attributes_length; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + /* Check if the document is already quoted */ + element = webkit_dom_document_query_selector ( + document, ".-x-evo-plaintext-quoted", NULL); + if (element) + return NULL; + + body = webkit_dom_document_get_body (document); + body_clone = webkit_dom_node_clone_node (WEBKIT_DOM_NODE (body), TRUE); + + /* Clean unwanted spaces before and after blockquotes */ + list = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (body_clone), "blockquote[type|=cite]", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *blockquote = webkit_dom_node_list_item (list, ii); + WebKitDOMNode *prev_sibling = webkit_dom_node_get_previous_sibling (blockquote); + WebKitDOMNode *next_sibling = webkit_dom_node_get_next_sibling (blockquote); + + if (prev_sibling && WEBKIT_DOM_IS_HTMLBR_ELEMENT (prev_sibling)) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (prev_sibling), + prev_sibling, + NULL); + } + if (next_sibling && WEBKIT_DOM_IS_HTMLBR_ELEMENT (next_sibling)) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (next_sibling), + next_sibling, + NULL); + } + if (webkit_dom_node_has_child_nodes (blockquote)) { + WebKitDOMNode *child = webkit_dom_node_get_first_child (blockquote); + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (child)) { + webkit_dom_node_remove_child ( + blockquote, + child, + NULL); + } + } + } + + quote_plain_text_recursive (document, body_clone, body_clone, 0); + + /* Copy attributes */ + attributes = webkit_dom_element_get_attributes (WEBKIT_DOM_ELEMENT (body)); + attributes_length = webkit_dom_named_node_map_get_length (attributes); + for (ii = 0; ii < attributes_length; ii++) { + gchar *name, *value; + WebKitDOMNode *node = webkit_dom_named_node_map_item (attributes, ii); + + name = webkit_dom_node_get_local_name (node); + value = webkit_dom_node_get_node_value (node); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body_clone), name, value, NULL); + + g_free (name); + g_free (value); + } + + /* Replace old BODY with one, that is quoted */ + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (body)), + body_clone, + WEBKIT_DOM_NODE (body), + NULL); + + return WEBKIT_DOM_ELEMENT (body_clone); +} + +/** + * e_html_editor_view_dequote_plain_text: + * @view: an #EHTMLEditorView + * + * Dequote already quoted plain text in editor. + * Editor have to be quoted with e_html_editor_view_quote_plain_text otherwise + * it's not working. + */ +void +e_html_editor_view_dequote_plain_text (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMNodeList *paragraphs; + gint length, ii; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + paragraphs = webkit_dom_document_query_selector_all ( + document, "blockquote.-x-evo-plaintext-quoted", NULL); + length = webkit_dom_node_list_get_length (paragraphs); + for (ii = 0; ii < length; ii++) { + WebKitDOMNodeList *list; + WebKitDOMElement *element; + gint jj, list_length; + + element = WEBKIT_DOM_ELEMENT (webkit_dom_node_list_item (paragraphs, ii)); + + if (is_citation_node (WEBKIT_DOM_NODE (element))) { + element_remove_class (element, "-x-evo-plaintext-quoted"); + + list = webkit_dom_element_query_selector_all ( + element, "span.-x-evo-quoted", NULL); + list_length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < list_length; jj++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, jj); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (node), + node, + NULL); + } + list = webkit_dom_element_query_selector_all ( + element, "span.-x-evo-temp-text-wrapper", NULL); + list_length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < list_length; jj++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, jj); + + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node (node), + webkit_dom_node_get_first_child (node), + node, + NULL); + } + } + } +} + +/** + * e_html_editor_view_get_html_mode: + * @view: an #EHTMLEditorView + * + * Whether the editor is in HTML mode or plain text mode. In HTML mode, + * more formatting options are avilable an the email is sent as + * multipart/alternative. + * + * Returns: @TRUE when HTML mode is enabled, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_html_mode (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->html_mode; +} + +static gint +get_indentation_level (WebKitDOMElement *element) +{ + WebKitDOMElement *parent; + gint level = 1; + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (element)); + /* Count level of indentation */ + while (!WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) { + if (element_has_class (parent, "-x-evo-indented")) + level++; + + parent = webkit_dom_node_get_parent_element (WEBKIT_DOM_NODE (parent)); + } + + return level; +} + +static void +process_blockquote (WebKitDOMElement *blockquote) +{ + WebKitDOMNodeList *list; + int jj, length; + + /* First replace wrappers */ + list = webkit_dom_element_query_selector_all ( + blockquote, "span.-x-evo-temp-text-wrapper", NULL); + length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < length; jj++) { + WebKitDOMNode *quoted_node; + gchar *text_content, *tmp = NULL; + + quoted_node = webkit_dom_node_list_item (list, jj); + text_content = webkit_dom_node_get_text_content (quoted_node); + if (webkit_dom_node_get_previous_sibling (quoted_node)) { + tmp = g_strconcat ("<br>", text_content, NULL); + g_free (text_content); + text_content = tmp; + } + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (quoted_node), text_content, NULL); + + g_free (text_content); + } + + /* Afterwards replace quote nodes with symbols */ + list = webkit_dom_element_query_selector_all ( + blockquote, "span.-x-evo-quoted", NULL); + length = webkit_dom_node_list_get_length (list); + for (jj = 0; jj < length; jj++) { + WebKitDOMNode *quoted_node; + gchar *text_content, *tmp = NULL; + + quoted_node = webkit_dom_node_list_item (list, jj); + text_content = webkit_dom_node_get_text_content (quoted_node); + if (webkit_dom_node_get_previous_sibling (quoted_node)) { + tmp = g_strconcat ("<br>", text_content, NULL); + g_free (text_content); + text_content = tmp; + } + + webkit_dom_html_element_set_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (quoted_node), text_content, NULL); + + g_free (text_content); + } + + if (element_has_class (blockquote, "-x-evo-indented")) { + WebKitDOMNode *child; + gchar *spaces; + + spaces = g_strnfill (4 * get_indentation_level (blockquote), ' '); + + child = webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (blockquote)); + while (child) { + /* If next sibling is indented blockqoute skip it, + * it will be processed afterwards */ + if (WEBKIT_DOM_IS_ELEMENT (child) && + element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-indented")) + child = webkit_dom_node_get_next_sibling (child); + + if (WEBKIT_DOM_IS_TEXT (child)) { + gchar *text_content; + gchar *indented_text; + + text_content = webkit_dom_text_get_whole_text (WEBKIT_DOM_TEXT (child)); + indented_text = g_strconcat (spaces, text_content, NULL); + + webkit_dom_text_replace_whole_text ( + WEBKIT_DOM_TEXT (child), + indented_text, + NULL); + + g_free (text_content); + g_free (indented_text); + } + + if (!child) + break; + + /* Move to next node */ + if (webkit_dom_node_has_child_nodes (child)) + child = webkit_dom_node_get_first_child (child); + else if (webkit_dom_node_get_next_sibling (child)) + child = webkit_dom_node_get_next_sibling (child); + else { + if (webkit_dom_node_is_equal_node (WEBKIT_DOM_NODE (blockquote), child)) + break; + + child = webkit_dom_node_get_parent_node (child); + if (child) + child = webkit_dom_node_get_next_sibling (child); + } + } + g_free (spaces); + + webkit_dom_element_remove_attribute (blockquote, "style"); + } +} + +/* Taken from GtkHTML */ +static gchar * +get_alpha_value (gint value, + gboolean lower) +{ + GString *str; + gchar *rv; + gint add = lower ? 'a' : 'A'; + + str = g_string_new (". "); + + do { + g_string_prepend_c (str, ((value - 1) % 26) + add); + value = (value - 1) / 26; + } while (value); + + rv = str->str; + g_string_free (str, FALSE); + + return rv; +} + +/* Taken from GtkHTML */ +static gchar * +get_roman_value (gint value, + gboolean lower) +{ + GString *str; + const gchar *base = "IVXLCDM"; + gchar *rv; + gint b, r, add = lower ? 'a' - 'A' : 0; + + if (value > 3999) + return g_strdup ("?. "); + + str = g_string_new (". "); + + for (b = 0; value > 0 && b < 7 - 1; b += 2, value /= 10) { + r = value % 10; + if (r != 0) { + if (r < 4) { + for (; r; r--) + g_string_prepend_c (str, base[b] + add); + } else if (r == 4) { + g_string_prepend_c (str, base[b + 1] + add); + g_string_prepend_c (str, base[b] + add); + } else if (r == 5) { + g_string_prepend_c (str, base[b + 1] + add); + } else if (r < 9) { + for (; r > 5; r--) + g_string_prepend_c (str, base[b] + add); + g_string_prepend_c (str, base[b + 1] + add); + } else if (r == 9) { + g_string_prepend_c (str, base[b + 2] + add); + g_string_prepend_c (str, base[b] + add); + } + } + } + + rv = str->str; + g_string_free (str, FALSE); + + return rv; +} + +static void +process_list_to_plain_text (EHTMLEditorView *view, + WebKitDOMElement *element, + gint level, + GString *output) +{ + EHTMLEditorSelectionBlockFormat format; + EHTMLEditorSelectionAlignment alignment; + gint counter = 1; + gchar *indent_per_level = g_strnfill (SPACES_PER_LIST_LEVEL, ' '); + WebKitDOMNode *item; + gint word_wrap_length = e_html_editor_selection_get_word_wrap_length ( + e_html_editor_view_get_selection (view)); + + format = e_html_editor_selection_get_list_format_from_node ( + WEBKIT_DOM_NODE (element)); + + /* Process list items to plain text */ + item = webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (element)); + while (item) { + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (item)) + g_string_append (output, "\n"); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (item)) { + gchar *space, *item_str = NULL; + gint ii = 0; + WebKitDOMElement *wrapped; + GString *item_value = g_string_new (""); + + alignment = e_html_editor_selection_get_list_alignment_from_node ( + WEBKIT_DOM_NODE (item)); + + wrapped = webkit_dom_element_query_selector ( + WEBKIT_DOM_ELEMENT (item), ".-x-evo-wrap-br", NULL); + /* Wrapped text */ + if (wrapped) { + WebKitDOMNode *node = webkit_dom_node_get_first_child (item); + GString *line = g_string_new (""); + while (node) { + if (WEBKIT_DOM_IS_TEXT (node)) { + /* append text from line */ + gchar *text_content; + text_content = webkit_dom_node_get_text_content (node); + g_string_append (line, text_content); + g_free (text_content); + } + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (node) && + element_has_class (WEBKIT_DOM_ELEMENT (node), "-x-evo-wrap-br")) { + /* put spaces before line characters -> wordwraplength - indentation */ + g_string_append (line, "\n"); + /* put spaces before line characters -> wordwraplength - indentation */ + for (ii = 0; ii < level; ii++) + g_string_append (line, indent_per_level); + g_string_append (item_value, line->str); + g_string_erase (line, 0, -1); + } + node = webkit_dom_node_get_next_sibling (node); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT) + g_string_append (item_value, line->str); + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER) { + gchar *fill = NULL; + gint fill_length; + + fill_length = word_wrap_length - g_utf8_strlen (line->str, -1); + fill_length -= ii * SPACES_PER_LIST_LEVEL; + fill_length /= 2; + + if (fill_length < 0) + fill_length = 0; + + fill = g_strnfill (fill_length, ' '); + + g_string_append (item_value, fill); + g_string_append (item_value, line->str); + g_free (fill); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT) { + gchar *fill = NULL; + gint fill_length; + + fill_length = word_wrap_length - g_utf8_strlen (line->str, -1); + fill_length -= ii * SPACES_PER_LIST_LEVEL; + + if (fill_length < 0) + fill_length = 0; + + fill = g_strnfill (fill_length, ' '); + + g_string_append (item_value, fill); + g_string_append (item_value, line->str); + g_free (fill); + } + g_string_free (line, TRUE); + /* that same here */ + } else { + gchar *text_content = + webkit_dom_node_get_text_content (item); + g_string_append (item_value, text_content); + g_free (text_content); + } + + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST) { + space = g_strnfill (SPACES_PER_LIST_LEVEL - 2, ' '); + item_str = g_strdup_printf ( + "%s* %s", space, item_value->str); + g_free (space); + } + + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST) { + gint length = 1, tmp = counter; + + while ((tmp = tmp / 10) > 1) + length++; + + if (tmp == 1) + length++; + + space = g_strnfill (SPACES_PER_LIST_LEVEL - 2 - length, ' '); + item_str = g_strdup_printf ( + "%s%d. %s", space, counter, item_value->str); + g_free (space); + } + + if (format > E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST) { + gchar *value; + + if (format == E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA) + value = get_alpha_value (counter, FALSE); + else + value = get_roman_value (counter, FALSE); + + /* Value already containes dot and space */ + space = g_strnfill (SPACES_PER_LIST_LEVEL - strlen (value), ' '); + item_str = g_strdup_printf ( + "%s%s%s", space, value, item_value->str); + g_free (space); + g_free (value); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_LEFT) { + for (ii = 0; ii < level - 1; ii++) { + g_string_append (output, indent_per_level); + } + g_string_append (output, item_str); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_RIGHT) { + if (!wrapped) { + gchar *fill = NULL; + gint fill_length; + + fill_length = word_wrap_length - g_utf8_strlen (item_str, -1); + fill_length -= ii * SPACES_PER_LIST_LEVEL; + + if (fill_length < 0) + fill_length = 0; + + if (g_str_has_suffix (item_str, " ")) + fill_length++; + + fill = g_strnfill (fill_length, ' '); + + g_string_append (output, fill); + g_free (fill); + } + if (g_str_has_suffix (item_str, " ")) + g_string_append_len (output, item_str, g_utf8_strlen (item_str, -1) - 1); + else + g_string_append (output, item_str); + } + + if (alignment == E_HTML_EDITOR_SELECTION_ALIGNMENT_CENTER) { + if (!wrapped) { + gchar *fill = NULL; + gint fill_length = 0; + + for (ii = 0; ii < level - 1; ii++) + g_string_append (output, indent_per_level); + + fill_length = word_wrap_length - g_utf8_strlen (item_str, -1); + fill_length -= ii * SPACES_PER_LIST_LEVEL; + fill_length /= 2; + + if (fill_length < 0) + fill_length = 0; + + if (g_str_has_suffix (item_str, " ")) + fill_length++; + + fill = g_strnfill (fill_length, ' '); + + g_string_append (output, fill); + g_free (fill); + } + if (g_str_has_suffix (item_str, " ")) + g_string_append_len (output, item_str, g_utf8_strlen (item_str, -1) - 1); + else + g_string_append (output, item_str); + } + + counter++; + item = webkit_dom_node_get_next_sibling (item); + if (item) + g_string_append (output, "\n"); + + g_free (item_str); + g_string_free (item_value, TRUE); + } else if (WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (item) || + WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (item)) { + process_list_to_plain_text ( + view, WEBKIT_DOM_ELEMENT (item), level + 1, output); + item = webkit_dom_node_get_next_sibling (item); + } else { + item = webkit_dom_node_get_next_sibling (item); + } + } + + if (webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (element))) + g_string_append (output, "\n"); + + g_free (indent_per_level); +} + +static void +remove_base_attributes (WebKitDOMElement *element) +{ + webkit_dom_element_remove_attribute (element, "class"); + webkit_dom_element_remove_attribute (element, "id"); + webkit_dom_element_remove_attribute (element, "name"); +} + +static void +remove_evolution_attributes (WebKitDOMElement *element) +{ + webkit_dom_element_remove_attribute (element, "x-evo-smiley"); + webkit_dom_element_remove_attribute (element, "data-converted"); + webkit_dom_element_remove_attribute (element, "data-edit-as-new"); + webkit_dom_element_remove_attribute (element, "data-inline"); + webkit_dom_element_remove_attribute (element, "data-uri"); + webkit_dom_element_remove_attribute (element, "data-message"); + webkit_dom_element_remove_attribute (element, "data-name"); + webkit_dom_element_remove_attribute (element, "data-new-message"); + webkit_dom_element_remove_attribute (element, "spellcheck"); +} +/* +static void +remove_style_attributes (WebKitDOMElement *element) +{ + webkit_dom_element_remove_attribute (element, "bgcolor"); + webkit_dom_element_remove_attribute (element, "background"); + webkit_dom_element_remove_attribute (element, "style"); +} +*/ +static void +process_elements (EHTMLEditorView *view, + WebKitDOMNode *node, + gboolean to_html, + gboolean changing_mode, + gboolean to_plain_text, + GString *buffer) +{ + WebKitDOMNodeList *nodes; + gulong ii, length; + gchar *content; + gboolean skip_nl = FALSE; + + if (to_plain_text && !buffer) + return; + + if (WEBKIT_DOM_IS_HTML_BODY_ELEMENT (node)) { + if (changing_mode && to_plain_text) { + WebKitDOMNamedNodeMap *attributes; + gulong attributes_length; + + /* Copy attributes */ + g_string_append (buffer, "<html><head></head><body "); + attributes = webkit_dom_element_get_attributes ( + WEBKIT_DOM_ELEMENT (node)); + attributes_length = + webkit_dom_named_node_map_get_length (attributes); + + for (ii = 0; ii < attributes_length; ii++) { + gchar *name, *value; + WebKitDOMNode *node = + webkit_dom_named_node_map_item ( + attributes, ii); + + name = webkit_dom_node_get_local_name (node); + value = webkit_dom_node_get_node_value (node); + + g_string_append (buffer, name); + g_string_append (buffer, "=\""); + g_string_append (buffer, value); + g_string_append (buffer, "\" "); + + g_free (name); + g_free (value); + } + g_string_append (buffer, ">"); + } + if (to_html) + remove_evolution_attributes (WEBKIT_DOM_ELEMENT (node)); + } + + nodes = webkit_dom_node_get_child_nodes (node); + length = webkit_dom_node_list_get_length (nodes); + + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *child; + gboolean skip_node = FALSE; + + child = webkit_dom_node_list_item (nodes, ii); + + if (WEBKIT_DOM_IS_TEXT (child)) { + gchar *content, *tmp; + GRegex *regex; + + content = webkit_dom_node_get_text_content (child); + /* Replace tabs with 4 whitespaces, otherwise they got + * replaced by single whitespace */ + if (strstr (content, "\x9")) { + regex = g_regex_new ("\x9", 0, 0, NULL); + tmp = g_regex_replace ( + regex, content, -1, 0, " ", 0, NULL); + webkit_dom_node_set_text_content (child, tmp, NULL); + g_free (content); + g_free (tmp); + content = webkit_dom_node_get_text_content (child); + g_regex_unref (regex); + } + + if (strstr (content, UNICODE_ZERO_WIDTH_SPACE)) { + regex = g_regex_new (UNICODE_ZERO_WIDTH_SPACE, 0, 0, NULL); + tmp = g_regex_replace ( + regex, content, -1, 0, "", 0, NULL); + webkit_dom_node_set_text_content (child, tmp, NULL); + g_free (tmp); + g_free (content); + content = webkit_dom_node_get_text_content (child); + g_regex_unref (regex); + } + + if (to_plain_text && !changing_mode) { + gchar *style; + const gchar *css_align; + + style = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "style"); + + if ((css_align = strstr (style, "text-align: "))) { + gchar *align; + gchar *content_with_align; + gint length; + gint word_wrap_length = + e_html_editor_selection_get_word_wrap_length ( + e_html_editor_view_get_selection (view)); + + if (!g_str_has_prefix (css_align + 12, "left")) { + if (g_str_has_prefix (css_align + 12, "center")) + length = (word_wrap_length - g_utf8_strlen (content, -1)) / 2; + else + length = word_wrap_length - g_utf8_strlen (content, -1); + + if (g_str_has_suffix (content, " ")) { + char *tmp; + + length++; + align = g_strnfill (length, ' '); + + tmp = g_strndup (content, g_utf8_strlen (content, -1) -1); + + content_with_align = g_strconcat ( + align, tmp, NULL); + g_free (tmp); + } else { + align = g_strnfill (length, ' '); + + content_with_align = g_strconcat ( + align, content, NULL); + } + + g_free (content); + g_free (align); + content = content_with_align; + } + } + + g_free (style); + } + + if (to_plain_text || changing_mode) + g_string_append (buffer, content); + + g_free (content); + + goto next; + } + + if (WEBKIT_DOM_IS_COMMENT (child) || !WEBKIT_DOM_IS_ELEMENT (child)) + goto next; + + /* Leave caret position untouched */ + if (element_has_id (WEBKIT_DOM_ELEMENT (child), "-x-evo-caret-position")) { + if (changing_mode && to_plain_text) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + } + if (to_html) { + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (child), + child, + NULL); + } + + skip_node = TRUE; + } + + if (element_has_class (WEBKIT_DOM_ELEMENT (child), "Apple-tab-span")) { + if (!changing_mode && to_plain_text) { + gchar *content, *tmp; + GRegex *regex; + + content = webkit_dom_node_get_text_content (child); + /* Replace tabs with 4 whitespaces, otherwise they got + * replaced by single whitespace */ + if (strstr (content, "\x9")) { + regex = g_regex_new ("\x9", 0, 0, NULL); + tmp = g_regex_replace ( + regex, content, -1, 0, " ", 0, NULL); + g_string_append (buffer, tmp); + g_free (tmp); + content = webkit_dom_node_get_text_content (child); + g_regex_unref (regex); + } + } + if (to_html) { + element_remove_class ( + WEBKIT_DOM_ELEMENT (child), + "Applet-tab-span"); + } + + skip_node = TRUE; + } + + /* Leave blockquotes as they are */ + if (WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (child)) { + if (changing_mode && to_plain_text) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + skip_node = TRUE; + } else { + if (!changing_mode && to_plain_text) { + if (get_citation_level (child, FALSE) == 0) { + gchar *value; + value = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (child), "type"); + if (value && g_strcmp0 (value, "cite") == 0) { + g_string_append (buffer, "\n"); + } + g_free (value); + } + } + process_blockquote (WEBKIT_DOM_ELEMENT (child)); + } + } + + if (WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (child) || + WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (child)) { + if (to_plain_text) { + if (changing_mode) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + } else { + process_list_to_plain_text ( + view, WEBKIT_DOM_ELEMENT (child), 1, buffer); + } + skip_node = TRUE; + goto next; + } + } + + if (element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-resizable-wrapper") && + !element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-smiley-wrapper")) { + WebKitDOMNode *image = + webkit_dom_node_get_first_child (child); + + if (to_html && WEBKIT_DOM_IS_HTML_IMAGE_ELEMENT (image)) { + remove_evolution_attributes ( + WEBKIT_DOM_ELEMENT (image)); + + webkit_dom_node_replace_child ( + node, image, child, NULL); + } + + skip_node = TRUE; + } + + /* Leave paragraphs as they are */ + if (element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-paragraph")) { + if (changing_mode && to_plain_text) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + skip_node = TRUE; + } + if (to_html) { + remove_base_attributes (WEBKIT_DOM_ELEMENT (child)); + remove_evolution_attributes (WEBKIT_DOM_ELEMENT (child)); + } + } + + /* Signature */ + if (WEBKIT_DOM_IS_HTML_DIV_ELEMENT (child)) { + WebKitDOMNode *first_child; + + first_child = webkit_dom_node_get_first_child (child); + if (WEBKIT_DOM_IS_ELEMENT (first_child)) { + if (element_has_class ( + WEBKIT_DOM_ELEMENT (first_child), + "-x-evo-signature")) { + + if (to_html) { + remove_base_attributes ( + WEBKIT_DOM_ELEMENT (first_child)); + remove_evolution_attributes ( + WEBKIT_DOM_ELEMENT (first_child)); + } + if (to_plain_text && !changing_mode) { + g_string_append (buffer, "\n"); + content = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (first_child)); + g_string_append (buffer, content); + g_free (content); + skip_nl = TRUE; + } + skip_node = TRUE; + } + } + } + + /* Replace smileys with their text representation */ + if (element_has_class (WEBKIT_DOM_ELEMENT (child), "-x-evo-smiley-wrapper")) { + if (to_plain_text && !changing_mode) { + WebKitDOMNode *text_version; + + text_version = webkit_dom_node_get_last_child (child); + content = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (text_version)); + g_string_append (buffer, content); + g_free (content); + skip_node = TRUE; + } + if (to_html) { + WebKitDOMElement *img; + + img = WEBKIT_DOM_ELEMENT ( + webkit_dom_node_get_first_child (child)), + + remove_evolution_attributes (img); + remove_base_attributes (img); + + webkit_dom_node_insert_before ( + webkit_dom_node_get_parent_node (child), + WEBKIT_DOM_NODE (img), + child, + NULL); + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (child), + child, + NULL); + skip_node = TRUE; + } + } + + /* Leave PRE elements untouched */ + if (WEBKIT_DOM_IS_HTML_PRE_ELEMENT (child)) { + if (changing_mode && to_plain_text) { + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (child)); + g_string_append (buffer, content); + g_free (content); + skip_node = TRUE; + } + if (to_html) + remove_evolution_attributes (WEBKIT_DOM_ELEMENT (child)); + } + + if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (child)) { + if (to_plain_text) { + /* Insert new line when we hit BR element */ + g_string_append (buffer, changing_mode ? "<br>" : "\n"); + } + } + next: + if (webkit_dom_node_has_child_nodes (child) && !skip_node) + process_elements ( + view, child, to_html, changing_mode, to_plain_text, buffer); + } + + if (to_plain_text && ( + WEBKIT_DOM_IS_HTML_DIV_ELEMENT (node) || + WEBKIT_DOM_IS_HTML_PARAGRAPH_ELEMENT (node) || + WEBKIT_DOM_IS_HTML_PRE_ELEMENT (node) || + WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (node))) { + + gboolean add_br = TRUE; + WebKitDOMNode *next_sibling = webkit_dom_node_get_next_sibling (node); + + /* If we don't have next sibling (last element in body) or next element is + * signature we are not adding the BR element */ + if (!next_sibling) + add_br = FALSE; + + if (element_has_class (webkit_dom_node_get_parent_element (node), "-x-evo-indented")) + add_br = FALSE; + + if (next_sibling && WEBKIT_DOM_IS_HTML_DIV_ELEMENT (next_sibling)) { + if (webkit_dom_element_query_selector ( + WEBKIT_DOM_ELEMENT (next_sibling), + "span.-x-evo-signature", NULL)) { + + add_br = FALSE; + } + } + + content = webkit_dom_node_get_text_content (node); + if (add_br && g_utf8_strlen (content, -1) > 0 && !skip_nl) + g_string_append (buffer, changing_mode ? "<br>" : "\n"); + + g_free (content); + } + +} + +static void +remove_wrapping (EHTMLEditorView *view) +{ + gint length; + gint ii; + WebKitDOMDocument *document; + WebKitDOMNodeList *list; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + list = webkit_dom_document_query_selector_all (document, "br.-x-evo-wrap-br", 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_node_remove_child ( + webkit_dom_node_get_parent_node (node), node, NULL); + } +} + +static void +remove_images_in_element (EHTMLEditorView *view, + WebKitDOMElement *element, + gboolean html_mode) +{ + gint length, ii; + WebKitDOMNodeList *images; + + images = webkit_dom_element_query_selector_all ( + element, "img:not(.-x-evo-smiley-img)", NULL); + + length = webkit_dom_node_list_get_length (images); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *img = webkit_dom_node_list_item (images, ii); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (img), img, NULL); + } +} + +static void +remove_images (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + remove_images_in_element ( + view, + WEBKIT_DOM_ELEMENT (webkit_dom_document_get_body (document)), + view->priv->html_mode); +} + +static void +toggle_smileys (EHTMLEditorView *view) +{ + gboolean html_mode; + gint length; + gint ii; + WebKitDOMDocument *document; + WebKitDOMNodeList *smileys; + + html_mode = e_html_editor_view_get_html_mode (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + smileys = webkit_dom_document_query_selector_all ( + document, "img.-x-evo-smiley-img", NULL); + + length = webkit_dom_node_list_get_length (smileys); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *img = webkit_dom_node_list_item (smileys, ii); + WebKitDOMNode *text = webkit_dom_node_get_next_sibling (img); + WebKitDOMElement *parent = webkit_dom_node_get_parent_element (img); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (html_mode ? text : img), + "style", + "display: none", + NULL); + + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (html_mode ? img : text), "style"); + + if (html_mode) + element_add_class (parent, "-x-evo-resizable-wrapper"); + else + element_remove_class (parent, "-x-evo-resizable-wrapper"); + } +} + +static void +toggle_paragraphs_style_in_element (EHTMLEditorView *view, + WebKitDOMElement *element, + gboolean html_mode) +{ + EHTMLEditorSelection *selection; + gint ii, length; + WebKitDOMNodeList *paragraphs; + + html_mode = e_html_editor_view_get_html_mode (view); + selection = e_html_editor_view_get_selection (view); + + paragraphs = webkit_dom_element_query_selector_all ( + element, ".-x-evo-paragraph", NULL); + + length = webkit_dom_node_list_get_length (paragraphs); + + for (ii = 0; ii < length; ii++) { + gchar *style; + const gchar *css_align; + WebKitDOMNode *node = webkit_dom_node_list_item (paragraphs, ii); + + if (html_mode) { + style = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "style"); + + if ((css_align = strstr (style, "text-align: "))) { + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (node), + "style", + g_str_has_prefix (css_align + 12, "center") ? + "text-align: center" : + "text-align: right", + NULL); + } else { + /* In HTML mode the paragraphs don't have width limit */ + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (node), "style"); + } + g_free (style); + } else { + WebKitDOMNode *parent; + + parent = webkit_dom_node_get_parent_node (node); + /* If the paragraph is inside indented paragraph don't set + * the style as it will be inherited */ + if (!element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-indented")) { + style = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "style"); + + if ((css_align = strstr (style, "text-align: "))) { + const gchar *style_to_add; + + style_to_add = g_str_has_prefix ( + css_align + 12, "center") ? + "text-align: center;" : + "text-align: right;"; + + /* In HTML mode the paragraphs have width limit */ + e_html_editor_selection_set_paragraph_style ( + selection, WEBKIT_DOM_ELEMENT (node), + -1, 0, style_to_add); + } + g_free (style); + } + } + } +} + +static void +toggle_paragraphs_style (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + toggle_paragraphs_style_in_element ( + view, + WEBKIT_DOM_ELEMENT (webkit_dom_document_get_body (document)), + view->priv->html_mode); +} + +static gchar * +process_content_for_saving_as_draft (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + WebKitDOMElement *element, *document_element; + gchar *content; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + element = webkit_dom_document_get_element_by_id ( + document, "-x-evo-caret-position"); + + if (element) + webkit_dom_element_set_attribute ( + element, "style", "display: none; color: red;", NULL); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-evo-draft", "", NULL); + + document_element = webkit_dom_document_get_document_element (document); + content = webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (document_element)); + + webkit_dom_element_remove_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-evo-draft"); + + if (element) + webkit_dom_element_set_attribute ( + element, "style", "color: red;", NULL); + + return content; +} + +static gchar * +process_content_for_mode_change (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMNode *body; + GString *plain_text; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)); + + plain_text = g_string_sized_new (1024); + + process_elements (view, body, FALSE, TRUE, TRUE, plain_text); + + g_string_append (plain_text, "</body></html>"); + + return g_string_free (plain_text, FALSE); +} + +static void +convert_element_from_html_to_plain_text (EHTMLEditorView *view, + WebKitDOMElement *element, + gboolean *wrap, + gboolean *quote) +{ + EHTMLEditorSelection *selection; + gint blockquotes_count; + gchar *inner_text, *inner_html; + gboolean restore = TRUE; + WebKitDOMDocument *document; + WebKitDOMElement *top_signature, *signature, *blockquote, *main_blockquote; + WebKitDOMNode *signature_clone, *from; + + selection = e_html_editor_view_get_selection (view); + + document = webkit_dom_node_get_owner_document (WEBKIT_DOM_NODE (element)); + + top_signature = webkit_dom_element_query_selector ( + element, ".-x-evo-top-signature", NULL); + signature = webkit_dom_element_query_selector ( + element, "span.-x-evo-signature", NULL); + main_blockquote = webkit_dom_element_query_selector ( + element, "#-x-evo-main-cite", NULL); + + blockquote = webkit_dom_document_create_element ( + document, "blockquote", NULL); + + if (main_blockquote) { + WebKitDOMElement *input_start; + + webkit_dom_element_set_attribute ( + blockquote, "type", "cite", NULL); + + input_start = webkit_dom_element_query_selector ( + element, "#-x-evo-input-start", NULL); + + restore = input_start ? TRUE : FALSE; + + if (input_start) { + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (input_start), + e_html_editor_selection_get_caret_position_node ( + document), + NULL); + } + from = WEBKIT_DOM_NODE (main_blockquote); + } else { + if (signature) { + signature_clone = webkit_dom_node_clone_node ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)), + TRUE); + + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (element), + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (signature)), + NULL); + } + from = WEBKIT_DOM_NODE (element); + } + + blockquotes_count = create_text_markers_for_citations_in_element ( + WEBKIT_DOM_ELEMENT (from)); + + inner_text = webkit_dom_html_element_get_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (from)); + + webkit_dom_html_element_set_inner_text ( + WEBKIT_DOM_HTML_ELEMENT (blockquote), inner_text, NULL); + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (blockquote)); + + parse_html_into_paragraphs ( + view, document, + main_blockquote ? blockquote : WEBKIT_DOM_ELEMENT (element), + inner_html, + FALSE); + + if (main_blockquote) { + webkit_dom_node_replace_child ( + webkit_dom_node_get_parent_node ( + WEBKIT_DOM_NODE (main_blockquote)), + WEBKIT_DOM_NODE (blockquote), + WEBKIT_DOM_NODE (main_blockquote), + NULL); + + remove_evolution_attributes (WEBKIT_DOM_ELEMENT (element)); + *wrap = TRUE; + } else { + WebKitDOMNode *first_child; + + if (signature) { + if (!top_signature) { + signature_clone = webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (element), + signature_clone, + NULL); + } else { + webkit_dom_node_insert_before ( + WEBKIT_DOM_NODE (element), + signature_clone, + webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (element)), + NULL); + } + } + + first_child = webkit_dom_node_get_first_child ( + WEBKIT_DOM_NODE (element)); + + if (!webkit_dom_node_has_child_nodes (first_child)) { + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (first_child), + UNICODE_ZERO_WIDTH_SPACE, + NULL); + } + webkit_dom_node_insert_before ( + first_child, + e_html_editor_selection_get_caret_position_node ( + document), + webkit_dom_node_get_first_child (first_child), + NULL); + + *wrap = TRUE; + } + + *quote = main_blockquote || blockquotes_count > 0; + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (element), "data-converted", "", NULL); + + g_free (inner_text); + g_free (inner_html); + + if (restore) + e_html_editor_selection_restore_caret_position (selection); +} + +static gchar * +process_content_for_plain_text (EHTMLEditorView *view) +{ + gboolean converted, wrap = FALSE, quote = FALSE, clean = FALSE; + gint length, ii; + GString *plain_text; + WebKitDOMDocument *document; + WebKitDOMNode *body, *source; + WebKitDOMNodeList *paragraphs; + + plain_text = g_string_sized_new (1024); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)); + converted = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-converted"); + source = webkit_dom_node_clone_node (WEBKIT_DOM_NODE (body), TRUE); + + /* If composer is in HTML mode we have to move the content to plain version */ + if (view->priv->html_mode) { + if (converted) { + toggle_paragraphs_style_in_element ( + view, WEBKIT_DOM_ELEMENT (source), FALSE); + remove_images_in_element ( + view, WEBKIT_DOM_ELEMENT (source), FALSE); + } else { + gchar *inner_html; + WebKitDOMElement *div; + + inner_html = webkit_dom_html_element_get_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (body)); + + div = webkit_dom_document_create_element ( + document, "div", NULL); + + webkit_dom_html_element_set_inner_html ( + WEBKIT_DOM_HTML_ELEMENT (div), inner_html, NULL); + + webkit_dom_node_append_child ( + WEBKIT_DOM_NODE (body), + WEBKIT_DOM_NODE (div), + NULL); + + paragraphs = webkit_dom_element_query_selector_all ( + div, "#-x-evo-input-start", NULL); + + length = webkit_dom_node_list_get_length (paragraphs); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *paragraph; + + paragraph = webkit_dom_node_list_item (paragraphs, ii); + + webkit_dom_element_set_id ( + WEBKIT_DOM_ELEMENT (paragraph), ""); + } + + convert_element_from_html_to_plain_text ( + view, div, &wrap, "e); + + source = WEBKIT_DOM_NODE (div); + + clean = TRUE; + } + } + + paragraphs = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (source), ".-x-evo-paragraph", NULL); + + length = webkit_dom_node_list_get_length (paragraphs); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *paragraph; + + paragraph = webkit_dom_node_list_item (paragraphs, ii); + + if (WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (paragraph) || + WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (paragraph)) { + WebKitDOMNode *item = webkit_dom_node_get_first_child (paragraph); + + while (item) { + WebKitDOMNode *next_item = + webkit_dom_node_get_next_sibling (item); + + if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (item)) { + e_html_editor_selection_wrap_paragraph ( + e_html_editor_view_get_selection (view), + WEBKIT_DOM_ELEMENT (item)); + } + item = next_item; + } + } else { + e_html_editor_selection_wrap_paragraph ( + e_html_editor_view_get_selection (view), + WEBKIT_DOM_ELEMENT (paragraph)); + } + } + + paragraphs = webkit_dom_element_query_selector_all ( + WEBKIT_DOM_ELEMENT (source), + "span[id^=\"-x-evo-selection-\"], span#-x-evo-caret-position", + NULL); + + length = webkit_dom_node_list_get_length (paragraphs); + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (paragraphs, ii); + WebKitDOMNode *parent = webkit_dom_node_get_parent_node (node); + + webkit_dom_node_remove_child (parent, node, NULL); + webkit_dom_node_normalize (parent); + } + + if (view->priv->html_mode || quote) + quote_plain_text_recursive (document, source, source, 0); + + process_elements (view, source, FALSE, FALSE, TRUE, plain_text); + + if (clean) + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (body), source, NULL); + + /* Return text content between <body> and </body> */ + return g_string_free (plain_text, FALSE); +} + +static gchar * +process_content_for_html (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMNode *body; + WebKitDOMElement *element; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)); + process_elements (view, body, TRUE, FALSE, FALSE, NULL); + element = webkit_dom_document_get_document_element (document); + return webkit_dom_html_element_get_outer_html ( + WEBKIT_DOM_HTML_ELEMENT (element)); +} + +static gboolean +show_lose_formatting_dialog (EHTMLEditorView *view) +{ + gint result; + GtkWidget *toplevel, *dialog; + GtkWindow *parent = NULL; + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view)); + + if (GTK_IS_WINDOW (toplevel)) + parent = GTK_WINDOW (toplevel); + + dialog = gtk_message_dialog_new ( + parent, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_NONE, + _("Turning HTML mode off will cause the text " + "to lose all formatting. Do you want to continue?")); + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + _("_Don't lose formatting"), GTK_RESPONSE_CANCEL, + _("_Lose formatting"), GTK_RESPONSE_OK, + NULL); + + result = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (result != GTK_RESPONSE_OK) { + gtk_widget_destroy (dialog); + /* Nothing has changed, but notify anyway */ + g_object_notify (G_OBJECT (view), "html-mode"); + return FALSE; + } + + gtk_widget_destroy (dialog); + + return TRUE; +} + +static void +clear_attributes (WebKitDOMDocument *document) +{ + gint length, ii; + WebKitDOMNamedNodeMap *attributes; + WebKitDOMHTMLElement *body = webkit_dom_document_get_body (document); + WebKitDOMHTMLHeadElement *head = webkit_dom_document_get_head (document); + WebKitDOMElement *document_element = + webkit_dom_document_get_document_element (document); + + /* Remove all attributes from HTML element */ + attributes = webkit_dom_element_get_attributes (document_element); + length = webkit_dom_named_node_map_get_length (attributes); + for (ii = length - 1; ii >= 0; ii--) { + WebKitDOMNode *node = webkit_dom_named_node_map_item (attributes, ii); + + webkit_dom_element_remove_attribute_node ( + document_element, WEBKIT_DOM_ATTR (node), NULL); + } + + /* Remove everything from HEAD element */ + while (webkit_dom_node_has_child_nodes (WEBKIT_DOM_NODE (head))) { + webkit_dom_node_remove_child ( + WEBKIT_DOM_NODE (head), + webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (head)), + NULL); + } + + /* Remove non Evolution attributes from BODY element */ + attributes = webkit_dom_element_get_attributes (WEBKIT_DOM_ELEMENT (body)); + length = webkit_dom_named_node_map_get_length (attributes); + for (ii = length - 1; ii >= 0; 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, "data-") || + g_str_has_prefix (name, "data-inline") || + g_str_has_prefix (name, "data-name")) { + webkit_dom_element_remove_attribute_node ( + WEBKIT_DOM_ELEMENT (body), + WEBKIT_DOM_ATTR (node), + NULL); + } + + g_free (name); + } +} + +static void +convert_when_changing_composer_mode (EHTMLEditorView *view) +{ + EHTMLEditorSelection *selection; + gboolean quote = FALSE, wrap = FALSE; + WebKitDOMDocument *document; + WebKitDOMHTMLElement *body; + + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + convert_element_from_html_to_plain_text ( + view, WEBKIT_DOM_ELEMENT (body), &wrap, "e); + + if (wrap) + e_html_editor_selection_wrap_paragraphs_in_document (selection, document); + + if (quote) { + e_html_editor_selection_save_caret_position (selection); + body = WEBKIT_DOM_HTML_ELEMENT (e_html_editor_view_quote_plain_text (view)); + e_html_editor_selection_restore_caret_position (selection); + } + + clear_attributes (document); + + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-converted", "", NULL); + + /* Update fonts - in plain text we only want monospace */ + e_html_editor_view_update_fonts (view); + + e_html_editor_view_force_spell_check (view); +} + +/** + * e_html_editor_view_set_html_mode: + * @view: an #EHTMLEditorView + * @html_mode: @TRUE to enable HTML mode, @FALSE to enable plain text mode + * + * When switching from HTML to plain text mode, user will be prompted whether + * he/she really wants to switch the mode and lose all formatting. When user + * declines, the property is not changed. When they accept, the all formatting + * is lost. + */ +void +e_html_editor_view_set_html_mode (EHTMLEditorView *view, + gboolean html_mode) +{ + EHTMLEditorSelection *selection; + gboolean is_from_new_message, converted, edit_as_new, message, convert; + gboolean reply, hide; + WebKitDOMElement *blockquote; + WebKitDOMHTMLElement *body; + WebKitDOMDocument *document; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + selection = e_html_editor_view_get_selection (view); + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + body = webkit_dom_document_get_body (document); + + is_from_new_message = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-new-message"); + converted = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-converted"); + edit_as_new = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-edit-as-new"); + message = webkit_dom_element_has_attribute ( + WEBKIT_DOM_ELEMENT (body), "data-message"); + + reply = !is_from_new_message && !edit_as_new && message; + hide = !reply && !converted; + + convert = message && ((!hide && reply && !converted) || (edit_as_new && !converted)); + + /* If toggling from HTML to plain text mode, ask user first */ + if (convert && view->priv->html_mode && !html_mode) { + if (!show_lose_formatting_dialog (view)) + return; + + view->priv->html_mode = html_mode; + + convert_when_changing_composer_mode (view); + + goto out; + } + + if (html_mode == view->priv->html_mode) + return; + + view->priv->html_mode = html_mode; + + /* Update fonts - in plain text we only want monospace */ + e_html_editor_view_update_fonts (view); + + blockquote = webkit_dom_document_query_selector ( + document, "blockquote[type|=cite]", NULL); + + if (view->priv->html_mode) { + if (blockquote) + e_html_editor_view_dequote_plain_text (view); + + toggle_paragraphs_style (view); + toggle_smileys (view); + remove_wrapping (view); + } else { + gchar *plain; + + /* Save caret position -> it will be restored in e-composer-private.c */ + e_html_editor_selection_save_caret_position (selection); + + if (blockquote) { + e_html_editor_selection_wrap_paragraphs_in_document ( + selection, document); + e_html_editor_view_quote_plain_text (view); + } + + toggle_paragraphs_style (view); + toggle_smileys (view); + remove_images (view); + + plain = process_content_for_mode_change (view); + + if (*plain) + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), plain, NULL, NULL, "file://"); + + g_free (plain); + } + + out: + g_object_notify (G_OBJECT (view), "html-mode"); +} + +/** + * e_html_editor_view_get_inline_spelling: + * @view: an #EHTMLEditorView + * + * Returns whether automatic spellchecking is enabled or not. When enabled, + * editor will perform spellchecking as user is typing. Otherwise spellcheck + * has to be run manually from menu. + * + * Returns: @TRUE when automatic spellchecking is enabled, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_inline_spelling (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->inline_spelling; +} + +/** + * e_html_editor_view_set_inline_spelling: + * @view: an #EHTMLEditorView + * @inline_spelling: @TRUE to enable automatic spellchecking, @FALSE otherwise + * + * Enables or disables automatic spellchecking. + */ +void +e_html_editor_view_set_inline_spelling (EHTMLEditorView *view, + gboolean inline_spelling) +{ + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + if (view->priv->inline_spelling == inline_spelling) + return; + + view->priv->inline_spelling = inline_spelling; + + g_object_notify (G_OBJECT (view), "inline-spelling"); +} + +/** + * e_html_editor_view_get_magic_links: + * @view: an #EHTMLEditorView + * + * Returns whether automatic links conversion is enabled. When enabled, the editor + * will automatically convert any HTTP links into clickable HTML links. + * + * Returns: @TRUE when magic links are enabled, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_magic_links (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->magic_links; +} + +/** + * e_html_editor_view_set_magic_links: + * @view: an #EHTMLEditorView + * @magic_links: @TRUE to enable magic links, @FALSE to disable them + * + * Enables or disables automatic links conversion. + */ +void +e_html_editor_view_set_magic_links (EHTMLEditorView *view, + gboolean magic_links) +{ + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + if (view->priv->magic_links == magic_links) + return; + + view->priv->magic_links = magic_links; + + g_object_notify (G_OBJECT (view), "magic-links"); +} + +/** + * e_html_editor_view_get_magic_smileys: + * @view: an #EHTMLEditorView + * + * Returns whether automatic conversion of smileys is enabled or disabled. When + * enabled, the editor will automatically convert text smileys ( :-), ;-),...) + * into images. + * + * Returns: @TRUE when magic smileys are enabled, @FALSE otherwise. + */ +gboolean +e_html_editor_view_get_magic_smileys (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), FALSE); + + return view->priv->magic_smileys; +} + +/** + * e_html_editor_view_set_magic_smileys: + * @view: an #EHTMLEditorView + * @magic_smileys: @TRUE to enable magic smileys, @FALSE to disable them + * + * Enables or disables magic smileys. + */ +void +e_html_editor_view_set_magic_smileys (EHTMLEditorView *view, + gboolean magic_smileys) +{ + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + if (view->priv->magic_smileys == magic_smileys) + return; + + view->priv->magic_smileys = magic_smileys; + + g_object_notify (G_OBJECT (view), "magic-smileys"); +} + +/** + * e_html_editor_view_get_spell_checker: + * @view: an #EHTMLEditorView + * + * Returns an #ESpellChecker object that is used to perform spellchecking. + * + * Returns: An always-valid #ESpellChecker object + */ +ESpellChecker * +e_html_editor_view_get_spell_checker (EHTMLEditorView *view) +{ + return E_SPELL_CHECKER (webkit_get_text_checker ()); +} + +/** + * e_html_editor_view_get_text_html: + * @view: an #EHTMLEditorView: + * + * Returns processed HTML content of the editor document (with elements attributes + * used in Evolution composer) + * + * Returns: A newly allocated string + */ +gchar * +e_html_editor_view_get_text_html (EHTMLEditorView *view) +{ + return process_content_for_html (view); +} + +/** + * e_html_editor_view_get_text_html_for_drafts: + * @view: an #EHTMLEditorView: + * + * Returns HTML content of the editor document (without elements attributes + * used in Evolution composer) + * + * Returns: A newly allocated string + */ +gchar * +e_html_editor_view_get_text_html_for_drafts (EHTMLEditorView *view) +{ + return process_content_for_saving_as_draft (view); +} + +/** + * e_html_editor_view_get_text_plain: + * @view: an #EHTMLEditorView + * + * Returns plain text content of the @view. The algorithm removes any + * formatting or styles from the document and keeps only the text and line + * breaks. + * + * Returns: A newly allocated string with plain text content of the document. + */ +gchar * +e_html_editor_view_get_text_plain (EHTMLEditorView *view) +{ + return process_content_for_plain_text (view); +} + +static void +convert_and_load_html_to_plain_text (EHTMLEditorView *view, + const gchar *html) +{ + view->priv->convertor_insert = FALSE; + + webkit_web_view_load_string ( + view->priv->convertor_web_view, html, NULL, NULL, "file://"); +} + +void +e_html_editor_view_convert_and_insert_html_to_plain_text (EHTMLEditorView *view, + const gchar *html) +{ + view->priv->convertor_insert = TRUE; + + webkit_web_view_load_string ( + view->priv->convertor_web_view, html, NULL, NULL, "file://"); +} + +/** + * e_html_editor_view_set_text_html: + * @view: an #EHTMLEditorView + * @text: HTML code to load into the editor + * + * Loads given @text into the editor, destroying any content already present. + */ +void +e_html_editor_view_set_text_html (EHTMLEditorView *view, + const gchar *text) +{ + view->priv->reload_in_progress = TRUE; + + /* Only convert messages that are in HTML */ + if (!view->priv->html_mode && *text && !strstr (text, "data-evo-draft")) { + if (strstr (text, "<!-- text/html -->")) { + if (!show_lose_formatting_dialog (view)) { + e_html_editor_view_set_html_mode (view, TRUE); + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), text, NULL, NULL, "file://"); + return; + } + convert_and_load_html_to_plain_text (view, text); + } else { + convert_and_load_html_to_plain_text (view, text); + } + } else { + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), text, NULL, NULL, "file://"); + } +} + +/** + * e_html_editor_view_set_text_plain: + * @view: an #EHTMLEditorView + * @text: A plain text to load into the editor + * + * Loads given @text into the editor, destryoing any content already present. + */ +void +e_html_editor_view_set_text_plain (EHTMLEditorView *view, + const gchar *text) +{ + view->priv->reload_in_progress = TRUE; + + webkit_web_view_load_string ( + WEBKIT_WEB_VIEW (view), text, "text/plain", NULL, "file://"); +} + +/** + * e_html_editor_view_paste_clipboard_quoted: + * @view: an #EHTMLEditorView + * + * Pastes current content of clipboard into the editor as quoted text + */ +void +e_html_editor_view_paste_clipboard_quoted (EHTMLEditorView *view) +{ + EHTMLEditorViewClass *class; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + class = E_HTML_EDITOR_VIEW_GET_CLASS (view); + g_return_if_fail (class->paste_clipboard_quoted != NULL); + + class->paste_clipboard_quoted (view); +} + +void +e_html_editor_view_embed_styles (EHTMLEditorView *view) +{ + WebKitWebSettings *settings; + WebKitDOMDocument *document; + WebKitDOMElement *sheet; + gchar *stylesheet_uri; + gchar *stylesheet_content; + const gchar *stylesheet; + gsize length; + + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (view)); + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + + g_object_get ( + G_OBJECT (settings), + "user-stylesheet-uri", &stylesheet_uri, + NULL); + + stylesheet = strstr (stylesheet_uri, ","); + stylesheet_content = (gchar *) g_base64_decode (stylesheet, &length); + g_free (stylesheet_uri); + + if (length == 0) { + g_free (stylesheet_content); + return; + } + + e_web_view_create_and_add_css_style_sheet (document, "-x-evo-composer-sheet"); + + sheet = webkit_dom_document_get_element_by_id (document, "-x-evo-composer-sheet"); + webkit_dom_element_set_attribute ( + sheet, + "type", + "text/css", + NULL); + + webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (sheet), stylesheet_content, NULL); + + g_free (stylesheet_content); +} + +void +e_html_editor_view_remove_embed_styles (EHTMLEditorView *view) +{ + WebKitDOMDocument *document; + WebKitDOMElement *sheet; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + sheet = webkit_dom_document_get_element_by_id ( + document, "-x-evo-composer-sheet"); + + webkit_dom_node_remove_child ( + webkit_dom_node_get_parent_node (WEBKIT_DOM_NODE (sheet)), + WEBKIT_DOM_NODE (sheet), + NULL); +} + +static const gchar * +citation_color_level_1 (void) +{ + return "rgb(114,159,207)"; /* Sky Blue 1 */ +} + +static const gchar * +citation_color_level_2 (void) +{ + return "rgb(173,127,168)"; /* Plum 1 */ +} + +static const gchar * +citation_color_level_3 (void) +{ + return "rgb(138,226,52)"; /* Chameleon 1 */ +} + +static const gchar * +citation_color_level_4 (void) +{ + return "rgb(252,175,62)"; /* Orange 1 */ +} + +static const gchar * +citation_color_level_5 (void) +{ + return "rgb(233,185,110)"; /* Chocolate 1 */ +} + +/** + * e_html_editor_view_update_fonts: + * @view: an #EHTMLEditorView + * + * Forces the editor to reload font settings from WebKitWebSettings and apply + * it on the content of the editor document. + */ +void +e_html_editor_view_update_fonts (EHTMLEditorView *view) +{ + GString *stylesheet; + gchar *base64; + gchar *aa = NULL; + WebKitWebSettings *settings; + PangoFontDescription *ms, *vw; + const gchar *styles[] = { "normal", "oblique", "italic" }; + const gchar *smoothing = NULL; + GtkStyleContext *context; + GdkColor *link = NULL; + GdkColor *visited = NULL; + gchar *font; + + font = g_settings_get_string ( + view->priv->font_settings, + "monospace-font-name"); + ms = pango_font_description_from_string ( + font ? font : "monospace 10"); + g_free (font); + + if (view->priv->html_mode) { + font = g_settings_get_string ( + view->priv->font_settings, + "font-name"); + vw = pango_font_description_from_string ( + font ? font : "serif 10"); + g_free (font); + } else { + /* When in plain text mode, force monospace font */ + vw = pango_font_description_copy (ms); + } + + stylesheet = g_string_new (""); + g_string_append_printf ( + stylesheet, + "body {\n" + " font-family: '%s';\n" + " font-size: %dpt;\n" + " font-weight: %d;\n" + " font-style: %s;\n", + pango_font_description_get_family (vw), + pango_font_description_get_size (vw) / PANGO_SCALE, + pango_font_description_get_weight (vw), + styles[pango_font_description_get_style (vw)]); + + if (view->priv->aliasing_settings != NULL) + aa = g_settings_get_string ( + view->priv->aliasing_settings, "antialiasing"); + + if (g_strcmp0 (aa, "none") == 0) + smoothing = "none"; + else if (g_strcmp0 (aa, "grayscale") == 0) + smoothing = "antialiased"; + else if (g_strcmp0 (aa, "rgba") == 0) + smoothing = "subpixel-antialiased"; + + if (smoothing != NULL) + g_string_append_printf ( + stylesheet, + " -webkit-font-smoothing: %s;\n", + smoothing); + + g_free (aa); + + g_string_append (stylesheet, "}\n"); + + g_string_append_printf ( + stylesheet, + "pre,code,.pre {\n" + " font-family: '%s';\n" + " font-size: %dpt;\n" + " font-weight: %d;\n" + " font-style: %s;\n" + "}", + pango_font_description_get_family (ms), + pango_font_description_get_size (ms) / PANGO_SCALE, + pango_font_description_get_weight (ms), + styles[pango_font_description_get_style (ms)]); + + context = gtk_widget_get_style_context (GTK_WIDGET (view)); + gtk_style_context_get_style ( + context, + "link-color", &link, + "visited-link-color", &visited, + NULL); + + if (link == NULL) { + link = g_slice_new0 (GdkColor); + link->blue = G_MAXINT16; + } + + if (visited == NULL) { + visited = g_slice_new0 (GdkColor); + visited->red = G_MAXINT16; + } + + g_string_append_printf ( + stylesheet, + "a {\n" + " color: #%06x;\n" + "}\n" + "a:visited {\n" + " color: #%06x;\n" + "}\n", + e_color_to_value (link), + e_color_to_value (visited)); + + /* See bug #689777 for details */ + g_string_append ( + stylesheet, + "p,pre,code,address {\n" + " margin: 0;\n" + "}\n" + "h1,h2,h3,h4,h5,h6 {\n" + " margin-top: 0.2em;\n" + " margin-bottom: 0.2em;\n" + "}\n"); + + g_string_append ( + stylesheet, + "img " + "{\n" + " height: inherit; \n" + " width: inherit; \n" + "}\n"); + + g_string_append ( + stylesheet, + "span.-x-evo-resizable-wrapper " + "{\n" + " resize: both; \n" + " overflow: hidden; \n" + " display: inline-block; \n" + "}\n"); + + g_string_append ( + stylesheet, + "span.-x-evo-resizable-wrapper:hover " + "{\n" + " outline: 1px dashed red; \n" + "}\n"); + + g_string_append ( + stylesheet, + "ul,ol " + "{\n" + " -webkit-padding-start: 7ch; \n" + "}\n"); + + g_string_append ( + stylesheet, + ".-x-evo-list-item-alignt-left " + "{\n" + " text-align: left; \n" + "}\n"); + + g_string_append ( + stylesheet, + ".-x-evo-list-item-align-center " + "{\n" + " text-align: center; \n" + " -webkit-padding-start: 0ch; \n" + " margin-left: -3ch; \n" + " margin-right: 1ch; \n" + " list-style-position: inside; \n" + "}\n"); + + g_string_append ( + stylesheet, + ".-x-evo-list-item-align-right " + "{\n" + " text-align: right; \n" + " -webkit-padding-start: 0ch; \n" + " margin-left: -3ch; \n" + " margin-right: 1ch; \n" + " list-style-position: inside; \n" + "}\n"); + + g_string_append ( + stylesheet, + "ol,ul " + "{\n" + " -webkit-margin-before: 0em; \n" + " -webkit-margin-after: 0em; \n" + "}\n"); + + g_string_append ( + stylesheet, + "blockquote " + "{\n" + " -webkit-margin-before: 0em; \n" + " -webkit-margin-after: 0em; \n" + "}\n"); + + g_string_append ( + stylesheet, + "blockquote[type=cite] " + "{\n" + " padding: 0.0ex 0ex;\n" + " margin: 0ex;\n" + " -webkit-margin-start: 0em; \n" + " -webkit-margin-end : 0em; \n" + " color: #737373 !important;\n" + "}\n"); + + g_string_append_printf ( + stylesheet, + ".quote_character " + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_1 ()); + + g_string_append_printf ( + stylesheet, + ".quote_character+" + ".quote_character" + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_2 ()); + + g_string_append_printf ( + stylesheet, + ".quote_character+" + ".quote_character+" + ".quote_character" + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_3 ()); + + g_string_append_printf ( + stylesheet, + ".quote_character+" + ".quote_character+" + ".quote_character+" + ".quote_character" + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_4 ()); + + g_string_append_printf ( + stylesheet, + ".quote_character+" + ".quote_character+" + ".quote_character+" + ".quote_character+" + ".quote_character" + "{\n" + " color: %s;\n" + "}\n", + citation_color_level_5 ()); + + g_string_append ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " padding: 0.4ex 1ex;\n" + " margin: 1ex;\n" + " border-width: 0px 2px 0px 2px;\n" + " border-style: none solid none solid;\n" + " border-radius: 2px;\n" + "}\n"); + + /* Block quote border colors are borrowed from Thunderbird. */ + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_1 ()); + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_2 ()); + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_3 ()); + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_4 ()); + + g_string_append_printf ( + stylesheet, + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "blockquote[type=cite]:not(.-x-evo-plaintext-quoted) " + "{\n" + " border-color: %s;\n" + "}\n", + citation_color_level_5 ()); + + gdk_color_free (link); + gdk_color_free (visited); + + base64 = g_base64_encode ((guchar *) stylesheet->str, stylesheet->len); + g_string_free (stylesheet, TRUE); + + stylesheet = g_string_new ("data:text/css;charset=utf-8;base64,"); + g_string_append (stylesheet, base64); + g_free (base64); + + settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (view)); + g_object_set ( + G_OBJECT (settings), + "default-font-size", pango_font_description_get_size (vw) / PANGO_SCALE, + "default-font-family", pango_font_description_get_family (vw), + "monospace-font-family", pango_font_description_get_family (ms), + "default-monospace-font-size", (pango_font_description_get_size (ms) / PANGO_SCALE), + "user-stylesheet-uri", stylesheet->str, + NULL); + + g_string_free (stylesheet, TRUE); + + pango_font_description_free (ms); + pango_font_description_free (vw); +} + +/** + * e_html_editor_view_get_element_under_mouse_click: + * @view: an #EHTMLEditorView + * + * Returns DOM element, that was clicked on. + * + * Returns: DOM element on that was clicked. + */ +WebKitDOMElement * +e_html_editor_view_get_element_under_mouse_click (EHTMLEditorView *view) +{ + g_return_val_if_fail (E_IS_HTML_EDITOR_VIEW (view), NULL); + + return view->priv->element_under_mouse; +} + +/** + * e_html_editor_view_check_magic_links + * @view: an #EHTMLEditorView + * @include_space: If TRUE the pattern for link expects space on end + * + * Check if actual selection in given editor is link. If so, it is surrounded + * with ANCHOR element. + */ +void +e_html_editor_view_check_magic_links (EHTMLEditorView *view, + gboolean include_space) +{ + WebKitDOMRange *range; + + g_return_if_fail (E_IS_HTML_EDITOR_VIEW (view)); + + range = html_editor_view_get_dom_range (view); + html_editor_view_check_magic_links (view, range, include_space, NULL); +} + +static CamelMimePart * +e_html_editor_view_add_inline_image_from_element (EHTMLEditorView *view, + WebKitDOMElement *element, + const gchar *attribute) +{ + CamelStream *stream; + CamelDataWrapper *wrapper; + CamelMimePart *part = NULL; + gsize decoded_size; + gssize size; + gchar *mime_type = NULL; + gchar *element_src, *cid, *name; + const gchar *base64_encoded_data; + guchar *base64_decoded_data; + + if (!WEBKIT_DOM_IS_ELEMENT (element)) { + return NULL; + } + + element_src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (element), attribute); + + base64_encoded_data = strstr (element_src, ";base64,"); + if (!base64_encoded_data) + goto out; + + mime_type = g_strndup ( + element_src + 5, + base64_encoded_data - (strstr (element_src, "data:") + 5)); + + /* Move to actual data */ + base64_encoded_data += 8; + + base64_decoded_data = g_base64_decode (base64_encoded_data, &decoded_size); + + stream = camel_stream_mem_new (); + size = camel_stream_write ( + stream, (gchar *) base64_decoded_data, decoded_size, NULL, NULL); + + if (size == -1) + goto out; + + wrapper = camel_data_wrapper_new (); + camel_data_wrapper_construct_from_stream_sync ( + wrapper, stream, NULL, NULL); + g_object_unref (stream); + + camel_data_wrapper_set_mime_type (wrapper, mime_type); + + part = camel_mime_part_new (); + camel_medium_set_content (CAMEL_MEDIUM (part), wrapper); + g_object_unref (wrapper); + + cid = camel_header_msgid_generate (); + camel_mime_part_set_content_id (part, cid); + name = webkit_dom_element_get_attribute (element, "data-name"); + camel_mime_part_set_filename (part, name); + g_free (name); + camel_mime_part_set_encoding (part, CAMEL_TRANSFER_ENCODING_BASE64); +out: + g_free (mime_type); + g_free (element_src); + g_free (base64_decoded_data); + + return part; +} + +GList * +e_html_editor_view_get_parts_for_inline_images (EHTMLEditorView *view) +{ + GHashTable *added; + GList *parts = NULL; + gint length, ii; + WebKitDOMDocument *document; + WebKitDOMNodeList *list; + + document = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); + list = webkit_dom_document_query_selector_all (document, "img[data-inline]", NULL); + + length = webkit_dom_node_list_get_length (list); + if (length == 0) + return parts; + + added = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); + for (ii = 0; ii < length; ii++) { + CamelMimePart *part; + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + gchar *src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "src"); + + if (!g_hash_table_lookup (added, src)) { + part = e_html_editor_view_add_inline_image_from_element ( + view, WEBKIT_DOM_ELEMENT (node), "src"); + parts = g_list_append (parts, part); + g_hash_table_insert ( + added, src, (gpointer) camel_mime_part_get_content_id (part)); + } + g_free (src); + } + + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + const gchar *id; + gchar *src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "src"); + + if ((id = g_hash_table_lookup (added, src)) != NULL) { + gchar *cid = g_strdup_printf ("cid:%s", id); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (node), "src", cid, NULL); + g_free (cid); + } + g_free (src); + } + + list = webkit_dom_document_query_selector_all ( + document, "[data-inline][background]", NULL); + length = webkit_dom_node_list_get_length (list); + for (ii = 0; ii < length; ii++) { + CamelMimePart *part; + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + gchar *src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "background"); + + if (!g_hash_table_lookup (added, src)) { + part = e_html_editor_view_add_inline_image_from_element ( + view, WEBKIT_DOM_ELEMENT (node), "background"); + if (part) { + parts = g_list_append (parts, part); + g_hash_table_insert ( + added, src, + (gpointer) camel_mime_part_get_content_id (part)); + } + } + g_free (src); + } + + for (ii = 0; ii < length; ii++) { + WebKitDOMNode *node = webkit_dom_node_list_item (list, ii); + gchar *src = webkit_dom_element_get_attribute ( + WEBKIT_DOM_ELEMENT (node), "background"); + const gchar *id; + + if ((id = g_hash_table_lookup (added, src)) != NULL) { + gchar *cid = g_strdup_printf ("cid:%s", id); + webkit_dom_element_set_attribute ( + WEBKIT_DOM_ELEMENT (node), "background", cid, NULL); + g_free (cid); + } + g_free (src); + } + + g_hash_table_destroy (added); + + return parts; +} + +/** + * e_html_editor_view_add_inline_image_from_mime_part: + * @composer: a composer object + * @part: a CamelMimePart containing image data + * + * This adds the mime part @part to @composer as an inline image. + **/ +void +e_html_editor_view_add_inline_image_from_mime_part (EHTMLEditorView *view, + CamelMimePart *part) +{ + CamelDataWrapper *dw; + CamelStream *stream; + GByteArray *byte_array; + gchar *src, *base64_encoded, *mime_type, *cid_src; + const gchar *cid, *name; + + stream = camel_stream_mem_new (); + dw = camel_medium_get_content (CAMEL_MEDIUM (part)); + g_return_if_fail (dw); + + mime_type = camel_data_wrapper_get_mime_type (dw); + camel_data_wrapper_decode_to_stream_sync (dw, stream, NULL, NULL); + camel_stream_close (stream, NULL, NULL); + + byte_array = camel_stream_mem_get_byte_array (CAMEL_STREAM_MEM (stream)); + + if (!byte_array->data) + return; + + base64_encoded = g_base64_encode ((const guchar *) byte_array->data, byte_array->len); + + name = camel_mime_part_get_filename (part); + /* Insert file name before new src */ + src = g_strconcat (name, ";data:", mime_type, ";base64,", base64_encoded, NULL); + + cid = camel_mime_part_get_content_id (part); + if (!cid) { + camel_mime_part_set_content_id (part, NULL); + cid = camel_mime_part_get_content_id (part); + } + cid_src = g_strdup_printf ("cid:%s", cid); + + g_hash_table_insert (view->priv->inline_images, cid_src, src); + + g_free (base64_encoded); + g_free (mime_type); + g_object_unref (stream); +} |