/* * e-html-editor-utils.c * * Copyright (C) 2012 Dan Vrátil * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with the program; if not, see * */ #ifdef HAVE_CONFIG_H #include #endif #include "e-html-editor-utils.h" #include "e-misc-utils.h" #include /** * 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") || 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; GString *result; 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, class) == 0) { webkit_dom_element_remove_attribute (element, "class"); g_free (element_class); return; } result = e_str_replace_string (element_class, class, ""); if (result) { webkit_dom_element_set_class_name (element, result->str); g_string_free (result, TRUE); } g_free (element_class); } 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) { if (!WEBKIT_DOM_IS_NODE (node)) return; if (!webkit_dom_node_get_first_child (node)) { 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); } } 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 ((sibling = webkit_dom_node_get_next_sibling (tmp))) webkit_dom_node_append_child (clone, sibling, NULL); 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 -1; 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 -1; } 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 == prev && format != -1 && prev != -1) merge_list_into_list (prev_sibling, list, TRUE); if (format == next && format != -1 && next != -1) 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); }