/*
* 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 UNICODE_NBSP "\xc2\xa0"
#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;
GHashTable *old_settings;
};
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 */
remove_node (WEBKIT_DOM_NODE (text));
/* 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 */
remove_node (WEBKIT_DOM_NODE (text));
/* 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)
remove_node (prev_sibling);
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;
remove_node (WEBKIT_DOM_NODE (cite_body));
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);
}
remove_node (node);
}
}
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 gboolean
is_return_key (GdkEventKey *event)
{
return (
(event->keyval == GDK_KEY_Return) ||
(event->keyval == GDK_KEY_Linefeed) ||
(event->keyval == GDK_KEY_KP_Enter));
}
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) {
return_pressed = is_return_key (event);
include_space = (event->keyval == GDK_KEY_space);
} 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);
remove_node (node);
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);
remove_node (node);
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)
remove_node (WEBKIT_DOM_NODE (style));
} 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);
if (priv->old_settings) {
g_hash_table_destroy (priv->old_settings);
priv->old_settings = NULL;
}
/* 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 (is_return_key (event)) {
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))
remove_node (next_sibling);
/* 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 (is_return_key (event) || (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);
}
/* 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, 0);
}
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){
gchar *rest_to_insert = g_strdup (rest);
GString *space_to_nbsp;
g_strchomp (rest_to_insert);
space_to_nbsp = e_str_replace_string (
rest_to_insert, " ", " ");
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);
g_free (rest_to_insert);
}
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 && (g_strcmp0 (prev_br, "<br>") != 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, 0);
}
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);
remove_node (webkit_dom_node_get_first_child (node));
}
remove_node (node);
}
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_settings_changed_cb (GSettings *settings,
const gchar *key,
EHTMLEditorView *view)
{
GVariant *new_value, *old_value;
new_value = g_settings_get_value (settings, key);
old_value = g_hash_table_lookup (view->priv->old_settings, key);
if (!new_value || !old_value || !g_variant_equal (new_value, old_value)) {
if (new_value)
g_hash_table_insert (view->priv->old_settings, g_strdup (key), new_value);
else
g_hash_table_remove (view->priv->old_settings, key);
e_html_editor_view_update_fonts (view);
} else if (new_value) {
g_variant_unref (new_value);
}
}
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);
view->priv->old_settings = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
/* 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 (
g_settings, "changed::font-name",
G_CALLBACK (e_html_editor_settings_changed_cb), view);
g_signal_connect (
g_settings, "changed::monospace-font-name",
G_CALLBACK (e_html_editor_settings_changed_cb), 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 (
settings, "changed::antialiasing",
G_CALLBACK (e_html_editor_settings_changed_cb), 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=\"-x-evo-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_TEXT (prev_sibling) &&
!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)
remove_node (node);
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);
remove_node (node);
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++)
remove_node (webkit_dom_node_list_item (list, ii));
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))
remove_node (prev_sibling);
if (next_sibling && WEBKIT_DOM_IS_HTMLBR_ELEMENT (next_sibling))
remove_node (next_sibling);
if (webkit_dom_node_has_child_nodes (blockquote)) {
WebKitDOMNode *child = webkit_dom_node_get_first_child (blockquote);
if (WEBKIT_DOM_IS_HTMLBR_ELEMENT (child))
remove_node (child);
}
}
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 8 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;
if (strstr (content, UNICODE_NBSP)) {
GString *nbsp_free;
nbsp_free = e_str_replace_string (
content, UNICODE_NBSP, " ");
g_free (content);
content = g_string_free (nbsp_free, FALSE);
}
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)
remove_node (child);
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);
remove_node (child);
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;
else 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++)
remove_node (webkit_dom_node_list_item (list, ii));
}
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++)
remove_node (webkit_dom_node_list_item (images, ii));
}
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;
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")) {
const gchar *style_to_add = "";
style = webkit_dom_element_get_attribute (
WEBKIT_DOM_ELEMENT (node), "style");
if ((css_align = strstr (style, "text-align: "))) {
style_to_add = g_str_has_prefix (
css_align + 12, "center") ?
"text-align: center;" :
"text-align: right;";
}
/* In plain text 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) {
WebKitDOMNode *parent = webkit_dom_node_get_parent_node (
WEBKIT_DOM_NODE (signature));
signature_clone = webkit_dom_node_clone_node (parent, TRUE);
remove_node (parent);
}
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);
remove_node (node);
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)
remove_node (source);
/* 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)))
remove_node (WEBKIT_DOM_NODE (head));
/* 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);
}
toggle_paragraphs_style (view);
toggle_smileys (view);
remove_images (view);
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");
remove_node (WEBKIT_DOM_NODE (sheet));
}
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,
".-x-evo-quote-character "
"{\n"
" color: %s;\n"
"}\n",
citation_color_level_1 ());
g_string_append_printf (
stylesheet,
".-x-evo-quote-character+"
".-x-evo-quote-character"
"{\n"
" color: %s;\n"
"}\n",
citation_color_level_2 ());
g_string_append_printf (
stylesheet,
".-x-evo-quote-character+"
".-x-evo-quote-character+"
".-x-evo-quote-character"
"{\n"
" color: %s;\n"
"}\n",
citation_color_level_3 ());
g_string_append_printf (
stylesheet,
".-x-evo-quote-character+"
".-x-evo-quote-character+"
".-x-evo-quote-character+"
".-x-evo-quote-character"
"{\n"
" color: %s;\n"
"}\n",
citation_color_level_4 ());
g_string_append_printf (
stylesheet,
".-x-evo-quote-character+"
".-x-evo-quote-character+"
".-x-evo-quote-character+"
".-x-evo-quote-character+"
".-x-evo-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,
const gchar *uid_domain)
{
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 (uid_domain);
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,
const gchar *uid_domain)
{
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++) {
const gchar *id;
gchar *cid;
WebKitDOMNode *node = webkit_dom_node_list_item (list, ii);
gchar *src = webkit_dom_element_get_attribute (
WEBKIT_DOM_ELEMENT (node), "src");
if ((id = g_hash_table_lookup (added, src)) != NULL) {
cid = g_strdup_printf ("cid:%s", id);
} else {
CamelMimePart *part;
part = e_html_editor_view_add_inline_image_from_element (
view, WEBKIT_DOM_ELEMENT (node), "src", uid_domain);
parts = g_list_append (parts, part);
id = camel_mime_part_get_content_id (part);
cid = g_strdup_printf ("cid:%s", id);
g_hash_table_insert (added, src, (gpointer) id);
}
webkit_dom_element_set_attribute (
WEBKIT_DOM_ELEMENT (node), "src", cid, NULL);
g_free (src);
g_free (cid);
}
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;
const gchar *id;
gchar *cid = NULL;
WebKitDOMNode *node = webkit_dom_node_list_item (list, ii);
gchar *src = webkit_dom_element_get_attribute (
WEBKIT_DOM_ELEMENT (node), "background");
if ((id = g_hash_table_lookup (added, src)) != NULL) {
cid = g_strdup_printf ("cid:%s", id);
webkit_dom_element_set_attribute (
WEBKIT_DOM_ELEMENT (node), "background", cid, NULL);
} else {
part = e_html_editor_view_add_inline_image_from_element (
view, WEBKIT_DOM_ELEMENT (node), "background", uid_domain);
if (part) {
parts = g_list_append (parts, part);
id = camel_mime_part_get_content_id (part);
g_hash_table_insert (added, src, (gpointer) id);
cid = g_strdup_printf ("cid:%s", id);
webkit_dom_element_set_attribute (
WEBKIT_DOM_ELEMENT (node), "background", cid, NULL);
}
}
g_free (src);
g_free (cid);
}
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);
}