summaryrefslogtreecommitdiffstats
path: root/src/text-editing.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/text-editing.cpp')
-rw-r--r--src/text-editing.cpp2186
1 files changed, 2186 insertions, 0 deletions
diff --git a/src/text-editing.cpp b/src/text-editing.cpp
new file mode 100644
index 0000000..2050584
--- /dev/null
+++ b/src/text-editing.cpp
@@ -0,0 +1,2186 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Parent class for text and flowtext
+ *
+ * Authors:
+ * bulia byak
+ * Richard Hughes
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-5 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+#endif
+
+#include <cstring>
+#include <string>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "text-editing.h"
+
+#include "object/sp-textpath.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-flowdiv.h"
+#include "object/sp-flowregion.h"
+#include "object/sp-item-group.h"
+#include "object/sp-tref.h"
+#include "object/sp-tspan.h"
+#include "style.h"
+
+#include "util/units.h"
+
+#include "xml/attribute-record.h"
+#include "xml/sp-css-attr.h"
+
+static const gchar *tref_edit_message = _("You cannot edit <b>cloned character data</b>.");
+static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend = false);
+
+static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration);
+
+Inkscape::Text::Layout const * te_get_layout (SPItem const *item)
+{
+ if (SP_IS_TEXT(item)) {
+ return &(SP_TEXT(item)->layout);
+ } else if (SP_IS_FLOWTEXT (item)) {
+ return &(SP_FLOWTEXT(item)->layout);
+ }
+ return nullptr;
+}
+
+static void te_update_layout_now (SPItem *item)
+{
+ if (SP_IS_TEXT(item))
+ SP_TEXT(item)->rebuildLayout();
+ else if (SP_IS_FLOWTEXT (item))
+ SP_FLOWTEXT(item)->rebuildLayout();
+ item->updateRepr();
+}
+
+void te_update_layout_now_recursive(SPItem *item)
+{
+ if (SP_IS_GROUP(item)) {
+ std::vector<SPItem*> item_list = sp_item_group_item_list(SP_GROUP(item));
+ for(auto list_item : item_list){
+ te_update_layout_now_recursive(list_item);
+ }
+ } else if (SP_IS_TEXT(item))
+ SP_TEXT(item)->rebuildLayout();
+ else if (SP_IS_FLOWTEXT (item))
+ SP_FLOWTEXT(item)->rebuildLayout();
+ item->updateRepr();
+}
+
+bool sp_te_output_is_empty(SPItem const *item)
+{
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+ return layout->begin() == layout->end();
+}
+
+bool sp_te_input_is_empty(SPObject const *item)
+{
+ bool empty = true;
+ if (SP_IS_STRING(item)) {
+ empty = SP_STRING(item)->string.empty();
+ } else {
+ for (auto& child: item->children) {
+ if (!sp_te_input_is_empty(&child)) {
+ empty = false;
+ break;
+ }
+ }
+ }
+ return empty;
+}
+
+Inkscape::Text::Layout::iterator
+sp_te_get_position_by_coords (SPItem const *item, Geom::Point const &i_p)
+{
+ Geom::Affine im (item->i2dt_affine ());
+ im = im.inverse();
+
+ Geom::Point p = i_p * im;
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+ return layout->getNearestCursorPositionTo(p);
+}
+
+std::vector<Geom::Point> sp_te_create_selection_quads(SPItem const *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, Geom::Affine const &transform)
+{
+ if (start == end)
+ return std::vector<Geom::Point>();
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+ if (layout == nullptr)
+ return std::vector<Geom::Point>();
+
+ return layout->createSelectionShape(start, end, transform);
+}
+
+void
+sp_te_get_cursor_coords (SPItem const *item, Inkscape::Text::Layout::iterator const &position, Geom::Point &p0, Geom::Point &p1)
+{
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+ double height, rotation;
+ layout->queryCursorShape(position, p0, height, rotation);
+ p1 = Geom::Point(p0[Geom::X] + height * sin(rotation), p0[Geom::Y] - height * cos(rotation)); // valgrind warns that rotation is not initialized here. Why is to be seen in queryCursorShape
+}
+
+SPStyle const * sp_te_style_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position)
+{
+ SPObject const *pos_obj = sp_te_object_at_position(text, position);
+ SPStyle *result = (pos_obj) ? pos_obj->style : nullptr;
+ return result;
+}
+
+SPObject const * sp_te_object_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position)
+{
+ Inkscape::Text::Layout const *layout = te_get_layout(text);
+ if (layout == nullptr) {
+ return nullptr;
+ }
+ SPObject *rawptr = nullptr;
+ layout->getSourceOfCharacter(position, &rawptr);
+ SPObject const *pos_obj = rawptr;
+ if (pos_obj == nullptr) {
+ pos_obj = text;
+ }
+ while (pos_obj->style == nullptr) {
+ pos_obj = pos_obj->parent; // not interested in SPStrings
+ }
+ return pos_obj;
+}
+
+/*
+ * for debugging input
+ *
+char * dump_hexy(const gchar * utf8)
+{
+ static char buffer[1024];
+
+ buffer[0]='\0';
+ for (const char *ptr=utf8; *ptr; ptr++) {
+ sprintf(buffer+strlen(buffer),"x%02X",(unsigned char)*ptr);
+ }
+ return buffer;
+}
+*/
+
+Inkscape::Text::Layout::iterator sp_te_replace(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, gchar const *utf8)
+{
+ iterator_pair pair;
+ sp_te_delete(item, start, end, pair);
+ return sp_te_insert(item, pair.first, utf8);
+}
+
+
+/* ***************************************************************************************************/
+// I N S E R T I N G T E X T
+
+static bool is_line_break_object(SPObject const *object)
+{
+ bool is_line_break = false;
+
+ if (object) {
+ if (SP_IS_TEXT(object)
+ || (SP_IS_TSPAN(object) && SP_TSPAN(object)->role != SP_TSPAN_ROLE_UNSPECIFIED)
+ || SP_IS_TEXTPATH(object)
+ || SP_IS_FLOWDIV(object)
+ || SP_IS_FLOWPARA(object)
+ || SP_IS_FLOWLINE(object)
+ || SP_IS_FLOWREGIONBREAK(object)) {
+
+ is_line_break = true;
+ }
+ }
+
+ return is_line_break;
+}
+
+/** returns the attributes for an object, or NULL if it isn't a text,
+tspan, tref, or textpath. */
+static TextTagAttributes* attributes_for_object(SPObject *object)
+{
+ if (SP_IS_TSPAN(object))
+ return &SP_TSPAN(object)->attributes;
+ if (SP_IS_TEXT(object))
+ return &SP_TEXT(object)->attributes;
+ if (SP_IS_TREF(object))
+ return &SP_TREF(object)->attributes;
+ if (SP_IS_TEXTPATH(object))
+ return &SP_TEXTPATH(object)->attributes;
+ return nullptr;
+}
+
+static const char * span_name_for_text_object(SPObject const *object)
+{
+ if (SP_IS_TEXT(object)) return "svg:tspan";
+ else if (SP_IS_FLOWTEXT(object)) return "svg:flowSpan";
+ return nullptr;
+}
+
+unsigned sp_text_get_length(SPObject const *item)
+{
+ unsigned length = 0;
+
+ if (SP_IS_STRING(item)) {
+ length = SP_STRING(item)->string.length();
+ } else {
+ if (is_line_break_object(item)) {
+ length++;
+ }
+
+ for (auto& child: item->children) {
+ if (SP_IS_STRING(&child)) {
+ length += SP_STRING(&child)->string.length();
+ } else {
+ length += sp_text_get_length(&child);
+ }
+ }
+ }
+
+ return length;
+}
+
+unsigned sp_text_get_length_upto(SPObject const *item, SPObject const *upto)
+{
+ unsigned length = 0;
+
+ // The string is the lowest level and the length can be counted directly.
+ if (SP_IS_STRING(item)) {
+ return SP_STRING(item)->string.length();
+ }
+
+ // Take care of new lines...
+ if (is_line_break_object(item) && !SP_IS_TEXT(item)) {
+ if (item != item->parent->firstChild()) {
+ // add 1 for each newline
+ length++;
+ }
+ }
+
+ // Count the length of the children
+ for (auto& child: item->children) {
+ if (upto && &child == upto) {
+ // hit upto, return immediately
+ return length;
+ }
+ if (SP_IS_STRING(&child)) {
+ length += SP_STRING(&child)->string.length();
+ }
+ else {
+ if (upto && child.isAncestorOf(upto)) {
+ // upto is below us, recurse and break loop
+ length += sp_text_get_length_upto(&child, upto);
+ return length;
+ } else {
+ // recurse and go to the next sibling
+ length += sp_text_get_length_upto(&child, upto);
+ }
+ }
+ }
+ return length;
+}
+
+static Inkscape::XML::Node* duplicate_node_without_children(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node const *old_node)
+{
+ switch (old_node->type()) {
+ case Inkscape::XML::NodeType::ELEMENT_NODE: {
+ Inkscape::XML::Node *new_node = xml_doc->createElement(old_node->name());
+ GQuark const id_key = g_quark_from_string("id");
+ for ( const auto & attr: old_node->attributeList() ) {
+ if (attr.key == id_key) continue;
+ new_node->setAttribute(g_quark_to_string(attr.key), attr.value);
+ }
+ return new_node;
+ }
+
+ case Inkscape::XML::NodeType::TEXT_NODE:
+ return xml_doc->createTextNode(old_node->content());
+
+ case Inkscape::XML::NodeType::COMMENT_NODE:
+ return xml_doc->createComment(old_node->content());
+
+ case Inkscape::XML::NodeType::PI_NODE:
+ return xml_doc->createPI(old_node->name(), old_node->content());
+
+ case Inkscape::XML::NodeType::DOCUMENT_NODE:
+ return nullptr; // this had better never happen
+ }
+ return nullptr;
+}
+
+/** returns the sum of the (recursive) lengths of all the SPStrings prior
+to \a item at the same level. */
+static unsigned sum_sibling_text_lengths_before(SPObject const *item)
+{
+ unsigned char_index = 0;
+ for (auto& sibling: item->parent->children) {
+ if (&sibling == item) {
+ break;
+ }
+ char_index += sp_text_get_length(&sibling);
+ }
+ return char_index;
+}
+
+/** splits the attributes for the first object at the given \a char_index
+and moves the ones after that point into \a second_item. */
+static void split_attributes(SPObject *first_item, SPObject *second_item, unsigned char_index)
+{
+ TextTagAttributes *first_attrs = attributes_for_object(first_item);
+ TextTagAttributes *second_attrs = attributes_for_object(second_item);
+ if (first_attrs && second_attrs)
+ first_attrs->split(char_index, second_attrs);
+}
+
+/** recursively divides the XML node tree into two objects: the original will
+contain all objects up to and including \a split_obj and the returned value
+will be the new leaf which represents the copy of \a split_obj and extends
+down the tree with new elements all the way to the common root which is the
+parent of the first line break node encountered.
+*/
+static SPObject* split_text_object_tree_at(SPObject *split_obj, unsigned char_index)
+{
+ Inkscape::XML::Document *xml_doc = split_obj->document->getReprDoc();
+ if (is_line_break_object(split_obj)) {
+ Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
+ split_obj->parent->getRepr()->addChild(new_node, split_obj->getRepr());
+ Inkscape::GC::release(new_node);
+ split_attributes(split_obj, split_obj->getNext(), char_index);
+ return split_obj->getNext();
+ } else if (!SP_IS_TSPAN(split_obj) &&
+ !SP_IS_FLOWTSPAN(split_obj) &&
+ !SP_IS_STRING(split_obj)) {
+ std::cerr << "split_text_object_tree_at: Illegal split object type! (Illegal document structure.)" << std::endl;
+ return nullptr;
+ }
+
+ unsigned char_count_before = sum_sibling_text_lengths_before(split_obj);
+ SPObject *duplicate_obj = split_text_object_tree_at(split_obj->parent, char_index + char_count_before);
+
+ if (duplicate_obj == nullptr) {
+ // Illegal document structure (no line break object).
+ return nullptr;
+ }
+
+ // copy the split node
+ Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
+ duplicate_obj->getRepr()->appendChild(new_node);
+ Inkscape::GC::release(new_node);
+
+ // sort out the copied attributes (x/y/dx/dy/rotate)
+ split_attributes(split_obj, duplicate_obj->firstChild(), char_index);
+
+ // then move all the subsequent nodes
+ split_obj = split_obj->getNext();
+ while (split_obj) {
+ Inkscape::XML::Node *move_repr = split_obj->getRepr();
+ SPObject *next_obj = split_obj->getNext(); // this is about to become invalidated by removeChild()
+ Inkscape::GC::anchor(move_repr);
+ split_obj->parent->getRepr()->removeChild(move_repr);
+ duplicate_obj->getRepr()->appendChild(move_repr);
+ Inkscape::GC::release(move_repr);
+
+ split_obj = next_obj;
+ }
+ return duplicate_obj->firstChild();
+}
+
+/** inserts a new line break at the given position in a text or flowtext
+object. If the position is in the middle of a span, the XML tree must be
+chopped in two such that the line can be created at the root of the text
+element. Returns an iterator pointing just after the inserted break. */
+Inkscape::Text::Layout::iterator sp_te_insert_line (SPItem *item, Inkscape::Text::Layout::iterator &position)
+{
+ // Disable newlines in a textpath; TODO: maybe on Enter in a textpath, separate it into two
+ // texpaths attached to the same path, with a vertical shift
+ if (SP_IS_TEXT_TEXTPATH (item) || SP_IS_TREF(item))
+ return position;
+
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+
+ // If this is plain SVG 1.1 text object without a tspan with sodipodi:role="line", we need
+ // to wrap it or our custom line breaking code won't work!
+ bool need_to_wrap = false;
+ SPText* text_object = dynamic_cast<SPText *>(item);
+ if (text_object && !text_object->has_shape_inside() && !text_object->has_inline_size()) {
+
+ need_to_wrap = true;
+ for (auto child : item->childList(false)) {
+ auto tspan = dynamic_cast<SPTSpan *>(child);
+ if (tspan && tspan->role == SP_TSPAN_ROLE_LINE) {
+ // Already wrapped
+ need_to_wrap = false;
+ break;
+ }
+ }
+
+ if (need_to_wrap) {
+
+ // We'll need to rebuild layout, so store character position:
+ int char_index = layout->iteratorToCharIndex(position);
+
+ // Create wrapping tspan.
+ Inkscape::XML::Node *text_repr = text_object->getRepr();
+ Inkscape::XML::Document *xml_doc = text_repr->document();
+ Inkscape::XML::Node *new_tspan_repr = xml_doc->createElement("svg:tspan");
+ new_tspan_repr->setAttribute("sodipodi:role", "line");
+
+ // Move text content to tspan and add tspan to text object.
+ // To do: This moves <desc> and <title> too.
+ move_child_nodes(text_repr, new_tspan_repr);
+ text_repr->appendChild(new_tspan_repr);
+
+ // Need to find new iterator.
+ text_object->rebuildLayout();
+ position = layout->charIndexToIterator(char_index);
+ }
+ }
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ SPObject *split_obj = nullptr;
+ Glib::ustring::iterator split_text_iter;
+ if (position != layout->end()) {
+ layout->getSourceOfCharacter(position, &split_obj, &split_text_iter);
+ }
+
+ if (split_obj == nullptr || is_line_break_object(split_obj)) {
+
+ if (split_obj == nullptr) split_obj = item->lastChild();
+
+ if (SP_IS_TREF(split_obj)) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
+ return position;
+ }
+
+ if (split_obj) {
+ Inkscape::XML::Document *xml_doc = split_obj->document->getReprDoc();
+ Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
+ // if we finally go to a text element without TSpan we mist set content to none
+ // new_node->setContent("");
+ split_obj->parent->getRepr()->addChild(new_node, split_obj->getRepr());
+ Inkscape::GC::release(new_node);
+ }
+
+ } else if (SP_IS_STRING(split_obj)) {
+
+ // If the parent is a tref, editing on this particular string is disallowed.
+ if (SP_IS_TREF(split_obj->parent)) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
+ return position;
+ }
+
+ Glib::ustring *string = &SP_STRING(split_obj)->string;
+ unsigned char_index = 0;
+ for (Glib::ustring::iterator it = string->begin() ; it != split_text_iter ; ++it)
+ char_index++;
+ // we need to split the entire text tree into two
+
+ SPObject *object = split_text_object_tree_at(split_obj, char_index);
+ if (object == nullptr) {
+ // Illegal document structure
+ return position;
+ }
+
+ SPString *new_string = SP_STRING(object);
+ new_string->getRepr()->setContent(&*split_text_iter.base()); // a little ugly
+ string->erase(split_text_iter, string->end());
+ split_obj->getRepr()->setContent(string->c_str());
+ // TODO: if the split point was at the beginning of a span we have a whole load of empty elements to clean up
+ } else {
+ // TODO
+ // I think the only case to put here is arbitrary gaps, which nobody uses yet
+ }
+
+ unsigned char_index = layout->iteratorToCharIndex(position);
+ te_update_layout_now(item);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ return layout->charIndexToIterator(char_index + 1);
+}
+
+/** finds the first SPString after the given position, including children, excluding parents */
+static SPString* sp_te_seek_next_string_recursive(SPObject *start_obj)
+{
+ while (start_obj) {
+ if (start_obj->hasChildren()) {
+ SPString *found_string = sp_te_seek_next_string_recursive(start_obj->firstChild());
+ if (found_string) {
+ return found_string;
+ }
+ }
+ if (SP_IS_STRING(start_obj)) {
+ return SP_STRING(start_obj);
+ }
+ start_obj = start_obj->getNext();
+ if (is_line_break_object(start_obj)) {
+ break; // don't cross line breaks
+ }
+ }
+ return nullptr;
+}
+
+/** inserts the given characters into the given string and inserts
+corresponding new x/y/dx/dy/rotate attributes into all its parents. */
+static void insert_into_spstring(SPString *string_item, Glib::ustring::iterator iter_at, gchar const *utf8)
+{
+ unsigned char_index = 0;
+ unsigned char_count = g_utf8_strlen(utf8, -1);
+ Glib::ustring *string = &string_item->string;
+
+ for (Glib::ustring::iterator it = string->begin() ; it != iter_at ; ++it)
+ char_index++;
+ string->replace(iter_at, iter_at, utf8);
+
+ SPObject *parent_item = string_item;
+ for ( ; ; ) {
+ char_index += sum_sibling_text_lengths_before(parent_item);
+ parent_item = parent_item->parent;
+ TextTagAttributes *attributes = attributes_for_object(parent_item);
+ if (!attributes) break;
+ attributes->insert(char_index, char_count);
+ }
+}
+
+/** Inserts the given text into a text or flowroot object. Line breaks
+cannot be inserted using this function, see sp_te_insert_line(). Returns
+an iterator pointing just after the inserted text. */
+Inkscape::Text::Layout::iterator
+sp_te_insert(SPItem *item, Inkscape::Text::Layout::iterator const &position, gchar const *utf8)
+{
+ if (!g_utf8_validate(utf8,-1,nullptr)) {
+ g_warning("Trying to insert invalid utf8");
+ return position;
+ }
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+ Glib::ustring::iterator iter_text;
+ // we want to insert after the previous char, not before the current char.
+ // it makes a difference at span boundaries
+ Inkscape::Text::Layout::iterator it_prev_char = position;
+ bool cursor_at_start = !it_prev_char.prevCharacter();
+ bool cursor_at_end = position == layout->end();
+ SPObject *source_obj = nullptr;
+ layout->getSourceOfCharacter(it_prev_char, &source_obj, &iter_text);
+ if (SP_IS_STRING(source_obj)) {
+ // If the parent is a tref, editing on this particular string is disallowed.
+ if (SP_IS_TREF(source_obj->parent)) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
+ return position;
+ }
+
+ // Now the simple case can begin...
+ if (!cursor_at_start){
+ ++iter_text;
+ }
+ SPString *string_item = SP_STRING(source_obj);
+ insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : iter_text, utf8);
+ } else {
+ // the not-so-simple case where we're at a line break or other control char; add to the next child/sibling SPString
+ Inkscape::XML::Document *xml_doc = item->getRepr()->document();
+ if (cursor_at_start) {
+ source_obj = item;
+ if (source_obj->hasChildren()) {
+ source_obj = source_obj->firstChild();
+ if (SP_IS_FLOWTEXT(item)) {
+ while (SP_IS_FLOWREGION(source_obj) || SP_IS_FLOWREGIONEXCLUDE(source_obj)) {
+ source_obj = source_obj->getNext();
+ }
+ if (source_obj == nullptr) {
+ source_obj = item;
+ }
+ }
+ }
+ if (source_obj == item && SP_IS_FLOWTEXT(item)) {
+ Inkscape::XML::Node *para = xml_doc->createElement("svg:flowPara");
+ item->getRepr()->appendChild(para);
+ source_obj = item->lastChild();
+ }
+ } else
+ source_obj = source_obj->getNext();
+
+ if (source_obj) { // never fails
+ SPString *string_item = sp_te_seek_next_string_recursive(source_obj);
+ if (string_item == nullptr) {
+ // need to add an SPString in this (pathological) case
+ Inkscape::XML::Node *rstring = xml_doc->createTextNode("");
+ source_obj->getRepr()->addChild(rstring, nullptr);
+ Inkscape::GC::release(rstring);
+ g_assert(SP_IS_STRING(source_obj->firstChild()));
+ string_item = SP_STRING(source_obj->firstChild());
+ }
+ // If the parent is a tref, editing on this particular string is disallowed.
+ if (SP_IS_TREF(string_item->parent)) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
+ return position;
+ }
+
+ insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : string_item->string.begin(), utf8);
+ }
+ }
+
+ unsigned char_index = layout->iteratorToCharIndex(position);
+ te_update_layout_now(item);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ return layout->charIndexToIterator(char_index + g_utf8_strlen(utf8, -1));
+}
+
+
+/* ***************************************************************************************************/
+// D E L E T I N G T E X T
+
+/** moves all the children of \a from_repr to \a to_repr, either before
+the existing children or after them. Order is maintained. The empty
+\a from_repr is not deleted. */
+static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend)
+{
+ while (from_repr->childCount()) {
+ Inkscape::XML::Node *child = prepend ? from_repr->lastChild() : from_repr->firstChild();
+ Inkscape::GC::anchor(child);
+ from_repr->removeChild(child);
+ if (prepend) to_repr->addChild(child, nullptr);
+ else to_repr->appendChild(child);
+ Inkscape::GC::release(child);
+ }
+}
+
+/** returns the object in the tree which is the closest ancestor of both
+\a one and \a two. It will never return anything higher than \a text. */
+static SPObject* get_common_ancestor(SPObject *text, SPObject *one, SPObject *two)
+{
+ if (one == nullptr || two == nullptr)
+ return text;
+ SPObject *common_ancestor = one;
+ if (SP_IS_STRING(common_ancestor))
+ common_ancestor = common_ancestor->parent;
+ while (!(common_ancestor == two || common_ancestor->isAncestorOf(two))) {
+ g_assert(common_ancestor != text);
+ common_ancestor = common_ancestor->parent;
+ }
+ return common_ancestor;
+}
+
+/** positions \a para_obj and \a text_iter to be pointing at the end
+of the last string in the last leaf object of \a para_obj. If the last
+leaf is not an SPString then \a text_iter will be unchanged. */
+static void move_to_end_of_paragraph(SPObject **para_obj, Glib::ustring::iterator *text_iter)
+{
+ while ((*para_obj)->hasChildren())
+ *para_obj = (*para_obj)->lastChild();
+ if (SP_IS_STRING(*para_obj))
+ *text_iter = SP_STRING(*para_obj)->string.end();
+}
+
+/** delete the line break pointed to by \a item by merging its children into
+the next suitable object and deleting \a item. Returns the object after the
+ones that have just been moved and sets \a next_is_sibling accordingly. */
+static SPObject* delete_line_break(SPObject *root, SPObject *item, bool *next_is_sibling)
+{
+ Inkscape::XML::Node *this_repr = item->getRepr();
+ SPObject *next_item = nullptr;
+ unsigned moved_char_count = sp_text_get_length(item) - 1; // the -1 is because it's going to count the line break
+
+ /* some sample cases (the div is the item to be deleted, the * represents where to put the new span):
+ <div></div><p>*text</p>
+ <p><div></div>*text</p>
+ <p><div></div></p><p>*text</p>
+ */
+ Inkscape::XML::Document *xml_doc = item->getRepr()->document();
+ Inkscape::XML::Node *new_span_repr = xml_doc->createElement(span_name_for_text_object(root));
+
+ new_span_repr->setAttributeOrRemoveIfEmpty("dx", this_repr->attribute("dx"));
+ new_span_repr->setAttributeOrRemoveIfEmpty("dy", this_repr->attribute("dy"));
+ new_span_repr->setAttributeOrRemoveIfEmpty("rotate", this_repr->attribute("rotate"));
+
+ SPObject *following_item = item;
+ while (following_item->getNext() == nullptr) {
+ following_item = following_item->parent;
+ g_assert(following_item != root);
+ }
+ following_item = following_item->getNext();
+
+ SPObject *new_parent_item;
+ if (SP_IS_STRING(following_item)) {
+ new_parent_item = following_item->parent;
+ new_parent_item->getRepr()->addChild(new_span_repr, following_item->getPrev() ? following_item->getPrev()->getRepr() : nullptr);
+ next_item = following_item;
+ *next_is_sibling = true;
+ } else {
+ new_parent_item = following_item;
+ next_item = new_parent_item->firstChild();
+ *next_is_sibling = true;
+ if (next_item == nullptr) {
+ next_item = new_parent_item;
+ *next_is_sibling = false;
+ }
+ new_parent_item->getRepr()->addChild(new_span_repr, nullptr);
+ }
+
+ // work around a bug in sp_style_write_difference() which causes the difference
+ // not to be written if the second param has a style set which the first does not
+ // by causing the first param to have everything set
+ SPCSSAttr *dest_node_attrs = sp_repr_css_attr(new_parent_item->getRepr(), "style");
+ SPCSSAttr *this_node_attrs = sp_repr_css_attr(this_repr, "style");
+ SPCSSAttr *this_node_attrs_inherited = sp_repr_css_attr_inherited(this_repr, "style");
+ for ( const auto & attr :dest_node_attrs->attributeList()) {
+ gchar const *key = g_quark_to_string(attr.key);
+ gchar const *this_attr = this_node_attrs_inherited->attribute(key);
+ if ((this_attr == nullptr || strcmp(attr.value, this_attr)) && this_node_attrs->attribute(key) == nullptr)
+ this_node_attrs->setAttribute(key, this_attr);
+ }
+ sp_repr_css_attr_unref(this_node_attrs_inherited);
+ sp_repr_css_attr_unref(this_node_attrs);
+ sp_repr_css_attr_unref(dest_node_attrs);
+ sp_repr_css_change(new_span_repr, this_node_attrs, "style");
+
+ TextTagAttributes *attributes = attributes_for_object(new_parent_item);
+ if (attributes)
+ attributes->insert(0, moved_char_count);
+ move_child_nodes(this_repr, new_span_repr);
+ this_repr->parent()->removeChild(this_repr);
+ return next_item;
+}
+
+/** erases the given characters from the given string and deletes the
+corresponding x/y/dx/dy/rotate attributes from all its parents. */
+static void erase_from_spstring(SPString *string_item, Glib::ustring::iterator iter_from, Glib::ustring::iterator iter_to)
+{
+ unsigned char_index = 0;
+ unsigned char_count = 0;
+ Glib::ustring *string = &string_item->string;
+
+ for (Glib::ustring::iterator it = string->begin() ; it != iter_from ; ++it){
+ char_index++;
+ }
+ for (Glib::ustring::iterator it = iter_from ; it != iter_to ; ++it){
+ char_count++;
+ }
+ string->erase(iter_from, iter_to);
+ string_item->getRepr()->setContent(string->c_str());
+
+ SPObject *parent_item = string_item;
+ for ( ; ; ) {
+ char_index += sum_sibling_text_lengths_before(parent_item);
+ parent_item = parent_item->parent;
+ TextTagAttributes *attributes = attributes_for_object(parent_item);
+ if (attributes == nullptr) {
+ break;
+ }
+
+ attributes->erase(char_index, char_count);
+ attributes->writeTo(parent_item->getRepr());
+ }
+}
+
+/* Deletes the given characters from a text or flowroot object. This is
+quite a complicated operation, partly due to the cleanup that is done if all
+the text in a subobject has been deleted, and partly due to the difficulty
+of figuring out what is a line break and how to delete one. Returns the
+real start and ending iterators based on the situation. */
+bool
+sp_te_delete (SPItem *item, Inkscape::Text::Layout::iterator const &start,
+ Inkscape::Text::Layout::iterator const &end, iterator_pair &iter_pair)
+{
+ bool success = false;
+
+ iter_pair.first = start;
+ iter_pair.second = end;
+
+ if (start == end) return success;
+
+ if (start > end) {
+ iter_pair.first = end;
+ iter_pair.second = start;
+ }
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+ SPObject *start_item = nullptr, *end_item = nullptr;
+ Glib::ustring::iterator start_text_iter, end_text_iter;
+ layout->getSourceOfCharacter(iter_pair.first, &start_item, &start_text_iter);
+ layout->getSourceOfCharacter(iter_pair.second, &end_item, &end_text_iter);
+ if (start_item == nullptr) {
+ return success; // start is at end of text
+ }
+ if (is_line_break_object(start_item)) {
+ move_to_end_of_paragraph(&start_item, &start_text_iter);
+ }
+ if (end_item == nullptr) {
+ end_item = item->lastChild();
+ move_to_end_of_paragraph(&end_item, &end_text_iter);
+ } else if (is_line_break_object(end_item)) {
+ move_to_end_of_paragraph(&end_item, &end_text_iter);
+ }
+
+ SPObject *common_ancestor = get_common_ancestor(item, start_item, end_item);
+
+ bool has_text_decoration = false;
+ gchar const *root_style = (item)->getRepr()->attribute("style");
+ if(root_style && strstr(root_style,"text-decoration"))has_text_decoration = true;
+
+ if (start_item == end_item) {
+ // the quick case where we're deleting stuff all from the same string
+ if (SP_IS_STRING(start_item)) { // always true (if it_start != it_end anyway)
+ // If the parent is a tref, editing on this particular string is disallowed.
+ if (SP_IS_TREF(start_item->parent)) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
+ } else {
+ erase_from_spstring(SP_STRING(start_item), start_text_iter, end_text_iter);
+ success = true;
+ }
+ }
+ } else {
+ SPObject *sub_item = start_item;
+ // walk the tree from start_item to end_item, deleting as we go
+ while (sub_item != item) {
+ if (sub_item == end_item) {
+ if (SP_IS_STRING(sub_item)) {
+ // If the parent is a tref, editing on this particular string is disallowed.
+ if (SP_IS_TREF(sub_item->parent)) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
+ break;
+ }
+
+ Glib::ustring *string = &SP_STRING(sub_item)->string;
+ erase_from_spstring(SP_STRING(sub_item), string->begin(), end_text_iter);
+ success = true;
+ }
+ break;
+ }
+ if (SP_IS_STRING(sub_item)) {
+ SPString *string = SP_STRING(sub_item);
+ if (sub_item == start_item)
+ erase_from_spstring(string, start_text_iter, string->string.end());
+ else
+ erase_from_spstring(string, string->string.begin(), string->string.end());
+ success = true;
+ }
+ // walk to the next item in the tree
+ if (sub_item->hasChildren())
+ sub_item = sub_item->firstChild();
+ else {
+ SPObject *next_item;
+ do {
+ bool is_sibling = true;
+ next_item = sub_item->getNext();
+ if (next_item == nullptr) {
+ next_item = sub_item->parent;
+ is_sibling = false;
+ }
+
+ if (is_line_break_object(sub_item))
+ next_item = delete_line_break(item, sub_item, &is_sibling);
+
+ sub_item = next_item;
+ if (is_sibling) break;
+ // no more siblings, go up a parent
+ } while (sub_item != item && sub_item != end_item);
+ }
+ }
+ }
+
+ while (tidy_xml_tree_recursively(common_ancestor, has_text_decoration)){};
+ te_update_layout_now(item);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ layout->validateIterator(&iter_pair.first);
+ layout->validateIterator(&iter_pair.second);
+ return success;
+}
+
+
+/* ***************************************************************************************************/
+// P L A I N T E X T F U N C T I O N S
+
+/** Gets a text-only representation of the given text or flowroot object,
+replacing line break elements with '\n'. */
+static void sp_te_get_ustring_multiline(SPObject const *root, Glib::ustring *string, bool *pending_line_break)
+{
+ if (*pending_line_break) {
+ *string += '\n';
+ *pending_line_break = false;
+ }
+ for (auto& child: root->children) {
+ if (SP_IS_STRING(&child)) {
+ *string += SP_STRING(&child)->string;
+ } else if (is_part_of_text_subtree(&child)) {
+ sp_te_get_ustring_multiline(&child, string, pending_line_break);
+ }
+ }
+ if (!SP_IS_TEXT(root) && !SP_IS_TEXTPATH(root) && is_line_break_object(root)) {
+ *pending_line_break = true;
+ }
+}
+
+/** Gets a text-only representation of the given text or flowroot object,
+replacing line break elements with '\n'. The return value must be free()d. */
+Glib::ustring
+sp_te_get_string_multiline (SPItem const *text)
+{
+ Glib::ustring string;
+ bool pending_line_break = false;
+
+ if (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)) {
+ sp_te_get_ustring_multiline(text, &string, &pending_line_break);
+ }
+ return string;
+}
+
+/** Gets a text-only representation of the characters in a text or flowroot
+object from \a start to \a end only. Line break elements are replaced with
+'\n'. */
+Glib::ustring
+sp_te_get_string_multiline (SPItem const *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end)
+{
+ if (start == end) return "";
+ Inkscape::Text::Layout::iterator first, last;
+ if (start < end) {
+ first = start;
+ last = end;
+ } else {
+ first = end;
+ last = start;
+ }
+ Inkscape::Text::Layout const *layout = te_get_layout(text);
+ Glib::ustring result;
+ // not a particularly fast piece of code. I'll optimise it if people start to notice.
+ for ( ; first < last ; first.nextCharacter()) {
+ SPObject *char_item = nullptr;
+ Glib::ustring::iterator text_iter;
+ layout->getSourceOfCharacter(first, &char_item, &text_iter);
+ if (SP_IS_STRING(char_item)) {
+ result += *text_iter;
+ } else {
+ result += '\n';
+ }
+ }
+ return result;
+}
+
+void
+sp_te_set_repr_text_multiline(SPItem *text, gchar const *str)
+{
+ g_return_if_fail (text != nullptr);
+ g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
+
+ Inkscape::XML::Document *xml_doc = text->getRepr()->document();
+ Inkscape::XML::Node *repr;
+ SPObject *object;
+ bool is_textpath = false;
+ if (SP_IS_TEXT_TEXTPATH (text)) {
+ repr = text->firstChild()->getRepr();
+ object = text->firstChild();
+ is_textpath = true;
+ } else {
+ repr = text->getRepr();
+ object = text;
+ }
+
+ if (!str) {
+ str = "";
+ }
+ gchar *content = g_strdup (str);
+
+ repr->setContent("");
+ for (auto& child: object->childList(false)) {
+ if (!SP_IS_FLOWREGION(child) && !SP_IS_FLOWREGIONEXCLUDE(child)) {
+ repr->removeChild(child->getRepr());
+ }
+ }
+
+ if (is_textpath) {
+ gchar *p = content;
+ while (*p != '\0') {
+ if (*p == '\n') {
+ *p = ' '; // No lines for textpath, replace newlines with spaces.
+ }
+ ++p;
+ }
+ Inkscape::XML::Node *rstr = xml_doc->createTextNode(content);
+ repr->addChild(rstr, nullptr);
+ Inkscape::GC::release(rstr);
+ } else {
+ SPText* sptext = dynamic_cast<SPText *>(text);
+ if (sptext && (sptext->has_inline_size() || sptext->has_shape_inside())) {
+ // Do nothing... we respect newlines (and assume CSS already set to do so).
+ Inkscape::XML::Node *rstr = xml_doc->createTextNode(content);
+ repr->addChild(rstr, nullptr);
+ Inkscape::GC::release(rstr);
+ } else {
+ // Break into tspans with sodipodi:role="line".
+ gchar *p = content;
+ while (p) {
+ gchar *e = strchr (p, '\n');
+ if (e) *e = '\0';
+ Inkscape::XML::Node *rtspan;
+ if (SP_IS_TEXT(text)) { // create a tspan for each line
+ rtspan = xml_doc->createElement("svg:tspan");
+ rtspan->setAttribute("sodipodi:role", "line");
+ } else { // create a flowPara for each line
+ rtspan = xml_doc->createElement("svg:flowPara");
+ }
+ Inkscape::XML::Node *rstr = xml_doc->createTextNode(p);
+ rtspan->addChild(rstr, nullptr);
+ Inkscape::GC::release(rstr);
+ repr->appendChild(rtspan);
+ Inkscape::GC::release(rtspan);
+
+ p = (e) ? e + 1 : nullptr;
+ }
+ }
+ }
+ g_free (content);
+}
+
+/* ***************************************************************************************************/
+// K E R N I N G A N D S P A C I N G
+
+/** Returns the attributes block and the character index within that block
+which represents the iterator \a position. */
+TextTagAttributes*
+text_tag_attributes_at_position(SPItem *item, Inkscape::Text::Layout::iterator const &position, unsigned *char_index)
+{
+ if (item == nullptr || char_index == nullptr || !SP_IS_TEXT(item)) {
+ return nullptr; // flowtext doesn't support kerning yet
+ }
+ SPText *text = SP_TEXT(item);
+
+ SPObject *source_item = nullptr;
+ Glib::ustring::iterator source_text_iter;
+ text->layout.getSourceOfCharacter(position, &source_item, &source_text_iter);
+
+ if (!SP_IS_STRING(source_item)) {
+ return nullptr;
+ }
+ Glib::ustring *string = &SP_STRING(source_item)->string;
+ *char_index = sum_sibling_text_lengths_before(source_item);
+ for (Glib::ustring::iterator it = string->begin() ; it != source_text_iter ; ++it) {
+ ++*char_index;
+ }
+
+ return attributes_for_object(source_item->parent);
+}
+
+void
+sp_te_adjust_kerning_screen (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, Geom::Point by)
+{
+ // divide increment by zoom
+ // divide increment by matrix expansion
+ gdouble factor = 1 / desktop->current_zoom();
+ Geom::Affine t (item->i2doc_affine());
+ factor = factor / t.descrim();
+ by = factor * by;
+
+ unsigned char_index;
+ TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
+ if (attributes) attributes->addToDxDy(char_index, by);
+ if (start != end) {
+ attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
+ if (attributes) attributes->addToDxDy(char_index, -by);
+ }
+
+ item->updateRepr();
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void sp_te_adjust_dx(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop * /*desktop*/, double delta)
+{
+ unsigned char_index = 0;
+ TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
+ if (attributes) {
+ attributes->addToDx(char_index, delta);
+ }
+ if (start != end) {
+ attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
+ if (attributes) {
+ attributes->addToDx(char_index, -delta);
+ }
+ }
+
+ item->updateRepr();
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void sp_te_adjust_dy(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop * /*desktop*/, double delta)
+{
+ unsigned char_index = 0;
+ TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
+ if (attributes) {
+ attributes->addToDy(char_index, delta);
+ }
+ if (start != end) {
+ attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
+ if (attributes) {
+ attributes->addToDy(char_index, -delta);
+ }
+ }
+
+ item->updateRepr();
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+sp_te_adjust_rotation_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble pixels)
+{
+ // divide increment by zoom
+ // divide increment by matrix expansion
+ gdouble factor = 1 / desktop->current_zoom();
+ Geom::Affine t (text->i2doc_affine());
+ factor = factor / t.descrim();
+ Inkscape::Text::Layout const *layout = te_get_layout(text);
+ if (layout == nullptr) return;
+ SPObject *source_item = nullptr;
+ layout->getSourceOfCharacter(std::min(start, end), &source_item);
+ if (source_item == nullptr) {
+ return;
+ }
+ gdouble degrees = (180/M_PI) * atan2(pixels, source_item->parent->style->font_size.computed / factor);
+
+ sp_te_adjust_rotation(text, start, end, desktop, degrees);
+}
+
+void
+sp_te_adjust_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop */*desktop*/, gdouble degrees)
+{
+ unsigned char_index;
+ TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index);
+ if (attributes == nullptr) return;
+
+ if (start != end) {
+ for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) {
+ attributes = text_tag_attributes_at_position(text, it, &char_index);
+ if (attributes) attributes->addToRotate(char_index, degrees);
+ }
+ } else
+ attributes->addToRotate(char_index, degrees);
+
+ text->updateRepr();
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void sp_te_set_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop */*desktop*/, gdouble degrees)
+{
+ unsigned char_index = 0;
+ TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index);
+ if (attributes != nullptr) {
+ if (start != end) {
+ for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) {
+ attributes = text_tag_attributes_at_position(text, it, &char_index);
+ if (attributes) {
+ attributes->setRotate(char_index, degrees);
+ }
+ }
+ } else {
+ attributes->setRotate(char_index, degrees);
+ }
+
+ text->updateRepr();
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+}
+
+void
+sp_te_adjust_tspan_letterspacing_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble by)
+{
+ g_return_if_fail (text != nullptr);
+ g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
+
+ Inkscape::Text::Layout const *layout = te_get_layout(text);
+
+ gdouble val;
+ SPObject *source_obj = nullptr;
+ unsigned nb_let;
+ layout->getSourceOfCharacter(std::min(start, end), &source_obj);
+
+ if (source_obj == nullptr) { // end of text
+ source_obj = text->lastChild();
+ }
+ if (SP_IS_STRING(source_obj)) {
+ source_obj = source_obj->parent;
+ }
+
+ SPStyle *style = source_obj->style;
+
+ // calculate real value
+ /* TODO: Consider calculating val unconditionally, i.e. drop the first `if' line, and
+ get rid of the `else val = 0.0'. Similarly below and in sp-string.cpp. */
+ if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
+ if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
+ val = style->font_size.computed * style->letter_spacing.value;
+ } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
+ val = style->font_size.computed * style->letter_spacing.value * 0.5;
+ } else { // unknown unit - should not happen
+ val = 0.0;
+ }
+ } else { // there's a real value in .computed, or it's zero
+ val = style->letter_spacing.computed;
+ }
+
+ if (start == end) {
+ while (!is_line_break_object(source_obj)) { // move up the tree so we apply to the closest paragraph
+ source_obj = source_obj->parent;
+ }
+ nb_let = sp_text_get_length(source_obj);
+ } else {
+ nb_let = abs(layout->iteratorToCharIndex(end) - layout->iteratorToCharIndex(start));
+ }
+
+ // divide increment by zoom and by the number of characters in the line,
+ // so that the entire line is expanded by by pixels, no matter what its length
+ gdouble const zoom = desktop->current_zoom();
+ gdouble const zby = (by
+ / (zoom * (nb_let > 1 ? nb_let - 1 : 1))
+ / SP_ITEM(source_obj)->i2doc_affine().descrim());
+ val += zby;
+
+ if (start == end) {
+ // set back value to entire paragraph
+ style->letter_spacing.normal = FALSE;
+ if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
+ if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
+ style->letter_spacing.value = val / style->font_size.computed;
+ } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
+ style->letter_spacing.value = val / style->font_size.computed * 2;
+ }
+ } else {
+ style->letter_spacing.computed = val;
+ }
+
+ style->letter_spacing.set = TRUE;
+ } else {
+ // apply to selection only
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ char string_val[40];
+ g_snprintf(string_val, sizeof(string_val), "%f", val);
+ sp_repr_css_set_property(css, "letter-spacing", string_val);
+ sp_te_apply_style(text, start, end, css);
+ sp_repr_css_attr_unref(css);
+ }
+
+ text->updateRepr();
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
+}
+
+// Only used for page-up and page-down and sp_te_adjust_linespacing_screen
+double
+sp_te_get_average_linespacing (SPItem *text)
+{
+ Inkscape::Text::Layout const *layout = te_get_layout(text);
+ if (!layout)
+ return 0;
+
+ unsigned line_count = layout->lineIndex(layout->end());
+ auto mode = text->style->writing_mode.computed;
+ bool horizontal = (mode == SP_CSS_WRITING_MODE_LR_TB || mode == SP_CSS_WRITING_MODE_RL_TB);
+ auto index = horizontal ? Geom::Y : Geom::X;
+ double all_lines_height = layout->characterAnchorPoint(layout->end())[index] - layout->characterAnchorPoint(layout->begin())[index];
+ double average_line_height = all_lines_height / (line_count == 0 ? 1 : line_count);
+ if (mode == SP_CSS_WRITING_MODE_TB_RL) {
+ average_line_height = -average_line_height;
+ }
+ return average_line_height;
+}
+
+/** Adjust the line height by 'amount'.
+ * If top_level is true then 'line-height' will be set where possible,
+ * otherwise objects that inherit line-height will not be touched.
+ */
+void
+sp_te_adjust_line_height (SPObject *object, double amount, double average, bool top_level = true) {
+
+ SPStyle *style = object->style;
+
+ // Always set if top level true.
+ // Also set if line_height is set to a non-zero value.
+ if (top_level ||
+ (style->line_height.set && !style->line_height.inherit && style->line_height.computed != 0)){
+
+ // Scale default values
+ if (!style->line_height.set || style->line_height.inherit || style->line_height.normal) {
+ style->line_height.set = TRUE;
+ style->line_height.inherit = FALSE;
+ style->line_height.normal = FALSE;
+ style->line_height.unit = SP_CSS_UNIT_NONE;
+ style->line_height.value = style->line_height.computed = Inkscape::Text::Layout::LINE_HEIGHT_NORMAL;
+ }
+
+ switch (style->line_height.unit) {
+
+ case SP_CSS_UNIT_NONE:
+ default:
+ // Multiplier-type units, stored in computed
+ if (fabs(style->line_height.computed) < 0.001) {
+ style->line_height.computed = amount < 0.0 ? -0.001 : 0.001;
+ // the formula below could get stuck at zero
+ } else {
+ style->line_height.computed *= (average + amount) / average;
+ }
+ style->line_height.value = style->line_height.computed;
+ break;
+
+
+ // Relative units, stored in value
+ case SP_CSS_UNIT_EM:
+ case SP_CSS_UNIT_EX:
+ case SP_CSS_UNIT_PERCENT:
+ if (fabs(style->line_height.value) < 0.001) {
+ style->line_height.value = amount < 0.0 ? -0.001 : 0.001;
+ } else {
+ style->line_height.value *= (average + amount) / average;
+ }
+ break;
+
+
+ // Absolute units
+ case SP_CSS_UNIT_PX:
+ style->line_height.computed += amount;
+ style->line_height.value = style->line_height.computed;
+ break;
+ case SP_CSS_UNIT_PT:
+ style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "pt");
+ style->line_height.value = style->line_height.computed;
+ break;
+ case SP_CSS_UNIT_PC:
+ style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "pc");
+ style->line_height.value = style->line_height.computed;
+ break;
+ case SP_CSS_UNIT_MM:
+ style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "mm");
+ style->line_height.value = style->line_height.computed;
+ break;
+ case SP_CSS_UNIT_CM:
+ style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "cm");
+ style->line_height.value = style->line_height.computed;
+ break;
+ case SP_CSS_UNIT_IN:
+ style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "in");
+ style->line_height.value = style->line_height.computed;
+ break;
+ }
+ object->updateRepr();
+ }
+
+ std::vector<SPObject*> children = object->childList(false);
+ for (auto child: children) {
+ sp_te_adjust_line_height (child, amount, average, false);
+ }
+}
+
+void
+sp_te_adjust_linespacing_screen (SPItem *text, Inkscape::Text::Layout::iterator const &/*start*/, Inkscape::Text::Layout::iterator const &/*end*/, SPDesktop *desktop, gdouble by)
+{
+ // TODO: use start and end iterators to delineate the area to be affected
+ g_return_if_fail (text != nullptr);
+ g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
+
+ Inkscape::Text::Layout const *layout = te_get_layout(text);
+
+ double average_line_height = sp_te_get_average_linespacing (text);
+ if (fabs(average_line_height) < 0.001) average_line_height = 0.001;
+
+ // divide increment by zoom and by the number of lines,
+ // so that the entire object is expanded by by pixels
+ unsigned line_count = layout->lineIndex(layout->end());
+ gdouble zby = by / (desktop->current_zoom() * (line_count == 0 ? 1 : line_count));
+
+ // divide increment by matrix expansion
+ Geom::Affine t(text->i2doc_affine());
+ zby = zby / t.descrim();
+
+ sp_te_adjust_line_height (text, zby, average_line_height, false);
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
+}
+
+
+/* ***************************************************************************************************/
+// S T Y L E A P P L I C A T I O N
+
+
+/** converts an iterator to a character index, mainly because ustring::substr()
+doesn't have a version that takes iterators as parameters. */
+static unsigned char_index_of_iterator(Glib::ustring const &string, Glib::ustring::const_iterator text_iter)
+{
+ unsigned n = 0;
+ for (Glib::ustring::const_iterator it = string.begin() ; it != string.end() && it != text_iter ; ++it)
+ n++;
+ return n;
+}
+
+// Move to style.h?
+/** applies the given style string on top of the existing styles for \a item,
+as opposed to sp_style_merge_from_style_string which merges its parameter
+underneath the existing styles (ie ignoring already set properties). */
+static void overwrite_style_with_string(SPObject *item, gchar const *style_string)
+{
+ SPStyle style(item->document);
+ style.mergeString(style_string);
+ gchar const *item_style_string = item->getRepr()->attribute("style");
+ if (item_style_string && *item_style_string) {
+ style.mergeString(item_style_string);
+ }
+ Glib::ustring new_style_string = style.write();
+ item->setAttributeOrRemoveIfEmpty("style", new_style_string);
+}
+
+// Move to style.h?
+/** Returns true if the style of \a parent and the style of \a child are
+equivalent (and hence the children of both will appear the same). It is a
+limitation of the current implementation that \a parent must be a (not
+necessarily immediate) ancestor of \a child. */
+static bool objects_have_equal_style(SPObject const *parent, SPObject const *child)
+{
+ // the obvious implementation of strcmp(style_write_all(parent), style_write_all(child))
+ // will not work. Firstly because of an inheritance bug in style.cpp that has
+ // implications too large for me to feel safe fixing, but mainly because the css spec
+ // requires that the computed value is inherited, not the specified value.
+ g_assert(parent->isAncestorOf(child));
+
+ Glib::ustring parent_style = parent->style->write( SP_STYLE_FLAG_ALWAYS );
+
+ // we have to write parent_style then read it again, because some properties format their values
+ // differently depending on whether they're set or not (*cough*dash-offset*cough*)
+ SPStyle parent_spstyle(parent->document);
+ parent_spstyle.mergeString(parent_style.c_str());
+ parent_style = parent_spstyle.write(SP_STYLE_FLAG_ALWAYS);
+
+ Glib::ustring child_style_construction;
+ while (child != parent) {
+ // FIXME: this assumes that child's style is only in style= whereas it can also be in css attributes!
+ char const *style_text = child->getRepr()->attribute("style");
+ if (style_text && *style_text) {
+ child_style_construction.insert(0, style_text);
+ child_style_construction.insert(0, 1, ';');
+ }
+ child = child->parent;
+ }
+ child_style_construction.insert(0, parent_style);
+
+ SPStyle child_spstyle(parent->document);
+ child_spstyle.mergeString(child_style_construction.c_str());
+ Glib::ustring child_style = child_spstyle.write(SP_STYLE_FLAG_ALWAYS);
+
+ bool equal = (child_style == parent_style); // Glib::ustring overloads == operator
+ return equal;
+}
+
+/** returns true if \a first and \a second contain all the same attributes
+with the same values as each other. Note that we have to compare both
+forwards and backwards to make sure we don't miss any attributes that are
+in one but not the other. */
+static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second)
+{
+ for ( const auto & attr : first->attributeList()) {
+ gchar const *other_attr = second->attribute(g_quark_to_string(attr.key));
+ if (other_attr == nullptr || strcmp(attr.value, other_attr))
+ return false;
+ }
+ for (const auto & attr : second->attributeList()) {
+ gchar const *other_attr = first->attribute(g_quark_to_string(attr.key));
+ if (other_attr == nullptr || strcmp(attr.value, other_attr))
+ return false;
+ }
+ return true;
+}
+
+/** sets the given css attribute on this object and all its descendants.
+Annoyingly similar to sp_desktop_apply_css_recursive(), except without the
+transform stuff. */
+static void apply_css_recursive(SPObject *o, SPCSSAttr const *css)
+{
+ sp_repr_css_change(o->getRepr(), const_cast<SPCSSAttr*>(css), "style");
+
+ for (auto& child: o->children) {
+ if (sp_repr_css_property(const_cast<SPCSSAttr*>(css), "opacity", nullptr) != nullptr) {
+ // Unset properties which are accumulating and thus should not be set recursively.
+ // For example, setting opacity 0.5 on a group recursively would result in the visible opacity of 0.25 for an item in the group.
+ SPCSSAttr *css_recurse = sp_repr_css_attr_new();
+ sp_repr_css_merge(css_recurse, const_cast<SPCSSAttr*>(css));
+ sp_repr_css_set_property(css_recurse, "opacity", nullptr);
+ apply_css_recursive(&child, css_recurse);
+ sp_repr_css_attr_unref(css_recurse);
+ } else {
+ apply_css_recursive(&child, const_cast<SPCSSAttr*>(css));
+ }
+ }
+}
+
+/** applies the given style to all the objects at the given level and below
+which are between \a start_item and \a end_item, creating spans as necessary.
+If \a start_item or \a end_item are NULL then the style is applied to all
+objects to the beginning or end respectively. \a span_object_name is the
+name of the xml for a text span (ie tspan or flowspan). */
+static void recursively_apply_style(SPObject *common_ancestor, SPCSSAttr const *css, SPObject *start_item, Glib::ustring::iterator start_text_iter, SPObject *end_item, Glib::ustring::iterator end_text_iter, char const *span_object_name)
+{
+ bool passed_start = start_item == nullptr ? true : false;
+ Inkscape::XML::Document *xml_doc = common_ancestor->document->getReprDoc();
+
+ for (SPObject *child = common_ancestor->firstChild() ; child ; child = child->getNext()) {
+ if (start_item == child) {
+ passed_start = true;
+ }
+
+ if (passed_start) {
+ if (end_item && child->isAncestorOf(end_item)) {
+ recursively_apply_style(child, css, nullptr, start_text_iter, end_item, end_text_iter, span_object_name);
+ break;
+ }
+ // apply style
+
+ // note that when adding stuff we must make sure that 'child' stays valid so the for loop keeps working.
+ // often this means that new spans are created before child and child is modified only
+ if (SP_IS_STRING(child)) {
+ SPString *string_item = SP_STRING(child);
+ bool surround_entire_string = true;
+
+ Inkscape::XML::Node *child_span = xml_doc->createElement(span_object_name);
+ sp_repr_css_set(child_span, const_cast<SPCSSAttr*>(css), "style"); // better hope that prototype wasn't nonconst for a good reason
+ SPObject *prev_item = child->getPrev();
+ Inkscape::XML::Node *prev_repr = prev_item ? prev_item->getRepr() : nullptr;
+
+ if (child == start_item || child == end_item) {
+ surround_entire_string = false;
+ if (start_item == end_item && start_text_iter != string_item->string.begin()) {
+ // eg "abcDEFghi" -> "abc"<span>"DEF"</span>"ghi"
+ unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
+ unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
+
+ Inkscape::XML::Node *text_before = xml_doc->createTextNode(string_item->string.substr(0, start_char_index).c_str());
+ common_ancestor->getRepr()->addChild(text_before, prev_repr);
+ common_ancestor->getRepr()->addChild(child_span, text_before);
+ Inkscape::GC::release(text_before);
+ Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(start_char_index, end_char_index - start_char_index).c_str());
+ child_span->appendChild(text_in_span);
+ Inkscape::GC::release(text_in_span);
+ child->getRepr()->setContent(string_item->string.substr(end_char_index).c_str());
+ } else if (child == end_item) {
+ // eg "ABCdef" -> <span>"ABC"</span>"def"
+ // (includes case where start_text_iter == begin())
+ // NB: we might create an empty string here. Doesn't matter, it'll get cleaned up later
+ unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
+
+ common_ancestor->getRepr()->addChild(child_span, prev_repr);
+ Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(0, end_char_index).c_str());
+ child_span->appendChild(text_in_span);
+ Inkscape::GC::release(text_in_span);
+ child->getRepr()->setContent(string_item->string.substr(end_char_index).c_str());
+ } else if (start_text_iter != string_item->string.begin()) {
+ // eg "abcDEF" -> "abc"<span>"DEF"</span>
+ unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
+
+ Inkscape::XML::Node *text_before = xml_doc->createTextNode(string_item->string.substr(0, start_char_index).c_str());
+ common_ancestor->getRepr()->addChild(text_before, prev_repr);
+ common_ancestor->getRepr()->addChild(child_span, text_before);
+ Inkscape::GC::release(text_before);
+ Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(start_char_index).c_str());
+ child_span->appendChild(text_in_span);
+ Inkscape::GC::release(text_in_span);
+ child->deleteObject();
+ child = common_ancestor->get_child_by_repr(child_span);
+
+ } else
+ surround_entire_string = true;
+ }
+ if (surround_entire_string) {
+ Inkscape::XML::Node *child_repr = child->getRepr();
+ common_ancestor->getRepr()->addChild(child_span, child_repr);
+ Inkscape::GC::anchor(child_repr);
+ common_ancestor->getRepr()->removeChild(child_repr);
+ child_span->appendChild(child_repr);
+ Inkscape::GC::release(child_repr);
+ child = common_ancestor->get_child_by_repr(child_span);
+ }
+ Inkscape::GC::release(child_span);
+
+ } else if (child != end_item) { // not a string and we're applying to the entire object. This is easy
+ apply_css_recursive(child, css);
+ }
+
+ } else { // !passed_start
+ if (child->isAncestorOf(start_item)) {
+ recursively_apply_style(child, css, start_item, start_text_iter, end_item, end_text_iter, span_object_name);
+ if (end_item && child->isAncestorOf(end_item))
+ break; // only happens when start_item == end_item (I think)
+ passed_start = true;
+ }
+ }
+
+ if (end_item == child)
+ break;
+ }
+}
+
+/* if item is at the beginning of a tree it doesn't matter which element
+it points to so for neatness we would like it to point to the highest
+possible child of \a common_ancestor. There is no iterator return because
+a string can never be an ancestor.
+
+eg: <span><span>*ABC</span>DEFghi</span> where * is the \a item. We would
+like * to point to the inner span because we can apply style to that whole
+span. */
+static SPObject* ascend_while_first(SPObject *item, Glib::ustring::iterator text_iter, SPObject *common_ancestor)
+{
+ if (item == common_ancestor)
+ return item;
+ if (SP_IS_STRING(item))
+ if (text_iter != SP_STRING(item)->string.begin())
+ return item;
+ for ( ; ; ) {
+ SPObject *parent = item->parent;
+ if (parent == common_ancestor) {
+ break;
+ }
+ if (item != parent->firstChild()) {
+ break;
+ }
+ item = parent;
+ }
+ return item;
+}
+
+
+/** empty spans: abc<span></span>def
+ -> abcdef */
+static bool tidy_operator_empty_spans(SPObject **item, bool /*has_text_decoration*/)
+{
+ bool result = false;
+ if ( !(*item)->hasChildren()
+ && !is_line_break_object(*item)
+ && !(SP_IS_STRING(*item) && !SP_STRING(*item)->string.empty())
+ ) {
+ SPObject *next = (*item)->getNext();
+ (*item)->deleteObject();
+ *item = next;
+ result = true;
+ }
+ return result;
+}
+
+/** inexplicable spans: abc<span style="">def</span>ghi
+ -> "abc""def""ghi"
+the repeated strings will be merged by another operator. */
+static bool tidy_operator_inexplicable_spans(SPObject **item, bool /*has_text_decoration*/)
+{
+ //XML Tree being directly used here while it shouldn't be.
+ if (*item && sp_repr_is_meta_element((*item)->getRepr())) {
+ return false;
+ }
+ if (SP_IS_STRING(*item)) {
+ return false;
+ }
+ if (is_line_break_object(*item)) {
+ return false;
+ }
+ TextTagAttributes *attrs = attributes_for_object(*item);
+ if (attrs && attrs->anyAttributesSet()) {
+ return false;
+ }
+ if (!objects_have_equal_style((*item)->parent, *item)) {
+ return false;
+ }
+ SPObject *next = *item;
+ while ((*item)->hasChildren()) {
+ Inkscape::XML::Node *repr = (*item)->firstChild()->getRepr();
+ Inkscape::GC::anchor(repr);
+ (*item)->getRepr()->removeChild(repr);
+ (*item)->parent->getRepr()->addChild(repr, next->getRepr());
+ Inkscape::GC::release(repr);
+ next = next->getNext();
+ }
+ (*item)->deleteObject();
+ *item = next;
+ return true;
+}
+
+/** repeated spans: <font a>abc</font><font a>def</font>
+ -> <font a>abcdef</font> */
+static bool tidy_operator_repeated_spans(SPObject **item, bool /*has_text_decoration*/)
+{
+ SPObject *first = *item;
+ SPObject *second = first->getNext();
+ if (second == nullptr) return false;
+
+ Inkscape::XML::Node *first_repr = first->getRepr();
+ Inkscape::XML::Node *second_repr = second->getRepr();
+
+ if (first_repr->type() != second_repr->type()) return false;
+
+ if (SP_IS_STRING(first) && SP_IS_STRING(second)) {
+ // also amalgamate consecutive SPStrings into one
+ Glib::ustring merged_string = SP_STRING(first)->string + SP_STRING(second)->string;
+ first->getRepr()->setContent(merged_string.c_str());
+ second_repr->parent()->removeChild(second_repr);
+ return true;
+ }
+
+ // merge consecutive spans with identical styles into one
+ if (first_repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE) return false;
+ if (strcmp(first_repr->name(), second_repr->name()) != 0) return false;
+ if (is_line_break_object(second)) return false;
+ gchar const *first_style = first_repr->attribute("style");
+ gchar const *second_style = second_repr->attribute("style");
+ if (!((first_style == nullptr && second_style == nullptr)
+ || (first_style != nullptr && second_style != nullptr && !strcmp(first_style, second_style))))
+ return false;
+
+ // all our tests passed: do the merge
+ TextTagAttributes *attributes_first = attributes_for_object(first);
+ TextTagAttributes *attributes_second = attributes_for_object(second);
+ if (attributes_first && attributes_second && attributes_second->anyAttributesSet()) {
+ TextTagAttributes attributes_first_copy = *attributes_first;
+ attributes_first->join(attributes_first_copy, *attributes_second, sp_text_get_length(first));
+ }
+ move_child_nodes(second_repr, first_repr);
+ second_repr->parent()->removeChild(second_repr);
+ return true;
+ // *item is still the next object to process
+}
+
+/** redundant nesting: <font a><font b>abc</font></font>
+ -> <font b>abc</font>
+ excessive nesting: <font a><size 1>abc</size></font>
+ -> <font a,size 1>abc</font> */
+static bool tidy_operator_excessive_nesting(SPObject **item, bool /*has_text_decoration*/)
+{
+ if (!(*item)->hasChildren()) {
+ return false;
+ }
+ if ((*item)->firstChild() != (*item)->lastChild()) {
+ return false;
+ }
+ if (SP_IS_FLOWREGION((*item)->firstChild()) || SP_IS_FLOWREGIONEXCLUDE((*item)->firstChild())) {
+ return false;
+ }
+ if (SP_IS_STRING((*item)->firstChild())) {
+ return false;
+ }
+ if (is_line_break_object((*item)->firstChild())) {
+ return false;
+ }
+ TextTagAttributes *attrs = attributes_for_object((*item)->firstChild());
+ if (attrs && attrs->anyAttributesSet()) {
+ return false;
+ }
+ gchar const *child_style = (*item)->firstChild()->getRepr()->attribute("style");
+ if (child_style && *child_style) {
+ overwrite_style_with_string(*item, child_style);
+ }
+ move_child_nodes((*item)->firstChild()->getRepr(), (*item)->getRepr());
+ (*item)->firstChild()->deleteObject();
+ return true;
+}
+
+/** helper for tidy_operator_redundant_double_nesting() */
+static bool redundant_double_nesting_processor(SPObject **item, SPObject *child, bool prepend)
+{
+ if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child)) {
+ return false;
+ }
+ if (SP_IS_STRING(child)) {
+ return false;
+ }
+ if (is_line_break_object(child)) {
+ return false;
+ }
+ if (is_line_break_object(*item)) {
+ return false;
+ }
+ TextTagAttributes *attrs = attributes_for_object(child);
+ if (attrs && attrs->anyAttributesSet()) {
+ return false;
+ }
+ if (!objects_have_equal_style((*item)->parent, child)) {
+ return false;
+ }
+
+ Inkscape::XML::Node *insert_after_repr = nullptr;
+ if (!prepend) {
+ insert_after_repr = (*item)->getRepr();
+ } else if ((*item)->getPrev()) {
+ insert_after_repr = (*item)->getPrev()->getRepr();
+ }
+ while (child->getRepr()->childCount()) {
+ Inkscape::XML::Node *move_repr = child->getRepr()->firstChild();
+ Inkscape::GC::anchor(move_repr);
+ child->getRepr()->removeChild(move_repr);
+ (*item)->parent->getRepr()->addChild(move_repr, insert_after_repr);
+ Inkscape::GC::release(move_repr);
+ insert_after_repr = move_repr; // I think this will stay valid long enough. It's garbage collected these days.
+ }
+ child->deleteObject();
+ return true;
+}
+
+/** redundant double nesting: <font b><font a><font b>abc</font>def</font>ghi</font>
+ -> <font b>abc<font a>def</font>ghi</font>
+this function does its work when the parameter is the <font a> tag in the
+example. You may note that this only does its work when the doubly-nested
+child is the first or last. The other cases are called 'style inversion'
+below, and I'm not yet convinced that the result of that operation will be
+tidier in all cases. */
+static bool tidy_operator_redundant_double_nesting(SPObject **item, bool /*has_text_decoration*/)
+{
+ if (!(*item)->hasChildren()) return false;
+ if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is excessive nesting, done above
+ if (redundant_double_nesting_processor(item, (*item)->firstChild(), true))
+ return true;
+ if (redundant_double_nesting_processor(item, (*item)->lastChild(), false))
+ return true;
+ return false;
+}
+
+/** helper for tidy_operator_redundant_semi_nesting(). Checks a few things,
+then compares the styles for item+child versus just child. If they're equal,
+tidying is possible. */
+static bool redundant_semi_nesting_processor(SPObject **item, SPObject *child, bool prepend)
+{
+ if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child))
+ return false;
+ if (SP_IS_STRING(child)) return false;
+ if (is_line_break_object(child)) return false;
+ if (is_line_break_object(*item)) return false;
+ TextTagAttributes *attrs = attributes_for_object(child);
+ if (attrs && attrs->anyAttributesSet()) return false;
+ attrs = attributes_for_object(*item);
+ if (attrs && attrs->anyAttributesSet()) return false;
+
+ SPCSSAttr *css_child_and_item = sp_repr_css_attr_new();
+ SPCSSAttr *css_child_only = sp_repr_css_attr_new();
+ gchar const *item_style = (*item)->getRepr()->attribute("style");
+ if (item_style && *item_style) {
+ sp_repr_css_attr_add_from_string(css_child_and_item, item_style);
+ }
+ gchar const *child_style = child->getRepr()->attribute("style");
+ if (child_style && *child_style) {
+ sp_repr_css_attr_add_from_string(css_child_and_item, child_style);
+ sp_repr_css_attr_add_from_string(css_child_only, child_style);
+ }
+ bool equal = css_attrs_are_equal(css_child_only, css_child_and_item);
+ sp_repr_css_attr_unref(css_child_and_item);
+ sp_repr_css_attr_unref(css_child_only);
+ if (!equal) return false;
+
+ Inkscape::XML::Document *xml_doc = (*item)->getRepr()->document();
+ Inkscape::XML::Node *new_span = xml_doc->createElement((*item)->getRepr()->name());
+ if (prepend) {
+ SPObject *prev = (*item)->getPrev();
+ (*item)->parent->getRepr()->addChild(new_span, prev ? prev->getRepr() : nullptr);
+ } else {
+ (*item)->parent->getRepr()->addChild(new_span, (*item)->getRepr());
+ }
+ new_span->setAttribute("style", child->getRepr()->attribute("style"));
+ move_child_nodes(child->getRepr(), new_span);
+ Inkscape::GC::release(new_span);
+ child->deleteObject();
+ return true;
+}
+
+/** redundant semi-nesting: <font a><font b>abc</font>def</font>
+ -> <font b>abc</font><font>def</font>
+test this by applying a colour to a region, then a different colour to
+a partially-overlapping region. */
+static bool tidy_operator_redundant_semi_nesting(SPObject **item, bool /*has_text_decoration*/)
+{
+ if (!(*item)->hasChildren()) return false;
+ if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is redundant nesting, done above
+ if (redundant_semi_nesting_processor(item, (*item)->firstChild(), true))
+ return true;
+ if (redundant_semi_nesting_processor(item, (*item)->lastChild(), false))
+ return true;
+ return false;
+}
+
+
+/* tidy_operator_styled_whitespace commented out: not only did it have bugs,
+ * but it did *not* preserve the rendering: spaces in different font sizes,
+ * for instance, have different width, so moving them out of tspans changes
+ * the document. cf https://bugs.launchpad.net/inkscape/+bug/1477723
+*/
+
+#if 0
+/** helper for tidy_operator_styled_whitespace(), finds the last string object
+in a paragraph which is not \a not_obj. */
+static SPString* find_last_string_child_not_equal_to(SPObject *root, SPObject *not_obj)
+{
+ for (SPObject *child = root->lastChild() ; child ; child = child->getPrev())
+ {
+ if (child == not_obj) continue;
+ if (child->hasChildren()) {
+ SPString *ret = find_last_string_child_not_equal_to(child, not_obj);
+ if (ret) return ret;
+ } else if (SP_IS_STRING(child))
+ return SP_STRING(child);
+ }
+ return NULL;
+}
+
+/** whitespace-only spans:
+ abc<font> </font>def
+ -> abc<font></font> def
+ abc<b><i>def</i> </b>ghi
+ -> abc<b><i>def</i></b> ghi
+
+ visible text-decoration changes on white spaces should not be subject
+ to this sort of processing. So
+ abc<text-decoration-color> </text-decoration-color>def
+ is unchanged.
+*/
+static bool tidy_operator_styled_whitespace(SPObject **item, bool has_text_decoration)
+{
+ // any type of visible text decoration is OK as pure spaces, so do nothing here in that case.
+ if (!SP_IS_STRING(*item) || has_text_decoration ) {
+ return false;
+ }
+ Glib::ustring const &str = SP_STRING(*item)->string;
+ for (Glib::ustring::const_iterator it = str.begin() ; it != str.end() ; ++it) {
+ if (!g_unichar_isspace(*it)) {
+ return false;
+ }
+ }
+
+
+ SPObject *test_item = *item;
+ SPString *next_string;
+ for ( ; ; ) { // find the next string
+ next_string = sp_te_seek_next_string_recursive(test_item->getNext());
+ if (next_string) {
+ next_string->string.insert(0, str);
+ break;
+ }
+ for ( ; ; ) { // go up one item in the xml
+ test_item = test_item->parent;
+ if (is_line_break_object(test_item)) {
+ break;
+ }
+ if (SP_IS_FLOWTEXT(test_item)) {
+ return false;
+ }
+ SPObject *next = test_item->getNext();
+ if (next) {
+ test_item = next;
+ break;
+ }
+ }
+ if (is_line_break_object(test_item)) { // no next string, see if there's a prev string
+ next_string = find_last_string_child_not_equal_to(test_item, *item);
+ if (next_string == NULL) {
+ return false; // an empty paragraph
+ }
+ next_string->string = str + next_string->string;
+ break;
+ }
+ }
+ next_string->getRepr()->setContent(next_string->string.c_str());
+ SPObject *delete_obj = *item;
+ *item = (*item)->getNext();
+ delete_obj->deleteObject();
+ return true;
+}
+#endif
+
+/* possible tidy operators that are not yet implemented, either because
+they are difficult, occur infrequently, or because I'm not sure that the
+output is tidier in all cases:
+ duplicate styles in line break elements: <div italic><para italic>abc</para></div>
+ -> <div italic><para>abc</para></div>
+ style inversion: <font a>abc<font b>def<font a>ghi</font>jkl</font>mno</font>
+ -> <font a>abc<font b>def</font>ghi<font b>jkl</font>mno</font>
+ mistaken precedence: <font a,size 1>abc</font><size 1>def</size>
+ -> <size 1><font a>abc</font>def</size>
+*/
+
+/** Recursively walks the xml tree calling a set of cleanup operations on
+every child. Returns true if any changes were made to the tree.
+
+All the tidy operators return true if they made changes, and alter their
+parameter to point to the next object that should be processed, or NULL.
+They must not significantly alter (ie delete) any ancestor elements of the
+one they are passed.
+
+It may be that some of the later tidy operators that I wrote are actually
+general cases of the earlier operators, and hence the special-case-only
+versions can be removed. I haven't analysed my work in detail to figure
+out if this is so. */
+static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration)
+{
+ gchar const *root_style = (root)->getRepr()->attribute("style");
+ if(root_style && strstr(root_style,"text-decoration"))has_text_decoration = true;
+ static bool (* const tidy_operators[])(SPObject**, bool) = {
+ tidy_operator_empty_spans,
+ tidy_operator_inexplicable_spans,
+ tidy_operator_repeated_spans,
+ tidy_operator_excessive_nesting,
+ tidy_operator_redundant_double_nesting,
+ tidy_operator_redundant_semi_nesting
+ };
+ bool changes = false;
+
+ for (SPObject *child = root->firstChild() ; child != nullptr ; ) {
+ if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child) || SP_IS_TREF(child)) {
+ child = child->getNext();
+ continue;
+ }
+ if (child->hasChildren()) {
+ changes |= tidy_xml_tree_recursively(child, has_text_decoration);
+ }
+
+ unsigned i;
+ for (i = 0 ; i < sizeof(tidy_operators) / sizeof(tidy_operators[0]) ; i++) {
+ if (tidy_operators[i](&child, has_text_decoration)) {
+ changes = true;
+ break;
+ }
+ }
+ if (i == sizeof(tidy_operators) / sizeof(tidy_operators[0])) {
+ child = child->getNext();
+ }
+ }
+ return changes;
+}
+
+/** Applies the given CSS fragment to the characters of the given text or
+flowtext object between \a start and \a end, creating or removing span
+elements as necessary and optimal. */
+void sp_te_apply_style(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPCSSAttr const *css)
+{
+ // in the comments in the code below, capital letters are inside the application region, lowercase are outside
+ if (start == end) return;
+ Inkscape::Text::Layout::iterator first, last;
+ if (start < end) {
+ first = start;
+ last = end;
+ } else {
+ first = end;
+ last = start;
+ }
+ Inkscape::Text::Layout const *layout = te_get_layout(text);
+ SPObject *start_item = nullptr, *end_item = nullptr;
+ Glib::ustring::iterator start_text_iter, end_text_iter;
+ layout->getSourceOfCharacter(first, &start_item, &start_text_iter);
+ layout->getSourceOfCharacter(last, &end_item, &end_text_iter);
+ if (start_item == nullptr) {
+ return; // start is at end of text
+ }
+ if (is_line_break_object(start_item)) {
+ start_item = start_item->getNext();
+ }
+ if (is_line_break_object(end_item)) {
+ end_item = end_item->getNext();
+ }
+ if (end_item == nullptr) {
+ end_item = text;
+ }
+
+
+ /* Special case: With a tref, we only want to change its style when the whole
+ * string is selected, in which case the style can be applied directly to the
+ * tref node. If only part of the tref's string child is selected, just return. */
+
+ if (!sp_tref_fully_contained(start_item, start_text_iter, end_item, end_text_iter)) {
+
+ return;
+ }
+
+ /* stage 1: applying the style. Go up to the closest common ancestor of
+ start and end and then semi-recursively apply the style to all the
+ objects in between. The semi-recursion is because it's only necessary
+ at the beginning and end; the style can just be applied to the root
+ child in the middle.
+ eg: <span>abcDEF</span><span>GHI</span><span>JKLmno</span>
+ The recursion may involve creating new spans.
+ */
+ SPObject *common_ancestor = get_common_ancestor(text, start_item, end_item);
+
+ // bug #168370 (consider parent transform and viewBox)
+ // snipplet copied from desktop-style.cpp sp_desktop_apply_css_recursive(...)
+ SPCSSAttr *css_set = sp_repr_css_attr_new();
+ sp_repr_css_merge(css_set, const_cast<SPCSSAttr*>(css));
+ {
+ Geom::Affine const local(SP_ITEM(common_ancestor)->i2doc_affine());
+ double const ex(local.descrim());
+ if ( ( ex != 0. )
+ && ( ex != 1. ) ) {
+ sp_css_attr_scale(css_set, 1/ex);
+ }
+ }
+
+ start_item = ascend_while_first(start_item, start_text_iter, common_ancestor);
+ end_item = ascend_while_first(end_item, end_text_iter, common_ancestor);
+ recursively_apply_style(common_ancestor, css_set, start_item, start_text_iter, end_item, end_text_iter, span_name_for_text_object(text));
+ sp_repr_css_attr_unref(css_set);
+
+ /* stage 2: cleanup the xml tree (of which there are multiple passes) */
+ /* discussion: this stage requires a certain level of inventiveness because
+ it's not clear what the best representation is in many cases. An ideal
+ implementation would provide some sort of scoring function to rate the
+ ugliness of a given xml tree and try to reduce said function, but providing
+ the various possibilities to be rated is non-trivial. Instead, I have opted
+ for a multi-pass technique which simply recognises known-ugly patterns and
+ has matching routines for optimising the patterns it finds. It's reasonably
+ easy to add new pattern matching processors. If everything gets disastrous
+ and neither option can be made to work, a fallback could be to reduce
+ everything to a single level of nesting and drop all pretense of
+ roundtrippability. */
+ bool has_text_decoration = false;
+ gchar const *root_style = (text)->getRepr()->attribute("style");
+ if(root_style && strstr(root_style,"text-decoration")) has_text_decoration = true;
+ while (tidy_xml_tree_recursively(common_ancestor, has_text_decoration)){};
+
+ // if we only modified subobjects this won't have been automatically sent
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+}
+
+bool is_part_of_text_subtree (SPObject const *obj)
+{
+ return (SP_IS_TSPAN(obj)
+ || SP_IS_TEXT(obj)
+ || SP_IS_FLOWTEXT(obj)
+ || SP_IS_FLOWTSPAN(obj)
+ || SP_IS_FLOWDIV(obj)
+ || SP_IS_FLOWPARA(obj)
+ || SP_IS_FLOWLINE(obj)
+ || SP_IS_FLOWREGIONBREAK(obj));
+}
+
+bool is_top_level_text_object (SPObject const *obj)
+{
+ return (SP_IS_TEXT(obj)
+ || SP_IS_FLOWTEXT(obj));
+}
+
+bool has_visible_text(SPObject const *obj)
+{
+ bool hasVisible = false;
+
+ if (SP_IS_STRING(obj) && !SP_STRING(obj)->string.empty()) {
+ hasVisible = true; // maybe we should also check that it's not all whitespace?
+ } else if (is_part_of_text_subtree(obj)) {
+ for (auto& child: obj->children) {
+ if (has_visible_text(&child)) {
+ hasVisible = true;
+ break;
+ }
+ }
+ }
+
+ return hasVisible;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :