diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/text-editing.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.tar.xz inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/text-editing.cpp')
-rw-r--r-- | src/text-editing.cpp | 2186 |
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 : |