Files
evolution/e-util/e-html-editor-utils.c
Tomas Popela a60e6be8f1 EHTMLEditorView - Improve how the content is processed
Split the process_elements function to:
 * process_node_to_plain_text_for_exporting
 * process_node_to_plain_text_changing_composer_mode
 * process_node_to_html_changing_composer_mode
 * process_node_to_html_for_exporting

that process the given nodes (usually just BODY) for purposes described by the
function names - i.e. the first function will process the BODY and will transform
the lists to their plain text version, fill the indentation or alignment with
spaces and so on; the process_node_to_html_for_exporting will remove all the
classes, ids and other attributes from the DOM.

Also in the process_node_to_plain_text_for_exporting correctly process the block
elements to avoid situations when an extra new line could be added on the end of
the block or not added at all. Always look if the new line is indeed there, if
not add it there explicitly. This fixes the bug 767681. Also avoid working with
the innerHTML of the BODY (that was previously leaked), but instead copy the
nodes. Also remove images from the BODY if they are presented to avoid unneeded
line break.

After this change the indented elements will be preserved when switching between
composer modes.

When removing an image also remove its wrappers and the block where the image
was, if the block is empty after the image was removed, otherwise an extra line
will be added to the output.
2016-06-29 15:19:13 +02:00

654 lines
18 KiB
C

/*
* e-html-editor-utils.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-utils.h"
#include "e-misc-utils.h"
#include <string.h>
/**
* e_html_editor_dom_node_find_parent_element:
* @node: Start node
* @tagname: Tag name of element to search
*
* Recursively searches for first occurance of element with given @tagname
* that is parent of given @node.
*
* Returns: A #WebKitDOMElement with @tagname representing parent of @node or
* @NULL when @node has no parent with given @tagname. When @node matches @tagname,
* then the @node is returned.
*/
WebKitDOMElement *
e_html_editor_dom_node_find_parent_element (WebKitDOMNode *node,
const gchar *tagname)
{
gint taglen = strlen (tagname);
while (node) {
if (WEBKIT_DOM_IS_ELEMENT (node)) {
gchar *node_tagname;
node_tagname = webkit_dom_element_get_tag_name (
WEBKIT_DOM_ELEMENT (node));
if (node_tagname &&
(strlen (node_tagname) == taglen) &&
(g_ascii_strncasecmp (node_tagname, tagname, taglen) == 0)) {
g_free (node_tagname);
return WEBKIT_DOM_ELEMENT (node);
}
g_free (node_tagname);
}
node = WEBKIT_DOM_NODE (webkit_dom_node_get_parent_element (node));
}
return NULL;
}
/**
* e_html_editor_dom_node_find_child_element:
* @node: Start node
* @tagname: Tag name of element to search.
*
* Recursively searches for first occurrence of element with given @tagname that
* is a child of @node.
*
* Returns: A #WebKitDOMElement with @tagname representing a child of @node or
* @NULL when @node has no child with given @tagname. When @node matches @tagname,
* then the @node is returned.
*/
WebKitDOMElement *
e_html_editor_dom_node_find_child_element (WebKitDOMNode *node,
const gchar *tagname)
{
WebKitDOMNode *start_node = node;
gint taglen = strlen (tagname);
do {
if (WEBKIT_DOM_IS_ELEMENT (node)) {
gchar *node_tagname;
node_tagname = webkit_dom_element_get_tag_name (
WEBKIT_DOM_ELEMENT (node));
if (node_tagname &&
(strlen (node_tagname) == taglen) &&
(g_ascii_strncasecmp (node_tagname, tagname, taglen) == 0)) {
g_free (node_tagname);
return WEBKIT_DOM_ELEMENT (node);
}
g_free (node_tagname);
}
if (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 {
node = webkit_dom_node_get_parent_node (node);
}
} while (!webkit_dom_node_is_same_node (node, start_node));
return NULL;
}
gboolean
e_html_editor_node_is_selection_position_node (WebKitDOMNode *node)
{
WebKitDOMElement *element;
if (!node || !WEBKIT_DOM_IS_ELEMENT (node))
return FALSE;
element = WEBKIT_DOM_ELEMENT (node);
return element_has_id (element, "-x-evo-selection-start-marker") ||
element_has_id (element, "-x-evo-selection-end-marker");
}
WebKitDOMNode *
e_html_editor_get_parent_block_node_from_child (WebKitDOMNode *node)
{
WebKitDOMNode *parent = node;
if (!WEBKIT_DOM_IS_ELEMENT (parent) ||
e_html_editor_node_is_selection_position_node (parent))
parent = webkit_dom_node_get_parent_node (parent);
if (element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-temp-text-wrapper") ||
element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-quoted") ||
element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-quote-character") ||
element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-signature") ||
element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-resizable-wrapper") ||
WEBKIT_DOM_IS_HTML_ANCHOR_ELEMENT (parent) ||
element_has_tag (WEBKIT_DOM_ELEMENT (parent), "b") ||
element_has_tag (WEBKIT_DOM_ELEMENT (parent), "i") ||
element_has_tag (WEBKIT_DOM_ELEMENT (parent), "u"))
parent = webkit_dom_node_get_parent_node (parent);
if (element_has_class (WEBKIT_DOM_ELEMENT (parent), "-x-evo-quoted") ||
element_has_class (WEBKIT_DOM_ELEMENT (parent), "Apple-tab-span"))
parent = webkit_dom_node_get_parent_node (parent);
return parent;
}
gboolean
element_has_id (WebKitDOMElement *element,
const gchar* id)
{
gchar *element_id;
if (!element)
return FALSE;
if (!WEBKIT_DOM_IS_ELEMENT (element))
return FALSE;
element_id = webkit_dom_element_get_id (element);
if (g_ascii_strcasecmp (element_id, id) != 0) {
g_free (element_id);
return FALSE;
}
g_free (element_id);
return TRUE;
}
gboolean
element_has_tag (WebKitDOMElement *element,
const gchar* tag)
{
gchar *element_tag;
if (!WEBKIT_DOM_IS_ELEMENT (element))
return FALSE;
element_tag = webkit_dom_node_get_local_name (WEBKIT_DOM_NODE (element));
if (g_ascii_strcasecmp (element_tag, tag) != 0) {
g_free (element_tag);
return FALSE;
}
g_free (element_tag);
return TRUE;
}
gboolean
element_has_class (WebKitDOMElement *element,
const gchar* class)
{
gchar *element_class;
if (!element)
return FALSE;
if (!WEBKIT_DOM_IS_ELEMENT (element))
return FALSE;
element_class = webkit_dom_element_get_class_name (element);
if (g_strstr_len (element_class, -1, class)) {
g_free (element_class);
return TRUE;
}
g_free (element_class);
return FALSE;
}
void
element_add_class (WebKitDOMElement *element,
const gchar* class)
{
gchar *element_class;
gchar *new_class;
if (!WEBKIT_DOM_IS_ELEMENT (element))
return;
if (element_has_class (element, class))
return;
element_class = webkit_dom_element_get_class_name (element);
if (g_strcmp0 (element_class, "") == 0)
new_class = g_strdup (class);
else
new_class = g_strconcat (element_class, " ", class, NULL);
webkit_dom_element_set_class_name (element, new_class);
g_free (element_class);
g_free (new_class);
}
void
element_remove_class (WebKitDOMElement *element,
const gchar* class)
{
gchar *element_class, *final_class;
GRegex *regex;
gchar *pattern = NULL;
if (!WEBKIT_DOM_IS_ELEMENT (element))
return;
if (!element_has_class (element, class))
return;
element_class = webkit_dom_element_get_class_name (element);
pattern = g_strconcat ("[\\s]*", class, "[\\s]*", NULL);
regex = g_regex_new (pattern, 0, 0, NULL);
final_class = g_regex_replace (regex, element_class, -1, 0, " ", 0, NULL);
if (g_strcmp0 (final_class, " ") != 0)
webkit_dom_element_set_class_name (element, final_class);
else
webkit_dom_element_remove_attribute (element, "class");
g_free (element_class);
g_free (final_class);
g_free (pattern);
g_regex_unref (regex);
}
void
remove_node (WebKitDOMNode *node)
{
WebKitDOMNode *parent = webkit_dom_node_get_parent_node (node);
/* Check if the parent exists, if so it means that the node is still
* in the DOM or at least the parent is. If it doesn't exists it is not
* in the DOM and we can free it. */
if (parent)
webkit_dom_node_remove_child (parent, node, NULL);
else
g_object_unref (node);
}
void
remove_node_if_empty (WebKitDOMNode *node)
{
WebKitDOMNode *child;
if (!WEBKIT_DOM_IS_NODE (node))
return;
if ((child = webkit_dom_node_get_first_child (node))) {
WebKitDOMNode *prev_sibling, *next_sibling;
prev_sibling = webkit_dom_node_get_previous_sibling (child);
next_sibling = webkit_dom_node_get_next_sibling (child);
/* Empty or BR as sibling, but no sibling after it. */
if (!webkit_dom_node_get_first_child (child) &&
!WEBKIT_DOM_IS_TEXT (child) &&
(!prev_sibling ||
(WEBKIT_DOM_IS_HTMLBR_ELEMENT (prev_sibling) &&
!webkit_dom_node_get_previous_sibling (prev_sibling))) &&
(!next_sibling ||
(WEBKIT_DOM_IS_HTMLBR_ELEMENT (next_sibling) &&
!webkit_dom_node_get_next_sibling (next_sibling)))) {
remove_node (node);
} else {
gchar *text_content;
text_content = webkit_dom_node_get_text_content (node);
if (!text_content)
remove_node (node);
if (text_content && !*text_content)
remove_node (node);
g_free (text_content);
}
} else
remove_node (node);
}
WebKitDOMNode *
split_node_into_two (WebKitDOMNode *item,
gint level)
{
gint current_level = 1;
WebKitDOMDocument *document;
WebKitDOMDocumentFragment *fragment;
WebKitDOMNode *parent, *prev_parent = NULL, *tmp = NULL;
document = webkit_dom_node_get_owner_document (item);
fragment = webkit_dom_document_create_document_fragment (document);
tmp = item;
parent = webkit_dom_node_get_parent_node (item);
while (!WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) {
WebKitDOMNode *clone, *first_child, *insert_before = NULL, *sibling;
first_child = webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (fragment));
clone = webkit_dom_node_clone_node (parent, FALSE);
webkit_dom_node_insert_before (
WEBKIT_DOM_NODE (fragment), clone, first_child, NULL);
if (first_child)
insert_before = webkit_dom_node_get_first_child (first_child);
while (first_child && (sibling = webkit_dom_node_get_next_sibling (first_child)))
webkit_dom_node_insert_before (first_child, sibling, insert_before, NULL);
while (tmp && (sibling = webkit_dom_node_get_next_sibling (tmp)))
webkit_dom_node_append_child (clone, sibling, NULL);
if (tmp)
webkit_dom_node_insert_before (
clone, tmp, webkit_dom_node_get_first_child (clone), NULL);
prev_parent = parent;
tmp = webkit_dom_node_get_next_sibling (parent);
parent = webkit_dom_node_get_parent_node (parent);
if (WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent)) {
first_child = webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (fragment));
insert_before = webkit_dom_node_get_first_child (first_child);
while (first_child && (sibling = webkit_dom_node_get_next_sibling (first_child))) {
webkit_dom_node_insert_before (
first_child, sibling, insert_before, NULL);
}
}
if (current_level >= level && level >= 0)
break;
current_level++;
}
if (prev_parent) {
tmp = webkit_dom_node_insert_before (
parent,
webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (fragment)),
webkit_dom_node_get_next_sibling (prev_parent),
NULL);
remove_node_if_empty (prev_parent);
}
return tmp;
}
WebKitDOMElement *
create_selection_marker (WebKitDOMDocument *document,
gboolean start)
{
WebKitDOMElement *element;
element = webkit_dom_document_create_element (
document, "SPAN", NULL);
webkit_dom_element_set_id (
element,
start ? "-x-evo-selection-start-marker" :
"-x-evo-selection-end-marker");
return element;
}
void
remove_selection_markers (WebKitDOMDocument *document)
{
WebKitDOMElement *marker;
marker = webkit_dom_document_get_element_by_id (
document, "-x-evo-selection-start-marker");
if (marker)
remove_node (WEBKIT_DOM_NODE (marker));
marker = webkit_dom_document_get_element_by_id (
document, "-x-evo-selection-end-marker");
if (marker)
remove_node (WEBKIT_DOM_NODE (marker));
}
void
add_selection_markers_into_element_start (WebKitDOMDocument *document,
WebKitDOMElement *element,
WebKitDOMElement **selection_start_marker,
WebKitDOMElement **selection_end_marker)
{
WebKitDOMElement *marker;
remove_selection_markers (document);
marker = create_selection_marker (document, FALSE);
webkit_dom_node_insert_before (
WEBKIT_DOM_NODE (element),
WEBKIT_DOM_NODE (marker),
webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (element)),
NULL);
if (selection_end_marker)
*selection_end_marker = marker;
marker = create_selection_marker (document, TRUE);
webkit_dom_node_insert_before (
WEBKIT_DOM_NODE (element),
WEBKIT_DOM_NODE (marker),
webkit_dom_node_get_first_child (WEBKIT_DOM_NODE (element)),
NULL);
if (selection_start_marker)
*selection_start_marker = marker;
}
void
add_selection_markers_into_element_end (WebKitDOMDocument *document,
WebKitDOMElement *element,
WebKitDOMElement **selection_start_marker,
WebKitDOMElement **selection_end_marker)
{
WebKitDOMElement *marker;
remove_selection_markers (document);
marker = create_selection_marker (document, TRUE);
webkit_dom_node_append_child (
WEBKIT_DOM_NODE (element), WEBKIT_DOM_NODE (marker), NULL);
if (selection_start_marker)
*selection_start_marker = marker;
marker = create_selection_marker (document, FALSE);
webkit_dom_node_append_child (
WEBKIT_DOM_NODE (element), WEBKIT_DOM_NODE (marker), NULL);
if (selection_end_marker)
*selection_end_marker = marker;
}
gboolean
node_is_list (WebKitDOMNode *node)
{
return node && (
WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (node) ||
WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (node));
}
gboolean
node_is_list_or_item (WebKitDOMNode *node)
{
return node && (node_is_list (node) || WEBKIT_DOM_IS_HTMLLI_ELEMENT (node));
}
/**
* get_list_format_from_node:
* @node: an #WebKitDOMNode
*
* Returns block format of given list.
*
* Returns: #EHTMLEditorSelectionBlockFormat
*/
EHTMLEditorSelectionBlockFormat
get_list_format_from_node (WebKitDOMNode *node)
{
EHTMLEditorSelectionBlockFormat format =
E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_UNORDERED_LIST;
if (WEBKIT_DOM_IS_HTMLLI_ELEMENT (node))
return E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_NONE;
if (WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (node))
return format;
if (WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (node)) {
gchar *type_value = webkit_dom_element_get_attribute (
WEBKIT_DOM_ELEMENT (node), "type");
if (!type_value)
return E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST;
if (!*type_value)
format = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST;
else if (g_ascii_strcasecmp (type_value, "A") == 0)
format = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ALPHA;
else if (g_ascii_strcasecmp (type_value, "I") == 0)
format = E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_ORDERED_LIST_ROMAN;
g_free (type_value);
return format;
}
return E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_NONE;
}
void
merge_list_into_list (WebKitDOMNode *from,
WebKitDOMNode *to,
gboolean insert_before)
{
WebKitDOMNode *item, *insert_before_node;
if (!(to && from))
return;
insert_before_node = webkit_dom_node_get_first_child (to);
while ((item = webkit_dom_node_get_first_child (from)) != NULL) {
if (insert_before)
webkit_dom_node_insert_before (
to, item, insert_before_node, NULL);
else
webkit_dom_node_append_child (to, item, NULL);
}
if (!webkit_dom_node_get_first_child (from))
remove_node (from);
}
void
merge_lists_if_possible (WebKitDOMNode *list)
{
EHTMLEditorSelectionBlockFormat format, prev, next;
gint ii, length;
WebKitDOMNode *prev_sibling, *next_sibling;
WebKitDOMNodeList *lists;
prev_sibling = webkit_dom_node_get_previous_sibling (WEBKIT_DOM_NODE (list));
next_sibling = webkit_dom_node_get_next_sibling (WEBKIT_DOM_NODE (list));
format = get_list_format_from_node (list),
prev = get_list_format_from_node (prev_sibling);
next = get_list_format_from_node (next_sibling);
if (format != E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_NONE) {
if (format == prev && prev != E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_NONE)
merge_list_into_list (prev_sibling, list, TRUE);
if (format == next && next != E_HTML_EDITOR_SELECTION_BLOCK_FORMAT_NONE)
merge_list_into_list (next_sibling, list, FALSE);
}
lists = webkit_dom_element_query_selector_all (
WEBKIT_DOM_ELEMENT (list), "ol + ol, ul + ul", NULL);
length = webkit_dom_node_list_get_length (lists);
for (ii = 0; ii < length; ii++) {
WebKitDOMNode *node;
node = webkit_dom_node_list_item (lists, ii);
merge_lists_if_possible (node);
g_object_unref (node);
}
g_object_unref (lists);
}
WebKitDOMElement *
get_parent_block_element (WebKitDOMNode *node)
{
WebKitDOMElement *parent = webkit_dom_node_get_parent_element (node);
if (WEBKIT_DOM_IS_HTML_BODY_ELEMENT (parent))
return WEBKIT_DOM_ELEMENT (node);
while (parent &&
!WEBKIT_DOM_IS_HTML_DIV_ELEMENT (parent) &&
!WEBKIT_DOM_IS_HTML_QUOTE_ELEMENT (parent) &&
!WEBKIT_DOM_IS_HTMLU_LIST_ELEMENT (parent) &&
!WEBKIT_DOM_IS_HTMLO_LIST_ELEMENT (parent) &&
!WEBKIT_DOM_IS_HTML_PRE_ELEMENT (parent) &&
!WEBKIT_DOM_IS_HTML_HEADING_ELEMENT (parent) &&
!WEBKIT_DOM_IS_HTML_TABLE_CELL_ELEMENT (parent) &&
!element_has_tag (parent, "address")) {
parent = webkit_dom_node_get_parent_element (
WEBKIT_DOM_NODE (parent));
}
return parent;
}
void
dom_element_rename_attribute (WebKitDOMElement *element,
const gchar *from,
const gchar *to)
{
gchar *value;
if (!webkit_dom_element_has_attribute (element, from))
return;
value = webkit_dom_element_get_attribute (element, from);
webkit_dom_element_set_attribute (element, to, (value && *value) ? value : "", NULL);
webkit_dom_element_remove_attribute (element, from);
g_free (value);
}
void
dom_element_swap_attributes (WebKitDOMElement *element,
const gchar *from,
const gchar *to)
{
gchar *value_from, *value_to;
if (!webkit_dom_element_has_attribute (element, from) ||
!webkit_dom_element_has_attribute (element, to))
return;
value_from = webkit_dom_element_get_attribute (element, from);
value_to = webkit_dom_element_get_attribute (element, to);
webkit_dom_element_set_attribute (element, to, (value_from && *value_from) ? value_from : "", NULL);
webkit_dom_element_set_attribute (element, from, (value_to && *value_to) ? value_to : "", NULL);
g_free (value_from);
g_free (value_to);
}