1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3  * Parent class for text and flowtext
4  *
5  * Authors:
6  *   bulia byak
7  *   Richard Hughes
8  *   Jon A. Cruz <jon@joncruz.org>
9  *   Abhishek Sharma
10  *
11  * Copyright (C) 2004-5 authors
12  *
13  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14  */
15 
16 #ifdef HAVE_CONFIG_H
17 #endif
18 
19 #include <cstring>
20 #include <string>
21 #include <glibmm/i18n.h>
22 
23 #include "desktop.h"
24 #include "document.h"
25 #include "inkscape.h"
26 #include "message-stack.h"
27 #include "text-editing.h"
28 
29 #include "object/sp-textpath.h"
30 #include "object/sp-flowtext.h"
31 #include "object/sp-flowdiv.h"
32 #include "object/sp-flowregion.h"
33 #include "object/sp-item-group.h"
34 #include "object/sp-tref.h"
35 #include "object/sp-tspan.h"
36 #include "style.h"
37 
38 #include "util/units.h"
39 
40 #include "xml/attribute-record.h"
41 #include "xml/sp-css-attr.h"
42 
43 static const gchar *tref_edit_message = _("You cannot edit <b>cloned character data</b>.");
44 static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend = false);
45 
46 static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration);
47 
te_get_layout(SPItem const * item)48 Inkscape::Text::Layout const * te_get_layout (SPItem const *item)
49 {
50     if (SP_IS_TEXT(item)) {
51         return &(SP_TEXT(item)->layout);
52     } else if (SP_IS_FLOWTEXT (item)) {
53         return &(SP_FLOWTEXT(item)->layout);
54     }
55     return nullptr;
56 }
57 
te_update_layout_now(SPItem * item)58 static void te_update_layout_now (SPItem *item)
59 {
60     if (SP_IS_TEXT(item))
61         SP_TEXT(item)->rebuildLayout();
62     else if (SP_IS_FLOWTEXT (item))
63         SP_FLOWTEXT(item)->rebuildLayout();
64     item->updateRepr();
65 }
66 
te_update_layout_now_recursive(SPItem * item)67 void te_update_layout_now_recursive(SPItem *item)
68 {
69     if (SP_IS_GROUP(item)) {
70     	std::vector<SPItem*> item_list = sp_item_group_item_list(SP_GROUP(item));
71         for(auto list_item : item_list){
72             te_update_layout_now_recursive(list_item);
73         }
74     } else if (SP_IS_TEXT(item))
75         SP_TEXT(item)->rebuildLayout();
76     else if (SP_IS_FLOWTEXT (item))
77         SP_FLOWTEXT(item)->rebuildLayout();
78     item->updateRepr();
79 }
80 
sp_te_output_is_empty(SPItem const * item)81 bool sp_te_output_is_empty(SPItem const *item)
82 {
83     Inkscape::Text::Layout const *layout = te_get_layout(item);
84     return layout->begin() == layout->end();
85 }
86 
sp_te_input_is_empty(SPObject const * item)87 bool sp_te_input_is_empty(SPObject const *item)
88 {
89     bool empty = true;
90     if (SP_IS_STRING(item)) {
91         empty = SP_STRING(item)->string.empty();
92     } else {
93         for (auto& child: item->children) {
94             if (!sp_te_input_is_empty(&child)) {
95                 empty = false;
96                 break;
97             }
98         }
99     }
100     return empty;
101 }
102 
103 Inkscape::Text::Layout::iterator
sp_te_get_position_by_coords(SPItem const * item,Geom::Point const & i_p)104 sp_te_get_position_by_coords (SPItem const *item, Geom::Point const &i_p)
105 {
106     Geom::Affine im (item->i2dt_affine ());
107     im = im.inverse();
108 
109     Geom::Point p = i_p * im;
110     Inkscape::Text::Layout const *layout = te_get_layout(item);
111     return layout->getNearestCursorPositionTo(p);
112 }
113 
sp_te_create_selection_quads(SPItem const * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,Geom::Affine const & transform)114 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)
115 {
116     if (start == end)
117         return std::vector<Geom::Point>();
118     Inkscape::Text::Layout const *layout = te_get_layout(item);
119     if (layout == nullptr)
120         return std::vector<Geom::Point>();
121 
122     return layout->createSelectionShape(start, end, transform);
123 }
124 
125 void
sp_te_get_cursor_coords(SPItem const * item,Inkscape::Text::Layout::iterator const & position,Geom::Point & p0,Geom::Point & p1)126 sp_te_get_cursor_coords (SPItem const *item, Inkscape::Text::Layout::iterator const &position, Geom::Point &p0, Geom::Point &p1)
127 {
128     Inkscape::Text::Layout const *layout = te_get_layout(item);
129     double height, rotation;
130     layout->queryCursorShape(position, p0, height, rotation);
131     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
132 }
133 
sp_te_style_at_position(SPItem const * text,Inkscape::Text::Layout::iterator const & position)134 SPStyle const * sp_te_style_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position)
135 {
136     SPObject const *pos_obj = sp_te_object_at_position(text, position);
137     SPStyle *result = (pos_obj) ? pos_obj->style : nullptr;
138     return result;
139 }
140 
sp_te_object_at_position(SPItem const * text,Inkscape::Text::Layout::iterator const & position)141 SPObject const * sp_te_object_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position)
142 {
143     Inkscape::Text::Layout const *layout = te_get_layout(text);
144     if (layout == nullptr) {
145         return nullptr;
146     }
147     SPObject *rawptr = nullptr;
148     layout->getSourceOfCharacter(position, &rawptr);
149     SPObject const *pos_obj = rawptr;
150     if (pos_obj == nullptr) {
151         pos_obj = text;
152     }
153     while (pos_obj->style == nullptr) {
154         pos_obj = pos_obj->parent;   // not interested in SPStrings
155     }
156     return pos_obj;
157 }
158 
159 /*
160  * for debugging input
161  *
162 char * dump_hexy(const gchar * utf8)
163 {
164     static char buffer[1024];
165 
166     buffer[0]='\0';
167     for (const char *ptr=utf8; *ptr; ptr++) {
168         sprintf(buffer+strlen(buffer),"x%02X",(unsigned char)*ptr);
169     }
170     return buffer;
171 }
172 */
173 
sp_te_replace(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,gchar const * utf8)174 Inkscape::Text::Layout::iterator sp_te_replace(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, gchar const *utf8)
175 {
176     iterator_pair pair;
177     sp_te_delete(item, start, end, pair);
178     return sp_te_insert(item, pair.first, utf8);
179 }
180 
181 
182 /* ***************************************************************************************************/
183 //                             I N S E R T I N G   T E X T
184 
is_line_break_object(SPObject const * object)185 static bool is_line_break_object(SPObject const *object)
186 {
187     bool is_line_break = false;
188 
189     if (object) {
190         if (SP_IS_TEXT(object)
191                 || (SP_IS_TSPAN(object) && SP_TSPAN(object)->role != SP_TSPAN_ROLE_UNSPECIFIED)
192                 || SP_IS_TEXTPATH(object)
193                 || SP_IS_FLOWDIV(object)
194                 || SP_IS_FLOWPARA(object)
195                 || SP_IS_FLOWLINE(object)
196                 || SP_IS_FLOWREGIONBREAK(object)) {
197 
198             is_line_break = true;
199         }
200     }
201 
202     return is_line_break;
203 }
204 
205 /** returns the attributes for an object, or NULL if it isn't a text,
206 tspan, tref, or textpath. */
attributes_for_object(SPObject * object)207 static TextTagAttributes* attributes_for_object(SPObject *object)
208 {
209     if (SP_IS_TSPAN(object))
210         return &SP_TSPAN(object)->attributes;
211     if (SP_IS_TEXT(object))
212         return &SP_TEXT(object)->attributes;
213     if (SP_IS_TREF(object))
214         return &SP_TREF(object)->attributes;
215     if (SP_IS_TEXTPATH(object))
216         return &SP_TEXTPATH(object)->attributes;
217     return nullptr;
218 }
219 
span_name_for_text_object(SPObject const * object)220 static const char * span_name_for_text_object(SPObject const *object)
221 {
222     if (SP_IS_TEXT(object)) return "svg:tspan";
223     else if (SP_IS_FLOWTEXT(object)) return "svg:flowSpan";
224     return nullptr;
225 }
226 
sp_text_get_length(SPObject const * item)227 unsigned sp_text_get_length(SPObject const *item)
228 {
229     unsigned length = 0;
230 
231     if (SP_IS_STRING(item)) {
232         length = SP_STRING(item)->string.length();
233     } else {
234         if (is_line_break_object(item)) {
235             length++;
236         }
237 
238         for (auto& child: item->children) {
239             if (SP_IS_STRING(&child)) {
240                 length += SP_STRING(&child)->string.length();
241             } else {
242                 length += sp_text_get_length(&child);
243             }
244         }
245     }
246 
247     return length;
248 }
249 
sp_text_get_length_upto(SPObject const * item,SPObject const * upto)250 unsigned sp_text_get_length_upto(SPObject const *item, SPObject const *upto)
251 {
252     unsigned length = 0;
253 
254     // The string is the lowest level and the length can be counted directly.
255     if (SP_IS_STRING(item)) {
256         return SP_STRING(item)->string.length();
257     }
258 
259     // Take care of new lines...
260     if (is_line_break_object(item) && !SP_IS_TEXT(item)) {
261         if (item != item->parent->firstChild()) {
262             // add 1 for each newline
263             length++;
264         }
265     }
266 
267     // Count the length of the children
268     for (auto& child: item->children) {
269         if (upto && &child == upto) {
270             // hit upto, return immediately
271             return length;
272         }
273         if (SP_IS_STRING(&child)) {
274             length += SP_STRING(&child)->string.length();
275         }
276         else {
277             if (upto && child.isAncestorOf(upto)) {
278                 // upto is below us, recurse and break loop
279                 length += sp_text_get_length_upto(&child, upto);
280                 return length;
281             } else {
282                 // recurse and go to the next sibling
283                 length += sp_text_get_length_upto(&child, upto);
284             }
285         }
286     }
287     return length;
288 }
289 
duplicate_node_without_children(Inkscape::XML::Document * xml_doc,Inkscape::XML::Node const * old_node)290 static Inkscape::XML::Node* duplicate_node_without_children(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node const *old_node)
291 {
292     switch (old_node->type()) {
293         case Inkscape::XML::NodeType::ELEMENT_NODE: {
294             Inkscape::XML::Node *new_node = xml_doc->createElement(old_node->name());
295             GQuark const id_key = g_quark_from_string("id");
296             for ( const auto & attr: old_node->attributeList() ) {
297                 if (attr.key == id_key) continue;
298                 new_node->setAttribute(g_quark_to_string(attr.key), attr.value);
299             }
300             return new_node;
301         }
302 
303         case Inkscape::XML::NodeType::TEXT_NODE:
304             return xml_doc->createTextNode(old_node->content());
305 
306         case Inkscape::XML::NodeType::COMMENT_NODE:
307             return xml_doc->createComment(old_node->content());
308 
309         case Inkscape::XML::NodeType::PI_NODE:
310             return xml_doc->createPI(old_node->name(), old_node->content());
311 
312         case Inkscape::XML::NodeType::DOCUMENT_NODE:
313             return nullptr;   // this had better never happen
314     }
315     return nullptr;
316 }
317 
318 /** returns the sum of the (recursive) lengths of all the SPStrings prior
319 to \a item at the same level. */
sum_sibling_text_lengths_before(SPObject const * item)320 static unsigned sum_sibling_text_lengths_before(SPObject const *item)
321 {
322     unsigned char_index = 0;
323     for (auto& sibling: item->parent->children) {
324         if (&sibling == item) {
325             break;
326         }
327         char_index += sp_text_get_length(&sibling);
328     }
329     return char_index;
330 }
331 
332 /** splits the attributes for the first object at the given \a char_index
333 and moves the ones after that point into \a second_item. */
split_attributes(SPObject * first_item,SPObject * second_item,unsigned char_index)334 static void split_attributes(SPObject *first_item, SPObject *second_item, unsigned char_index)
335 {
336     TextTagAttributes *first_attrs = attributes_for_object(first_item);
337     TextTagAttributes *second_attrs = attributes_for_object(second_item);
338     if (first_attrs && second_attrs)
339         first_attrs->split(char_index, second_attrs);
340 }
341 
342 /** recursively divides the XML node tree into two objects: the original will
343 contain all objects up to and including \a split_obj and the returned value
344 will be the new leaf which represents the copy of \a split_obj and extends
345 down the tree with new elements all the way to the common root which is the
346 parent of the first line break node encountered.
347 */
split_text_object_tree_at(SPObject * split_obj,unsigned char_index)348 static SPObject* split_text_object_tree_at(SPObject *split_obj, unsigned char_index)
349 {
350     Inkscape::XML::Document *xml_doc = split_obj->document->getReprDoc();
351     if (is_line_break_object(split_obj)) {
352         Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
353         split_obj->parent->getRepr()->addChild(new_node, split_obj->getRepr());
354         Inkscape::GC::release(new_node);
355         split_attributes(split_obj, split_obj->getNext(), char_index);
356         return split_obj->getNext();
357     } else  if (!SP_IS_TSPAN(split_obj) &&
358                 !SP_IS_FLOWTSPAN(split_obj) &&
359                 !SP_IS_STRING(split_obj)) {
360         std::cerr << "split_text_object_tree_at: Illegal split object type! (Illegal document structure.)" << std::endl;
361         return nullptr;
362     }
363 
364     unsigned char_count_before = sum_sibling_text_lengths_before(split_obj);
365     SPObject *duplicate_obj = split_text_object_tree_at(split_obj->parent, char_index + char_count_before);
366 
367     if (duplicate_obj == nullptr) {
368         // Illegal document structure (no line break object).
369         return nullptr;
370     }
371 
372     // copy the split node
373     Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
374     duplicate_obj->getRepr()->appendChild(new_node);
375     Inkscape::GC::release(new_node);
376 
377     // sort out the copied attributes (x/y/dx/dy/rotate)
378     split_attributes(split_obj, duplicate_obj->firstChild(), char_index);
379 
380     // then move all the subsequent nodes
381     split_obj = split_obj->getNext();
382     while (split_obj) {
383         Inkscape::XML::Node *move_repr = split_obj->getRepr();
384         SPObject *next_obj = split_obj->getNext();  // this is about to become invalidated by removeChild()
385         Inkscape::GC::anchor(move_repr);
386         split_obj->parent->getRepr()->removeChild(move_repr);
387         duplicate_obj->getRepr()->appendChild(move_repr);
388         Inkscape::GC::release(move_repr);
389 
390         split_obj = next_obj;
391     }
392     return duplicate_obj->firstChild();
393 }
394 
395 /** inserts a new line break at the given position in a text or flowtext
396 object. If the position is in the middle of a span, the XML tree must be
397 chopped in two such that the line can be created at the root of the text
398 element. Returns an iterator pointing just after the inserted break. */
sp_te_insert_line(SPItem * item,Inkscape::Text::Layout::iterator & position)399 Inkscape::Text::Layout::iterator sp_te_insert_line (SPItem *item, Inkscape::Text::Layout::iterator &position)
400 {
401     // Disable newlines in a textpath; TODO: maybe on Enter in a textpath, separate it into two
402     // texpaths attached to the same path, with a vertical shift
403     if (SP_IS_TEXT_TEXTPATH (item) || SP_IS_TREF(item))
404         return position;
405 
406     Inkscape::Text::Layout const *layout = te_get_layout(item);
407 
408     // If this is plain SVG 1.1 text object without a tspan with sodipodi:role="line", we need
409     // to wrap it or our custom line breaking code won't work!
410     bool need_to_wrap = false;
411     SPText* text_object = dynamic_cast<SPText *>(item);
412     if (text_object && !text_object->has_shape_inside() && !text_object->has_inline_size()) {
413 
414         need_to_wrap = true;
415         for (auto child : item->childList(false)) {
416             auto tspan = dynamic_cast<SPTSpan *>(child);
417             if (tspan && tspan->role == SP_TSPAN_ROLE_LINE) {
418                 // Already wrapped
419                 need_to_wrap = false;
420                 break;
421             }
422         }
423 
424         if (need_to_wrap) {
425 
426             // We'll need to rebuild layout, so store character postion:
427             int char_index = layout->iteratorToCharIndex(position);
428 
429             // Create wrapping tspan.
430             Inkscape::XML::Node *text_repr = text_object->getRepr();
431             Inkscape::XML::Document *xml_doc = text_repr->document();
432             Inkscape::XML::Node *new_tspan_repr = xml_doc->createElement("svg:tspan");
433             new_tspan_repr->setAttribute("sodipodi:role", "line");
434 
435             // Move text content to tspan and add tspan to text object.
436             // To do: This moves <desc> and <title> too.
437             move_child_nodes(text_repr, new_tspan_repr);
438             text_repr->appendChild(new_tspan_repr);
439 
440             // Need to find new iterator.
441             text_object->rebuildLayout();
442             position = layout->charIndexToIterator(char_index);
443         }
444     }
445 
446     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
447 
448     SPObject *split_obj = nullptr;
449     Glib::ustring::iterator split_text_iter;
450     if (position != layout->end()) {
451         layout->getSourceOfCharacter(position, &split_obj, &split_text_iter);
452     }
453 
454     if (split_obj == nullptr || is_line_break_object(split_obj)) {
455 
456         if (split_obj == nullptr) split_obj = item->lastChild();
457 
458         if (SP_IS_TREF(split_obj)) {
459             desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
460             return position;
461         }
462 
463         if (split_obj) {
464             Inkscape::XML::Document *xml_doc = split_obj->document->getReprDoc();
465             Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr());
466             // if we finaly go to a text element without TSpan we mist set content to none
467             // new_node->setContent("");
468             split_obj->parent->getRepr()->addChild(new_node, split_obj->getRepr());
469             Inkscape::GC::release(new_node);
470         }
471 
472     } else if (SP_IS_STRING(split_obj)) {
473 
474         // If the parent is a tref, editing on this particular string is disallowed.
475         if (SP_IS_TREF(split_obj->parent)) {
476             desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
477             return position;
478         }
479 
480         Glib::ustring *string = &SP_STRING(split_obj)->string;
481         unsigned char_index = 0;
482         for (Glib::ustring::iterator it = string->begin() ; it != split_text_iter ; ++it)
483             char_index++;
484         // we need to split the entire text tree into two
485 
486         SPObject *object = split_text_object_tree_at(split_obj, char_index);
487         if (object == nullptr) {
488             // Illegal document structure
489             return position;
490         }
491 
492         SPString *new_string = SP_STRING(object);
493         new_string->getRepr()->setContent(&*split_text_iter.base());   // a little ugly
494         string->erase(split_text_iter, string->end());
495         split_obj->getRepr()->setContent(string->c_str());
496         // TODO: if the split point was at the beginning of a span we have a whole load of empty elements to clean up
497     } else {
498         // TODO
499         // I think the only case to put here is arbitrary gaps, which nobody uses yet
500     }
501 
502     unsigned char_index = layout->iteratorToCharIndex(position);
503     te_update_layout_now(item);
504     item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
505     return layout->charIndexToIterator(char_index + 1);
506 }
507 
508 /** finds the first SPString after the given position, including children, excluding parents */
sp_te_seek_next_string_recursive(SPObject * start_obj)509 static SPString* sp_te_seek_next_string_recursive(SPObject *start_obj)
510 {
511     while (start_obj) {
512         if (start_obj->hasChildren()) {
513             SPString *found_string = sp_te_seek_next_string_recursive(start_obj->firstChild());
514             if (found_string) {
515                 return found_string;
516             }
517         }
518         if (SP_IS_STRING(start_obj)) {
519             return SP_STRING(start_obj);
520         }
521         start_obj = start_obj->getNext();
522         if (is_line_break_object(start_obj)) {
523             break;   // don't cross line breaks
524         }
525     }
526     return nullptr;
527 }
528 
529 /** inserts the given characters into the given string and inserts
530 corresponding new x/y/dx/dy/rotate attributes into all its parents. */
insert_into_spstring(SPString * string_item,Glib::ustring::iterator iter_at,gchar const * utf8)531 static void insert_into_spstring(SPString *string_item, Glib::ustring::iterator iter_at, gchar const *utf8)
532 {
533     unsigned char_index = 0;
534     unsigned char_count = g_utf8_strlen(utf8, -1);
535     Glib::ustring *string = &SP_STRING(string_item)->string;
536 
537     for (Glib::ustring::iterator it = string->begin() ; it != iter_at ; ++it)
538         char_index++;
539     string->replace(iter_at, iter_at, utf8);
540 
541     SPObject *parent_item = string_item;
542     for ( ; ; ) {
543         char_index += sum_sibling_text_lengths_before(parent_item);
544         parent_item = parent_item->parent;
545         TextTagAttributes *attributes = attributes_for_object(parent_item);
546         if (!attributes) break;
547         attributes->insert(char_index, char_count);
548     }
549 }
550 
551 /** Inserts the given text into a text or flowroot object. Line breaks
552 cannot be inserted using this function, see sp_te_insert_line(). Returns
553 an iterator pointing just after the inserted text. */
554 Inkscape::Text::Layout::iterator
sp_te_insert(SPItem * item,Inkscape::Text::Layout::iterator const & position,gchar const * utf8)555 sp_te_insert(SPItem *item, Inkscape::Text::Layout::iterator const &position, gchar const *utf8)
556 {
557     if (!g_utf8_validate(utf8,-1,nullptr)) {
558         g_warning("Trying to insert invalid utf8");
559         return position;
560     }
561 
562     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
563 
564     Inkscape::Text::Layout const *layout = te_get_layout(item);
565     Glib::ustring::iterator iter_text;
566     // we want to insert after the previous char, not before the current char.
567     // it makes a difference at span boundaries
568     Inkscape::Text::Layout::iterator it_prev_char = position;
569     bool cursor_at_start = !it_prev_char.prevCharacter();
570     bool cursor_at_end = position == layout->end();
571     SPObject *source_obj = nullptr;
572     layout->getSourceOfCharacter(it_prev_char, &source_obj, &iter_text);
573     if (SP_IS_STRING(source_obj)) {
574         // If the parent is a tref, editing on this particular string is disallowed.
575         if (SP_IS_TREF(source_obj->parent)) {
576             desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
577             return position;
578         }
579 
580         // Now the simple case can begin...
581         if (!cursor_at_start){
582             ++iter_text;
583         }
584         SPString *string_item = SP_STRING(source_obj);
585         insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : iter_text, utf8);
586     } else {
587         // the not-so-simple case where we're at a line break or other control char; add to the next child/sibling SPString
588         Inkscape::XML::Document *xml_doc = item->getRepr()->document();
589         if (cursor_at_start) {
590             source_obj = item;
591             if (source_obj->hasChildren()) {
592                 source_obj = source_obj->firstChild();
593                 if (SP_IS_FLOWTEXT(item)) {
594                     while (SP_IS_FLOWREGION(source_obj) || SP_IS_FLOWREGIONEXCLUDE(source_obj)) {
595                         source_obj = source_obj->getNext();
596                     }
597                     if (source_obj == nullptr) {
598                         source_obj = item;
599                     }
600                 }
601             }
602             if (source_obj == item && SP_IS_FLOWTEXT(item)) {
603                 Inkscape::XML::Node *para = xml_doc->createElement("svg:flowPara");
604                 item->getRepr()->appendChild(para);
605                 source_obj = item->lastChild();
606             }
607         } else
608             source_obj = source_obj->getNext();
609 
610         if (source_obj) {  // never fails
611             SPString *string_item = sp_te_seek_next_string_recursive(source_obj);
612             if (string_item == nullptr) {
613                 // need to add an SPString in this (pathological) case
614                 Inkscape::XML::Node *rstring = xml_doc->createTextNode("");
615                 source_obj->getRepr()->addChild(rstring, nullptr);
616                 Inkscape::GC::release(rstring);
617                 g_assert(SP_IS_STRING(source_obj->firstChild()));
618                 string_item = SP_STRING(source_obj->firstChild());
619             }
620             // If the parent is a tref, editing on this particular string is disallowed.
621             if (SP_IS_TREF(string_item->parent)) {
622                 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
623                 return position;
624             }
625 
626             insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : string_item->string.begin(), utf8);
627         }
628     }
629 
630     unsigned char_index = layout->iteratorToCharIndex(position);
631     te_update_layout_now(item);
632     item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
633     return layout->charIndexToIterator(char_index + g_utf8_strlen(utf8, -1));
634 }
635 
636 
637 /* ***************************************************************************************************/
638 //                            D E L E T I N G   T E X T
639 
640 /** moves all the children of \a from_repr to \a to_repr, either before
641 the existing children or after them. Order is maintained. The empty
642 \a from_repr is not deleted. */
move_child_nodes(Inkscape::XML::Node * from_repr,Inkscape::XML::Node * to_repr,bool prepend)643 static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend)
644 {
645     while (from_repr->childCount()) {
646         Inkscape::XML::Node *child = prepend ? from_repr->lastChild() : from_repr->firstChild();
647         Inkscape::GC::anchor(child);
648         from_repr->removeChild(child);
649         if (prepend) to_repr->addChild(child, nullptr);
650         else to_repr->appendChild(child);
651         Inkscape::GC::release(child);
652     }
653 }
654 
655 /** returns the object in the tree which is the closest ancestor of both
656 \a one and \a two. It will never return anything higher than \a text. */
get_common_ancestor(SPObject * text,SPObject * one,SPObject * two)657 static SPObject* get_common_ancestor(SPObject *text, SPObject *one, SPObject *two)
658 {
659     if (one == nullptr || two == nullptr)
660         return text;
661     SPObject *common_ancestor = one;
662     if (SP_IS_STRING(common_ancestor))
663         common_ancestor = common_ancestor->parent;
664     while (!(common_ancestor == two || common_ancestor->isAncestorOf(two))) {
665         g_assert(common_ancestor != text);
666         common_ancestor = common_ancestor->parent;
667     }
668     return common_ancestor;
669 }
670 
671 /** positions \a para_obj and \a text_iter to be pointing at the end
672 of the last string in the last leaf object of \a para_obj. If the last
673 leaf is not an SPString then \a text_iter will be unchanged. */
move_to_end_of_paragraph(SPObject ** para_obj,Glib::ustring::iterator * text_iter)674 static void move_to_end_of_paragraph(SPObject **para_obj, Glib::ustring::iterator *text_iter)
675 {
676     while ((*para_obj)->hasChildren())
677         *para_obj = (*para_obj)->lastChild();
678     if (SP_IS_STRING(*para_obj))
679         *text_iter = SP_STRING(*para_obj)->string.end();
680 }
681 
682 /** delete the line break pointed to by \a item by merging its children into
683 the next suitable object and deleting \a item. Returns the object after the
684 ones that have just been moved and sets \a next_is_sibling accordingly. */
delete_line_break(SPObject * root,SPObject * item,bool * next_is_sibling)685 static SPObject* delete_line_break(SPObject *root, SPObject *item, bool *next_is_sibling)
686 {
687     Inkscape::XML::Node *this_repr = item->getRepr();
688     SPObject *next_item = nullptr;
689     unsigned moved_char_count = sp_text_get_length(item) - 1;   // the -1 is because it's going to count the line break
690 
691     /* some sample cases (the div is the item to be deleted, the * represents where to put the new span):
692       <div></div><p>*text</p>
693       <p><div></div>*text</p>
694       <p><div></div></p><p>*text</p>
695     */
696     Inkscape::XML::Document *xml_doc = item->getRepr()->document();
697     Inkscape::XML::Node *new_span_repr = xml_doc->createElement(span_name_for_text_object(root));
698 
699     new_span_repr->setAttributeOrRemoveIfEmpty("dx", this_repr->attribute("dx"));
700     new_span_repr->setAttributeOrRemoveIfEmpty("dy", this_repr->attribute("dy"));
701     new_span_repr->setAttributeOrRemoveIfEmpty("rotate", this_repr->attribute("rotate"));
702 
703     SPObject *following_item = item;
704     while (following_item->getNext() == nullptr) {
705         following_item = following_item->parent;
706         g_assert(following_item != root);
707     }
708     following_item = following_item->getNext();
709 
710     SPObject *new_parent_item;
711     if (SP_IS_STRING(following_item)) {
712         new_parent_item = following_item->parent;
713         new_parent_item->getRepr()->addChild(new_span_repr, following_item->getPrev() ? following_item->getPrev()->getRepr() : nullptr);
714         next_item = following_item;
715         *next_is_sibling = true;
716     } else {
717         new_parent_item = following_item;
718         next_item = new_parent_item->firstChild();
719         *next_is_sibling = true;
720         if (next_item == nullptr) {
721             next_item = new_parent_item;
722             *next_is_sibling = false;
723         }
724         new_parent_item->getRepr()->addChild(new_span_repr, nullptr);
725     }
726 
727     // work around a bug in sp_style_write_difference() which causes the difference
728     // not to be written if the second param has a style set which the first does not
729     // by causing the first param to have everything set
730     SPCSSAttr *dest_node_attrs = sp_repr_css_attr(new_parent_item->getRepr(), "style");
731     SPCSSAttr *this_node_attrs = sp_repr_css_attr(this_repr, "style");
732     SPCSSAttr *this_node_attrs_inherited = sp_repr_css_attr_inherited(this_repr, "style");
733     for ( const auto & attr :dest_node_attrs->attributeList()) {
734         gchar const *key = g_quark_to_string(attr.key);
735         gchar const *this_attr = this_node_attrs_inherited->attribute(key);
736         if ((this_attr == nullptr || strcmp(attr.value, this_attr)) && this_node_attrs->attribute(key) == nullptr)
737             this_node_attrs->setAttribute(key, this_attr);
738     }
739     sp_repr_css_attr_unref(this_node_attrs_inherited);
740     sp_repr_css_attr_unref(this_node_attrs);
741     sp_repr_css_attr_unref(dest_node_attrs);
742     sp_repr_css_change(new_span_repr, this_node_attrs, "style");
743 
744     TextTagAttributes *attributes = attributes_for_object(new_parent_item);
745     if (attributes)
746         attributes->insert(0, moved_char_count);
747     move_child_nodes(this_repr, new_span_repr);
748     this_repr->parent()->removeChild(this_repr);
749     return next_item;
750 }
751 
752 /** erases the given characters from the given string and deletes the
753 corresponding x/y/dx/dy/rotate attributes from all its parents. */
erase_from_spstring(SPString * string_item,Glib::ustring::iterator iter_from,Glib::ustring::iterator iter_to)754 static void erase_from_spstring(SPString *string_item, Glib::ustring::iterator iter_from, Glib::ustring::iterator iter_to)
755 {
756     unsigned char_index = 0;
757     unsigned char_count = 0;
758     Glib::ustring *string = &SP_STRING(string_item)->string;
759 
760     for (Glib::ustring::iterator it = string->begin() ; it != iter_from ; ++it){
761         char_index++;
762     }
763     for (Glib::ustring::iterator it = iter_from ; it != iter_to ; ++it){
764         char_count++;
765     }
766     string->erase(iter_from, iter_to);
767     string_item->getRepr()->setContent(string->c_str());
768 
769     SPObject *parent_item = string_item;
770     for ( ; ; ) {
771         char_index += sum_sibling_text_lengths_before(parent_item);
772         parent_item = parent_item->parent;
773         TextTagAttributes *attributes = attributes_for_object(parent_item);
774         if (attributes == nullptr) {
775             break;
776         }
777 
778         attributes->erase(char_index, char_count);
779         attributes->writeTo(parent_item->getRepr());
780     }
781 }
782 
783 /* Deletes the given characters from a text or flowroot object. This is
784 quite a complicated operation, partly due to the cleanup that is done if all
785 the text in a subobject has been deleted, and partly due to the difficulty
786 of figuring out what is a line break and how to delete one. Returns the
787 real start and ending iterators based on the situation. */
788 bool
sp_te_delete(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,iterator_pair & iter_pair)789 sp_te_delete (SPItem *item, Inkscape::Text::Layout::iterator const &start,
790               Inkscape::Text::Layout::iterator const &end, iterator_pair &iter_pair)
791 {
792     bool success = false;
793 
794     iter_pair.first = start;
795     iter_pair.second = end;
796 
797     if (start == end) return success;
798 
799     if (start > end) {
800         iter_pair.first = end;
801         iter_pair.second = start;
802     }
803 
804     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
805 
806     Inkscape::Text::Layout const *layout = te_get_layout(item);
807     SPObject *start_item = nullptr, *end_item = nullptr;
808     Glib::ustring::iterator start_text_iter, end_text_iter;
809     layout->getSourceOfCharacter(iter_pair.first, &start_item, &start_text_iter);
810     layout->getSourceOfCharacter(iter_pair.second, &end_item, &end_text_iter);
811     if (start_item == nullptr) {
812         return success;   // start is at end of text
813     }
814     if (is_line_break_object(start_item)) {
815         move_to_end_of_paragraph(&start_item, &start_text_iter);
816     }
817     if (end_item == nullptr) {
818         end_item = item->lastChild();
819         move_to_end_of_paragraph(&end_item, &end_text_iter);
820     } else if (is_line_break_object(end_item)) {
821         move_to_end_of_paragraph(&end_item, &end_text_iter);
822     }
823 
824     SPObject *common_ancestor = get_common_ancestor(item, start_item, end_item);
825 
826     bool has_text_decoration = false;
827     gchar const *root_style = (item)->getRepr()->attribute("style");
828     if(root_style && strstr(root_style,"text-decoration"))has_text_decoration = true;
829 
830     if (start_item == end_item) {
831         // the quick case where we're deleting stuff all from the same string
832         if (SP_IS_STRING(start_item)) {     // always true (if it_start != it_end anyway)
833             // If the parent is a tref, editing on this particular string is disallowed.
834             if (SP_IS_TREF(start_item->parent)) {
835                 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
836             } else {
837                 erase_from_spstring(SP_STRING(start_item), start_text_iter, end_text_iter);
838                 success = true;
839             }
840         }
841     } else {
842         SPObject *sub_item = start_item;
843         // walk the tree from start_item to end_item, deleting as we go
844         while (sub_item != item) {
845             if (sub_item == end_item) {
846                 if (SP_IS_STRING(sub_item)) {
847                     // If the parent is a tref, editing on this particular string is disallowed.
848                     if (SP_IS_TREF(sub_item->parent)) {
849                         desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message);
850                         break;
851                     }
852 
853                     Glib::ustring *string = &SP_STRING(sub_item)->string;
854                     erase_from_spstring(SP_STRING(sub_item), string->begin(), end_text_iter);
855                     success = true;
856                 }
857                 break;
858             }
859             if (SP_IS_STRING(sub_item)) {
860                 SPString *string = SP_STRING(sub_item);
861                 if (sub_item == start_item)
862                     erase_from_spstring(string, start_text_iter, string->string.end());
863                 else
864                     erase_from_spstring(string, string->string.begin(), string->string.end());
865                 success = true;
866             }
867             // walk to the next item in the tree
868             if (sub_item->hasChildren())
869                 sub_item = sub_item->firstChild();
870             else {
871                 SPObject *next_item;
872                 do {
873                     bool is_sibling = true;
874                     next_item = sub_item->getNext();
875                     if (next_item == nullptr) {
876                         next_item = sub_item->parent;
877                         is_sibling = false;
878                     }
879 
880                     if (is_line_break_object(sub_item))
881                         next_item = delete_line_break(item, sub_item, &is_sibling);
882 
883                     sub_item = next_item;
884                     if (is_sibling) break;
885                     // no more siblings, go up a parent
886                 } while (sub_item != item && sub_item != end_item);
887             }
888         }
889     }
890 
891     while (tidy_xml_tree_recursively(common_ancestor, has_text_decoration)){};
892     te_update_layout_now(item);
893     item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
894     layout->validateIterator(&iter_pair.first);
895     layout->validateIterator(&iter_pair.second);
896     return success;
897 }
898 
899 
900 /* ***************************************************************************************************/
901 //                            P L A I N   T E X T   F U N C T I O N S
902 
903 /** Gets a text-only representation of the given text or flowroot object,
904 replacing line break elements with '\n'. */
sp_te_get_ustring_multiline(SPObject const * root,Glib::ustring * string,bool * pending_line_break)905 static void sp_te_get_ustring_multiline(SPObject const *root, Glib::ustring *string, bool *pending_line_break)
906 {
907     if (*pending_line_break) {
908         *string += '\n';
909     }
910     for (auto& child: root->children) {
911         if (SP_IS_STRING(&child)) {
912             *string += SP_STRING(&child)->string;
913         } else {
914             sp_te_get_ustring_multiline(&child, string, pending_line_break);
915         }
916     }
917     if (!SP_IS_TEXT(root) && !SP_IS_TEXTPATH(root) && is_line_break_object(root)) {
918         *pending_line_break = true;
919     }
920 }
921 
922 /** Gets a text-only representation of the given text or flowroot object,
923 replacing line break elements with '\n'. The return value must be free()d. */
924 Glib::ustring
sp_te_get_string_multiline(SPItem const * text)925 sp_te_get_string_multiline (SPItem const *text)
926 {
927     Glib::ustring string;
928     bool pending_line_break = false;
929 
930     if (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)) {
931         sp_te_get_ustring_multiline(text, &string, &pending_line_break);
932     }
933     return string;
934 }
935 
936 /** Gets a text-only representation of the characters in a text or flowroot
937 object from \a start to \a end only. Line break elements are replaced with
938 '\n'. */
939 Glib::ustring
sp_te_get_string_multiline(SPItem const * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end)940 sp_te_get_string_multiline (SPItem const *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end)
941 {
942     if (start == end) return "";
943     Inkscape::Text::Layout::iterator first, last;
944     if (start < end) {
945         first = start;
946         last = end;
947     } else {
948         first = end;
949         last = start;
950     }
951     Inkscape::Text::Layout const *layout = te_get_layout(text);
952     Glib::ustring result;
953     // not a particularly fast piece of code. I'll optimise it if people start to notice.
954     for ( ; first < last ; first.nextCharacter()) {
955         SPObject *char_item = nullptr;
956         Glib::ustring::iterator text_iter;
957         layout->getSourceOfCharacter(first, &char_item, &text_iter);
958         if (SP_IS_STRING(char_item)) {
959             result += *text_iter;
960         } else {
961             result += '\n';
962         }
963     }
964     return result;
965 }
966 
967 void
sp_te_set_repr_text_multiline(SPItem * text,gchar const * str)968 sp_te_set_repr_text_multiline(SPItem *text, gchar const *str)
969 {
970     g_return_if_fail (text != nullptr);
971     g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
972 
973     Inkscape::XML::Document *xml_doc = text->getRepr()->document();
974     Inkscape::XML::Node *repr;
975     SPObject *object;
976     bool is_textpath = false;
977     if (SP_IS_TEXT_TEXTPATH (text)) {
978         repr = text->firstChild()->getRepr();
979         object = text->firstChild();
980         is_textpath = true;
981     } else {
982         repr = text->getRepr();
983         object = text;
984     }
985 
986     if (!str) {
987         str = "";
988     }
989     gchar *content = g_strdup (str);
990 
991     repr->setContent("");
992     for (auto& child: object->childList(false)) {
993         if (!SP_IS_FLOWREGION(child) && !SP_IS_FLOWREGIONEXCLUDE(child)) {
994             repr->removeChild(child->getRepr());
995         }
996     }
997 
998     if (is_textpath) {
999         gchar *p = content;
1000         while (*p != '\0') {
1001             if (*p == '\n') {
1002                 *p = ' '; // No lines for textpath, replace newlines with spaces.
1003             }
1004             ++p;
1005         }
1006         Inkscape::XML::Node *rstr = xml_doc->createTextNode(content);
1007         repr->addChild(rstr, nullptr);
1008         Inkscape::GC::release(rstr);
1009     } else {
1010         SPText* sptext = dynamic_cast<SPText *>(text);
1011         if (sptext && (sptext->has_inline_size() || sptext->has_shape_inside())) {
1012             // Do nothing... we respect newlines (and assume CSS already set to do so).
1013             Inkscape::XML::Node *rstr = xml_doc->createTextNode(content);
1014             repr->addChild(rstr, nullptr);
1015             Inkscape::GC::release(rstr);
1016         } else {
1017             // Break into tspans with sodipodi:role="line".
1018             gchar *p = content;
1019             while (p) {
1020                 gchar *e = strchr (p, '\n');
1021                 if (e) *e = '\0';
1022                 Inkscape::XML::Node *rtspan;
1023                 if (SP_IS_TEXT(text)) { // create a tspan for each line
1024                     rtspan = xml_doc->createElement("svg:tspan");
1025                     rtspan->setAttribute("sodipodi:role", "line");
1026                 } else { // create a flowPara for each line
1027                     rtspan = xml_doc->createElement("svg:flowPara");
1028                 }
1029                 Inkscape::XML::Node *rstr = xml_doc->createTextNode(p);
1030                 rtspan->addChild(rstr, nullptr);
1031                 Inkscape::GC::release(rstr);
1032                 repr->appendChild(rtspan);
1033                 Inkscape::GC::release(rtspan);
1034 
1035                 p = (e) ? e + 1 : nullptr;
1036             }
1037         }
1038     }
1039     g_free (content);
1040 }
1041 
1042 /* ***************************************************************************************************/
1043 //                           K E R N I N G   A N D   S P A C I N G
1044 
1045 /** Returns the attributes block and the character index within that block
1046 which represents the iterator \a position. */
1047 TextTagAttributes*
text_tag_attributes_at_position(SPItem * item,Inkscape::Text::Layout::iterator const & position,unsigned * char_index)1048 text_tag_attributes_at_position(SPItem *item, Inkscape::Text::Layout::iterator const &position, unsigned *char_index)
1049 {
1050     if (item == nullptr || char_index == nullptr || !SP_IS_TEXT(item)) {
1051         return nullptr;   // flowtext doesn't support kerning yet
1052     }
1053     SPText *text = SP_TEXT(item);
1054 
1055     SPObject *source_item = nullptr;
1056     Glib::ustring::iterator source_text_iter;
1057     text->layout.getSourceOfCharacter(position, &source_item, &source_text_iter);
1058 
1059     if (!SP_IS_STRING(source_item)) {
1060         return nullptr;
1061     }
1062     Glib::ustring *string = &SP_STRING(source_item)->string;
1063     *char_index = sum_sibling_text_lengths_before(source_item);
1064     for (Glib::ustring::iterator it = string->begin() ; it != source_text_iter ; ++it) {
1065         ++*char_index;
1066     }
1067 
1068     return attributes_for_object(source_item->parent);
1069 }
1070 
1071 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)1072 sp_te_adjust_kerning_screen (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, Geom::Point by)
1073 {
1074     // divide increment by zoom
1075     // divide increment by matrix expansion
1076     gdouble factor = 1 / desktop->current_zoom();
1077     Geom::Affine t (item->i2doc_affine());
1078     factor = factor / t.descrim();
1079     by = factor * by;
1080 
1081     unsigned char_index;
1082     TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
1083     if (attributes) attributes->addToDxDy(char_index, by);
1084     if (start != end) {
1085         attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
1086         if (attributes) attributes->addToDxDy(char_index, -by);
1087     }
1088 
1089     item->updateRepr();
1090     item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1091 }
1092 
sp_te_adjust_dx(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop *,double delta)1093 void sp_te_adjust_dx(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop * /*desktop*/, double delta)
1094 {
1095     unsigned char_index = 0;
1096     TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
1097     if (attributes) {
1098         attributes->addToDx(char_index, delta);
1099     }
1100     if (start != end) {
1101         attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
1102         if (attributes) {
1103             attributes->addToDx(char_index, -delta);
1104         }
1105     }
1106 
1107     item->updateRepr();
1108     item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1109 }
1110 
sp_te_adjust_dy(SPItem * item,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop *,double delta)1111 void sp_te_adjust_dy(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop * /*desktop*/, double delta)
1112 {
1113     unsigned char_index = 0;
1114     TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
1115     if (attributes) {
1116         attributes->addToDy(char_index, delta);
1117     }
1118     if (start != end) {
1119         attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
1120         if (attributes) {
1121             attributes->addToDy(char_index, -delta);
1122         }
1123     }
1124 
1125     item->updateRepr();
1126     item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1127 }
1128 
1129 void
sp_te_adjust_rotation_screen(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop * desktop,gdouble pixels)1130 sp_te_adjust_rotation_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble pixels)
1131 {
1132     // divide increment by zoom
1133     // divide increment by matrix expansion
1134     gdouble factor = 1 / desktop->current_zoom();
1135     Geom::Affine t (text->i2doc_affine());
1136     factor = factor / t.descrim();
1137     Inkscape::Text::Layout const *layout = te_get_layout(text);
1138     if (layout == nullptr) return;
1139     SPObject *source_item = nullptr;
1140     layout->getSourceOfCharacter(std::min(start, end), &source_item);
1141     if (source_item == nullptr) {
1142         return;
1143     }
1144     gdouble degrees = (180/M_PI) * atan2(pixels, source_item->parent->style->font_size.computed / factor);
1145 
1146     sp_te_adjust_rotation(text, start, end, desktop, degrees);
1147 }
1148 
1149 void
sp_te_adjust_rotation(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop *,gdouble degrees)1150 sp_te_adjust_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop */*desktop*/, gdouble degrees)
1151 {
1152     unsigned char_index;
1153     TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index);
1154     if (attributes == nullptr) return;
1155 
1156     if (start != end) {
1157         for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) {
1158             attributes = text_tag_attributes_at_position(text, it, &char_index);
1159             if (attributes) attributes->addToRotate(char_index, degrees);
1160         }
1161     } else
1162         attributes->addToRotate(char_index, degrees);
1163 
1164     text->updateRepr();
1165     text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1166 }
1167 
sp_te_set_rotation(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPDesktop *,gdouble degrees)1168 void sp_te_set_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop */*desktop*/, gdouble degrees)
1169 {
1170     unsigned char_index = 0;
1171     TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index);
1172     if (attributes != nullptr) {
1173         if (start != end) {
1174             for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) {
1175                 attributes = text_tag_attributes_at_position(text, it, &char_index);
1176                 if (attributes) {
1177                     attributes->setRotate(char_index, degrees);
1178                 }
1179             }
1180         } else {
1181             attributes->setRotate(char_index, degrees);
1182         }
1183 
1184         text->updateRepr();
1185         text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
1186     }
1187 }
1188 
1189 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)1190 sp_te_adjust_tspan_letterspacing_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble by)
1191 {
1192     g_return_if_fail (text != nullptr);
1193     g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
1194 
1195     Inkscape::Text::Layout const *layout = te_get_layout(text);
1196 
1197     gdouble val;
1198     SPObject *source_obj = nullptr;
1199     unsigned nb_let;
1200     layout->getSourceOfCharacter(std::min(start, end), &source_obj);
1201 
1202     if (source_obj == nullptr) {   // end of text
1203         source_obj = text->lastChild();
1204     }
1205     if (SP_IS_STRING(source_obj)) {
1206         source_obj = source_obj->parent;
1207     }
1208 
1209     SPStyle *style = source_obj->style;
1210 
1211     // calculate real value
1212     /* TODO: Consider calculating val unconditionally, i.e. drop the first `if' line, and
1213        get rid of the `else val = 0.0'.  Similarly below and in sp-string.cpp. */
1214     if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
1215         if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
1216             val = style->font_size.computed * style->letter_spacing.value;
1217         } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
1218             val = style->font_size.computed * style->letter_spacing.value * 0.5;
1219         } else { // unknown unit - should not happen
1220             val = 0.0;
1221         }
1222     } else { // there's a real value in .computed, or it's zero
1223         val = style->letter_spacing.computed;
1224     }
1225 
1226     if (start == end) {
1227         while (!is_line_break_object(source_obj)) {     // move up the tree so we apply to the closest paragraph
1228             source_obj = source_obj->parent;
1229         }
1230         nb_let = sp_text_get_length(source_obj);
1231     } else {
1232         nb_let = abs(layout->iteratorToCharIndex(end) - layout->iteratorToCharIndex(start));
1233     }
1234 
1235     // divide increment by zoom and by the number of characters in the line,
1236     // so that the entire line is expanded by by pixels, no matter what its length
1237     gdouble const zoom = desktop->current_zoom();
1238     gdouble const zby = (by
1239                          / (zoom * (nb_let > 1 ? nb_let - 1 : 1))
1240                          / SP_ITEM(source_obj)->i2doc_affine().descrim());
1241     val += zby;
1242 
1243     if (start == end) {
1244         // set back value to entire paragraph
1245         style->letter_spacing.normal = FALSE;
1246         if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
1247             if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
1248                 style->letter_spacing.value = val / style->font_size.computed;
1249             } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
1250                 style->letter_spacing.value = val / style->font_size.computed * 2;
1251             }
1252         } else {
1253             style->letter_spacing.computed = val;
1254         }
1255 
1256         style->letter_spacing.set = TRUE;
1257     } else {
1258         // apply to selection only
1259         SPCSSAttr *css = sp_repr_css_attr_new();
1260         char string_val[40];
1261         g_snprintf(string_val, sizeof(string_val), "%f", val);
1262         sp_repr_css_set_property(css, "letter-spacing", string_val);
1263         sp_te_apply_style(text, start, end, css);
1264         sp_repr_css_attr_unref(css);
1265     }
1266 
1267     text->updateRepr();
1268     text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
1269 }
1270 
1271 // Only used for page-up and page-down and sp_te_adjust_linespacing_screen
1272 double
sp_te_get_average_linespacing(SPItem * text)1273 sp_te_get_average_linespacing (SPItem *text)
1274 {
1275     Inkscape::Text::Layout const *layout = te_get_layout(text);
1276     if (!layout)
1277         return 0;
1278 
1279     unsigned line_count = layout->lineIndex(layout->end());
1280     double all_lines_height = layout->characterAnchorPoint(layout->end())[Geom::Y] - layout->characterAnchorPoint(layout->begin())[Geom::Y];
1281     double average_line_height = all_lines_height / (line_count == 0 ? 1 : line_count);
1282     return average_line_height;
1283 }
1284 
1285 /** Adjust the line height by 'amount'.
1286  *  If top_level is true then objects without 'line-height' set or withwill get a set value,
1287  *  otherwise objects that inherit line-height will not get onw=e.
1288  */
1289 void
sp_te_adjust_line_height(SPObject * object,double amount,double average,bool top_level=true)1290 sp_te_adjust_line_height (SPObject *object, double amount, double average, bool top_level = true) {
1291 
1292     SPStyle *style = object->style;
1293 
1294     // Always set if top level true.
1295     // Also set if line_height is set to a non-zero value.
1296     if (top_level ||
1297         (style->line_height.set && !style->line_height.inherit && style->line_height.computed != 0)){
1298 
1299         // Scale default values
1300         if (!style->line_height.set || style->line_height.inherit || style->line_height.normal) {
1301             style->line_height.set = TRUE;
1302             style->line_height.inherit = FALSE;
1303             style->line_height.normal = FALSE;
1304             style->line_height.unit = SP_CSS_UNIT_NONE;
1305             style->line_height.value = style->line_height.computed = Inkscape::Text::Layout::LINE_HEIGHT_NORMAL;
1306         }
1307 
1308         switch (style->line_height.unit) {
1309 
1310             case SP_CSS_UNIT_NONE:
1311             default:
1312                 // Multiplier-type units, stored in computed
1313                 if (fabs(style->line_height.computed) < 0.001) {
1314                     style->line_height.computed = amount < 0.0 ? -0.001 : 0.001;
1315                     // the formula below could get stuck at zero
1316                 } else {
1317                     style->line_height.computed *= (average + amount) / average;
1318                 }
1319                 style->line_height.value = style->line_height.computed;
1320                 break;
1321 
1322 
1323             // Relative units, stored in value
1324             case SP_CSS_UNIT_EM:
1325             case SP_CSS_UNIT_EX:
1326             case SP_CSS_UNIT_PERCENT:
1327                 if (fabs(style->line_height.value) < 0.001) {
1328                     style->line_height.value = amount < 0.0 ? -0.001 : 0.001;
1329                 } else {
1330                     style->line_height.value *= (average + amount) / average;
1331                 }
1332                 break;
1333 
1334 
1335             // Absolute units
1336             case SP_CSS_UNIT_PX:
1337                 style->line_height.computed += amount;
1338                 style->line_height.value = style->line_height.computed;
1339                 break;
1340             case SP_CSS_UNIT_PT:
1341                 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "pt");
1342                 style->line_height.value = style->line_height.computed;
1343                 break;
1344             case SP_CSS_UNIT_PC:
1345                 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "pc");
1346                 style->line_height.value = style->line_height.computed;
1347                 break;
1348             case SP_CSS_UNIT_MM:
1349                 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "mm");
1350                 style->line_height.value = style->line_height.computed;
1351                 break;
1352             case SP_CSS_UNIT_CM:
1353                 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "cm");
1354                 style->line_height.value = style->line_height.computed;
1355                 break;
1356             case SP_CSS_UNIT_IN:
1357                 style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "in");
1358                 style->line_height.value = style->line_height.computed;
1359                 break;
1360         }
1361         object->updateRepr();
1362     }
1363 
1364     std::vector<SPObject*> children = object->childList(false);
1365     for (auto child: children) {
1366         sp_te_adjust_line_height (child, amount, average, false);
1367     }
1368 }
1369 
1370 void
sp_te_adjust_linespacing_screen(SPItem * text,Inkscape::Text::Layout::iterator const &,Inkscape::Text::Layout::iterator const &,SPDesktop * desktop,gdouble by)1371 sp_te_adjust_linespacing_screen (SPItem *text, Inkscape::Text::Layout::iterator const &/*start*/, Inkscape::Text::Layout::iterator const &/*end*/, SPDesktop *desktop, gdouble by)
1372 {
1373     // TODO: use start and end iterators to delineate the area to be affected
1374     g_return_if_fail (text != nullptr);
1375     g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
1376 
1377     Inkscape::Text::Layout const *layout = te_get_layout(text);
1378 
1379     double average_line_height = sp_te_get_average_linespacing (text);
1380     if (fabs(average_line_height) < 0.001) average_line_height = 0.001;
1381 
1382     // divide increment by zoom and by the number of lines,
1383     // so that the entire object is expanded by by pixels
1384     unsigned line_count = layout->lineIndex(layout->end());
1385     gdouble zby = by / (desktop->current_zoom() * (line_count == 0 ? 1 : line_count));
1386 
1387     // divide increment by matrix expansion
1388     Geom::Affine t(text->i2doc_affine());
1389     zby = zby / t.descrim();
1390 
1391     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1392     gint mode = prefs->getInt("/tools/text/line_spacing_mode", 0);
1393     if (mode == 0) { // Adaptive: <text> line-spacing is zero, only scale children.
1394         std::vector<SPObject*> children = text->childList(false);
1395         for (auto child: children) {
1396             sp_te_adjust_line_height (child, zby, average_line_height, false);
1397         }
1398     } else {
1399         sp_te_adjust_line_height (text, zby, average_line_height, true);
1400     }
1401 
1402     text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
1403 }
1404 
1405 
1406 /* ***************************************************************************************************/
1407 //                           S T Y L E   A P P L I C A T I O N
1408 
1409 
1410 /** converts an iterator to a character index, mainly because ustring::substr()
1411 doesn't have a version that takes iterators as parameters. */
char_index_of_iterator(Glib::ustring const & string,Glib::ustring::const_iterator text_iter)1412 static unsigned char_index_of_iterator(Glib::ustring const &string, Glib::ustring::const_iterator text_iter)
1413 {
1414     unsigned n = 0;
1415     for (Glib::ustring::const_iterator it = string.begin() ; it != string.end() && it != text_iter ; ++it)
1416         n++;
1417     return n;
1418 }
1419 
1420 // Move to style.h?
1421 /** applies the given style string on top of the existing styles for \a item,
1422 as opposed to sp_style_merge_from_style_string which merges its parameter
1423 underneath the existing styles (ie ignoring already set properties). */
overwrite_style_with_string(SPObject * item,gchar const * style_string)1424 static void overwrite_style_with_string(SPObject *item, gchar const *style_string)
1425 {
1426     SPStyle style(item->document);
1427     style.mergeString(style_string);
1428     gchar const *item_style_string = item->getRepr()->attribute("style");
1429     if (item_style_string && *item_style_string) {
1430         style.mergeString(item_style_string);
1431     }
1432     Glib::ustring new_style_string = style.write();
1433     item->setAttributeOrRemoveIfEmpty("style", new_style_string);
1434 }
1435 
1436 // Move to style.h?
1437 /** Returns true if the style of \a parent and the style of \a child are
1438 equivalent (and hence the children of both will appear the same). It is a
1439 limitation of the current implementation that \a parent must be a (not
1440 necessarily immediate) ancestor of \a child. */
objects_have_equal_style(SPObject const * parent,SPObject const * child)1441 static bool objects_have_equal_style(SPObject const *parent, SPObject const *child)
1442 {
1443     // the obvious implementation of strcmp(style_write_all(parent), style_write_all(child))
1444     // will not work. Firstly because of an inheritance bug in style.cpp that has
1445     // implications too large for me to feel safe fixing, but mainly because the css spec
1446     // requires that the computed value is inherited, not the specified value.
1447     g_assert(parent->isAncestorOf(child));
1448 
1449     Glib::ustring parent_style = parent->style->write( SP_STYLE_FLAG_ALWAYS );
1450 
1451     // we have to write parent_style then read it again, because some properties format their values
1452     // differently depending on whether they're set or not (*cough*dash-offset*cough*)
1453     SPStyle parent_spstyle(parent->document);
1454     parent_spstyle.mergeString(parent_style.c_str());
1455     parent_style = parent_spstyle.write(SP_STYLE_FLAG_ALWAYS);
1456 
1457     Glib::ustring child_style_construction;
1458     while (child != parent) {
1459         // FIXME: this assumes that child's style is only in style= whereas it can also be in css attributes!
1460         char const *style_text = child->getRepr()->attribute("style");
1461         if (style_text && *style_text) {
1462             child_style_construction.insert(0, style_text);
1463             child_style_construction.insert(0, 1, ';');
1464         }
1465         child = child->parent;
1466     }
1467     child_style_construction.insert(0, parent_style);
1468 
1469     SPStyle child_spstyle(parent->document);
1470     child_spstyle.mergeString(child_style_construction.c_str());
1471     Glib::ustring child_style = child_spstyle.write(SP_STYLE_FLAG_ALWAYS);
1472 
1473     bool equal = (child_style == parent_style);  // Glib::ustring overloads == operator
1474     return equal;
1475 }
1476 
1477 /** returns true if \a first and \a second contain all the same attributes
1478 with the same values as each other. Note that we have to compare both
1479 forwards and backwards to make sure we don't miss any attributes that are
1480 in one but not the other. */
css_attrs_are_equal(SPCSSAttr const * first,SPCSSAttr const * second)1481 static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second)
1482 {
1483     for ( const auto & attr : first->attributeList()) {
1484         gchar const *other_attr = second->attribute(g_quark_to_string(attr.key));
1485         if (other_attr == nullptr || strcmp(attr.value, other_attr))
1486             return false;
1487     }
1488     for (const auto & attr : second->attributeList()) {
1489         gchar const *other_attr = first->attribute(g_quark_to_string(attr.key));
1490         if (other_attr == nullptr || strcmp(attr.value, other_attr))
1491             return false;
1492     }
1493     return true;
1494 }
1495 
1496 /** sets the given css attribute on this object and all its descendants.
1497 Annoyingly similar to sp_desktop_apply_css_recursive(), except without the
1498 transform stuff. */
apply_css_recursive(SPObject * o,SPCSSAttr const * css)1499 static void apply_css_recursive(SPObject *o, SPCSSAttr const *css)
1500 {
1501     sp_repr_css_change(o->getRepr(), const_cast<SPCSSAttr*>(css), "style");
1502 
1503     for (auto& child: o->children) {
1504         if (sp_repr_css_property(const_cast<SPCSSAttr*>(css), "opacity", nullptr) != nullptr) {
1505             // Unset properties which are accumulating and thus should not be set recursively.
1506             // 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.
1507             SPCSSAttr *css_recurse = sp_repr_css_attr_new();
1508             sp_repr_css_merge(css_recurse, const_cast<SPCSSAttr*>(css));
1509             sp_repr_css_set_property(css_recurse, "opacity", nullptr);
1510             apply_css_recursive(&child, css_recurse);
1511             sp_repr_css_attr_unref(css_recurse);
1512         } else {
1513             apply_css_recursive(&child, const_cast<SPCSSAttr*>(css));
1514         }
1515     }
1516 }
1517 
1518 /** applies the given style to all the objects at the given level and below
1519 which are between \a start_item and \a end_item, creating spans as necessary.
1520 If \a start_item or \a end_item are NULL then the style is applied to all
1521 objects to the beginning or end respectively. \a span_object_name is the
1522 name of the xml for a text span (ie tspan or flowspan). */
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)1523 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)
1524 {
1525     bool passed_start = start_item == nullptr ? true : false;
1526     Inkscape::XML::Document *xml_doc = common_ancestor->document->getReprDoc();
1527 
1528     for (SPObject *child = common_ancestor->firstChild() ; child ; child = child->getNext()) {
1529         if (start_item == child) {
1530             passed_start = true;
1531         }
1532 
1533         if (passed_start) {
1534             if (end_item && child->isAncestorOf(end_item)) {
1535                 recursively_apply_style(child, css, nullptr, start_text_iter, end_item, end_text_iter, span_object_name);
1536                 break;
1537             }
1538             // apply style
1539 
1540             // note that when adding stuff we must make sure that 'child' stays valid so the for loop keeps working.
1541             // often this means that new spans are created before child and child is modified only
1542             if (SP_IS_STRING(child)) {
1543                 SPString *string_item = SP_STRING(child);
1544                 bool surround_entire_string = true;
1545 
1546                 Inkscape::XML::Node *child_span = xml_doc->createElement(span_object_name);
1547                 sp_repr_css_set(child_span, const_cast<SPCSSAttr*>(css), "style");   // better hope that prototype wasn't nonconst for a good reason
1548                 SPObject *prev_item = child->getPrev();
1549                 Inkscape::XML::Node *prev_repr = prev_item ? prev_item->getRepr() : nullptr;
1550 
1551                 if (child == start_item || child == end_item) {
1552                     surround_entire_string = false;
1553                     if (start_item == end_item && start_text_iter != string_item->string.begin()) {
1554                         // eg "abcDEFghi"  -> "abc"<span>"DEF"</span>"ghi"
1555                         unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
1556                         unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
1557 
1558                         Inkscape::XML::Node *text_before = xml_doc->createTextNode(string_item->string.substr(0, start_char_index).c_str());
1559                         common_ancestor->getRepr()->addChild(text_before, prev_repr);
1560                         common_ancestor->getRepr()->addChild(child_span, text_before);
1561                         Inkscape::GC::release(text_before);
1562                         Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(start_char_index, end_char_index - start_char_index).c_str());
1563                         child_span->appendChild(text_in_span);
1564                         Inkscape::GC::release(text_in_span);
1565                         child->getRepr()->setContent(string_item->string.substr(end_char_index).c_str());
1566                     } else if (child == end_item) {
1567                         // eg "ABCdef" -> <span>"ABC"</span>"def"
1568                         //  (includes case where start_text_iter == begin())
1569                         // NB: we might create an empty string here. Doesn't matter, it'll get cleaned up later
1570                         unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
1571 
1572                         common_ancestor->getRepr()->addChild(child_span, prev_repr);
1573                         Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(0, end_char_index).c_str());
1574                         child_span->appendChild(text_in_span);
1575                         Inkscape::GC::release(text_in_span);
1576                         child->getRepr()->setContent(string_item->string.substr(end_char_index).c_str());
1577                     } else if (start_text_iter != string_item->string.begin()) {
1578                         // eg "abcDEF" -> "abc"<span>"DEF"</span>
1579                         unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
1580 
1581                         Inkscape::XML::Node *text_before = xml_doc->createTextNode(string_item->string.substr(0, start_char_index).c_str());
1582                         common_ancestor->getRepr()->addChild(text_before, prev_repr);
1583                         common_ancestor->getRepr()->addChild(child_span, text_before);
1584                         Inkscape::GC::release(text_before);
1585                         Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(start_char_index).c_str());
1586                         child_span->appendChild(text_in_span);
1587                         Inkscape::GC::release(text_in_span);
1588                         child->deleteObject();
1589                         child = common_ancestor->get_child_by_repr(child_span);
1590 
1591                     } else
1592                         surround_entire_string = true;
1593                 }
1594                 if (surround_entire_string) {
1595                     Inkscape::XML::Node *child_repr = child->getRepr();
1596                     common_ancestor->getRepr()->addChild(child_span, child_repr);
1597                     Inkscape::GC::anchor(child_repr);
1598                     common_ancestor->getRepr()->removeChild(child_repr);
1599                     child_span->appendChild(child_repr);
1600                     Inkscape::GC::release(child_repr);
1601                     child = common_ancestor->get_child_by_repr(child_span);
1602                 }
1603                 Inkscape::GC::release(child_span);
1604 
1605             } else if (child != end_item) {   // not a string and we're applying to the entire object. This is easy
1606                 apply_css_recursive(child, css);
1607             }
1608 
1609         } else {  // !passed_start
1610             if (child->isAncestorOf(start_item)) {
1611                 recursively_apply_style(child, css, start_item, start_text_iter, end_item, end_text_iter, span_object_name);
1612                 if (end_item && child->isAncestorOf(end_item))
1613                     break;   // only happens when start_item == end_item (I think)
1614                 passed_start = true;
1615             }
1616         }
1617 
1618         if (end_item == child)
1619             break;
1620     }
1621 }
1622 
1623 /* if item is at the beginning of a tree it doesn't matter which element
1624 it points to so for neatness we would like it to point to the highest
1625 possible child of \a common_ancestor. There is no iterator return because
1626 a string can never be an ancestor.
1627 
1628 eg: <span><span>*ABC</span>DEFghi</span> where * is the \a item. We would
1629 like * to point to the inner span because we can apply style to that whole
1630 span. */
ascend_while_first(SPObject * item,Glib::ustring::iterator text_iter,SPObject * common_ancestor)1631 static SPObject* ascend_while_first(SPObject *item, Glib::ustring::iterator text_iter, SPObject *common_ancestor)
1632 {
1633     if (item == common_ancestor)
1634         return item;
1635     if (SP_IS_STRING(item))
1636         if (text_iter != SP_STRING(item)->string.begin())
1637             return item;
1638     for ( ; ; ) {
1639         SPObject *parent = item->parent;
1640         if (parent == common_ancestor) {
1641             break;
1642         }
1643         if (item != parent->firstChild()) {
1644             break;
1645         }
1646         item = parent;
1647     }
1648     return item;
1649 }
1650 
1651 
1652 /**     empty spans: abc<span></span>def
1653                       -> abcdef                  */
tidy_operator_empty_spans(SPObject ** item,bool)1654 static bool tidy_operator_empty_spans(SPObject **item, bool /*has_text_decoration*/)
1655 {
1656     bool result = false;
1657     if ( !(*item)->hasChildren()
1658          && !is_line_break_object(*item)
1659          && !(SP_IS_STRING(*item) && !SP_STRING(*item)->string.empty())
1660         ) {
1661         SPObject *next = (*item)->getNext();
1662         (*item)->deleteObject();
1663         *item = next;
1664         result = true;
1665     }
1666     return result;
1667 }
1668 
1669 /**    inexplicable spans: abc<span style="">def</span>ghi
1670                             -> "abc""def""ghi"
1671 the repeated strings will be merged by another operator. */
tidy_operator_inexplicable_spans(SPObject ** item,bool)1672 static bool tidy_operator_inexplicable_spans(SPObject **item, bool /*has_text_decoration*/)
1673 {
1674     //XML Tree being directly used here while it shouldn't be.
1675     if (*item && sp_repr_is_meta_element((*item)->getRepr())) {
1676         return false;
1677     }
1678     if (SP_IS_STRING(*item)) {
1679         return false;
1680     }
1681     if (is_line_break_object(*item)) {
1682         return false;
1683     }
1684     TextTagAttributes *attrs = attributes_for_object(*item);
1685     if (attrs && attrs->anyAttributesSet()) {
1686         return false;
1687     }
1688     if (!objects_have_equal_style((*item)->parent, *item)) {
1689         return false;
1690     }
1691     SPObject *next = *item;
1692     while ((*item)->hasChildren()) {
1693         Inkscape::XML::Node *repr = (*item)->firstChild()->getRepr();
1694         Inkscape::GC::anchor(repr);
1695         (*item)->getRepr()->removeChild(repr);
1696         (*item)->parent->getRepr()->addChild(repr, next->getRepr());
1697         Inkscape::GC::release(repr);
1698         next = next->getNext();
1699     }
1700     (*item)->deleteObject();
1701     *item = next;
1702     return true;
1703 }
1704 
1705 /**    repeated spans: <font a>abc</font><font a>def</font>
1706                         -> <font a>abcdef</font>            */
tidy_operator_repeated_spans(SPObject ** item,bool)1707 static bool tidy_operator_repeated_spans(SPObject **item, bool /*has_text_decoration*/)
1708 {
1709     SPObject *first = *item;
1710     SPObject *second = first->getNext();
1711     if (second == nullptr) return false;
1712 
1713     Inkscape::XML::Node *first_repr = first->getRepr();
1714     Inkscape::XML::Node *second_repr = second->getRepr();
1715 
1716     if (first_repr->type() != second_repr->type()) return false;
1717 
1718     if (SP_IS_STRING(first) && SP_IS_STRING(second)) {
1719         // also amalgamate consecutive SPStrings into one
1720         Glib::ustring merged_string = SP_STRING(first)->string + SP_STRING(second)->string;
1721         first->getRepr()->setContent(merged_string.c_str());
1722         second_repr->parent()->removeChild(second_repr);
1723         return true;
1724     }
1725 
1726     // merge consecutive spans with identical styles into one
1727     if (first_repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE) return false;
1728     if (strcmp(first_repr->name(), second_repr->name()) != 0) return false;
1729     if (is_line_break_object(second)) return false;
1730     gchar const *first_style = first_repr->attribute("style");
1731     gchar const *second_style = second_repr->attribute("style");
1732     if (!((first_style == nullptr && second_style == nullptr)
1733           || (first_style != nullptr && second_style != nullptr && !strcmp(first_style, second_style))))
1734         return false;
1735 
1736     // all our tests passed: do the merge
1737     TextTagAttributes *attributes_first = attributes_for_object(first);
1738     TextTagAttributes *attributes_second = attributes_for_object(second);
1739     if (attributes_first && attributes_second && attributes_second->anyAttributesSet()) {
1740         TextTagAttributes attributes_first_copy = *attributes_first;
1741         attributes_first->join(attributes_first_copy, *attributes_second, sp_text_get_length(first));
1742     }
1743     move_child_nodes(second_repr, first_repr);
1744     second_repr->parent()->removeChild(second_repr);
1745     return true;
1746     // *item is still the next object to process
1747 }
1748 
1749 /**    redundant nesting: <font a><font b>abc</font></font>
1750                            -> <font b>abc</font>
1751        excessive nesting: <font a><size 1>abc</size></font>
1752                            -> <font a,size 1>abc</font>      */
tidy_operator_excessive_nesting(SPObject ** item,bool)1753 static bool tidy_operator_excessive_nesting(SPObject **item, bool /*has_text_decoration*/)
1754 {
1755     if (!(*item)->hasChildren()) {
1756         return false;
1757     }
1758     if ((*item)->firstChild() != (*item)->lastChild()) {
1759         return false;
1760     }
1761     if (SP_IS_FLOWREGION((*item)->firstChild()) || SP_IS_FLOWREGIONEXCLUDE((*item)->firstChild())) {
1762         return false;
1763     }
1764     if (SP_IS_STRING((*item)->firstChild())) {
1765         return false;
1766     }
1767     if (is_line_break_object((*item)->firstChild())) {
1768         return false;
1769     }
1770     TextTagAttributes *attrs = attributes_for_object((*item)->firstChild());
1771     if (attrs && attrs->anyAttributesSet()) {
1772         return false;
1773     }
1774     gchar const *child_style = (*item)->firstChild()->getRepr()->attribute("style");
1775     if (child_style && *child_style) {
1776         overwrite_style_with_string(*item, child_style);
1777     }
1778     move_child_nodes((*item)->firstChild()->getRepr(), (*item)->getRepr());
1779     (*item)->firstChild()->deleteObject();
1780     return true;
1781 }
1782 
1783 /** helper for tidy_operator_redundant_double_nesting() */
redundant_double_nesting_processor(SPObject ** item,SPObject * child,bool prepend)1784 static bool redundant_double_nesting_processor(SPObject **item, SPObject *child, bool prepend)
1785 {
1786     if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child)) {
1787         return false;
1788     }
1789     if (SP_IS_STRING(child)) {
1790         return false;
1791     }
1792     if (is_line_break_object(child)) {
1793         return false;
1794     }
1795     if (is_line_break_object(*item)) {
1796         return false;
1797     }
1798     TextTagAttributes *attrs = attributes_for_object(child);
1799     if (attrs && attrs->anyAttributesSet()) {
1800         return false;
1801     }
1802     if (!objects_have_equal_style((*item)->parent, child)) {
1803         return false;
1804     }
1805 
1806     Inkscape::XML::Node *insert_after_repr = nullptr;
1807     if (!prepend) {
1808         insert_after_repr = (*item)->getRepr();
1809     } else if ((*item)->getPrev()) {
1810         insert_after_repr = (*item)->getPrev()->getRepr();
1811     }
1812     while (child->getRepr()->childCount()) {
1813         Inkscape::XML::Node *move_repr = child->getRepr()->firstChild();
1814         Inkscape::GC::anchor(move_repr);
1815         child->getRepr()->removeChild(move_repr);
1816         (*item)->parent->getRepr()->addChild(move_repr, insert_after_repr);
1817         Inkscape::GC::release(move_repr);
1818         insert_after_repr = move_repr;      // I think this will stay valid long enough. It's garbage collected these days.
1819     }
1820     child->deleteObject();
1821     return true;
1822 }
1823 
1824 /**    redundant double nesting: <font b><font a><font b>abc</font>def</font>ghi</font>
1825                                 -> <font b>abc<font a>def</font>ghi</font>
1826 this function does its work when the parameter is the <font a> tag in the
1827 example. You may note that this only does its work when the doubly-nested
1828 child is the first or last. The other cases are called 'style inversion'
1829 below, and I'm not yet convinced that the result of that operation will be
1830 tidier in all cases. */
tidy_operator_redundant_double_nesting(SPObject ** item,bool)1831 static bool tidy_operator_redundant_double_nesting(SPObject **item, bool /*has_text_decoration*/)
1832 {
1833     if (!(*item)->hasChildren()) return false;
1834     if ((*item)->firstChild() == (*item)->lastChild()) return false;     // this is excessive nesting, done above
1835     if (redundant_double_nesting_processor(item, (*item)->firstChild(), true))
1836         return true;
1837     if (redundant_double_nesting_processor(item, (*item)->lastChild(), false))
1838         return true;
1839     return false;
1840 }
1841 
1842 /** helper for tidy_operator_redundant_semi_nesting(). Checks a few things,
1843 then compares the styles for item+child versus just child. If they're equal,
1844 tidying is possible. */
redundant_semi_nesting_processor(SPObject ** item,SPObject * child,bool prepend)1845 static bool redundant_semi_nesting_processor(SPObject **item, SPObject *child, bool prepend)
1846 {
1847     if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child))
1848         return false;
1849     if (SP_IS_STRING(child)) return false;
1850     if (is_line_break_object(child)) return false;
1851     if (is_line_break_object(*item)) return false;
1852     TextTagAttributes *attrs = attributes_for_object(child);
1853     if (attrs && attrs->anyAttributesSet()) return false;
1854     attrs = attributes_for_object(*item);
1855     if (attrs && attrs->anyAttributesSet()) return false;
1856 
1857     SPCSSAttr *css_child_and_item = sp_repr_css_attr_new();
1858     SPCSSAttr *css_child_only = sp_repr_css_attr_new();
1859     gchar const *item_style = (*item)->getRepr()->attribute("style");
1860     if (item_style && *item_style) {
1861         sp_repr_css_attr_add_from_string(css_child_and_item, item_style);
1862     }
1863     gchar const *child_style = child->getRepr()->attribute("style");
1864     if (child_style && *child_style) {
1865         sp_repr_css_attr_add_from_string(css_child_and_item, child_style);
1866         sp_repr_css_attr_add_from_string(css_child_only, child_style);
1867     }
1868     bool equal = css_attrs_are_equal(css_child_only, css_child_and_item);
1869     sp_repr_css_attr_unref(css_child_and_item);
1870     sp_repr_css_attr_unref(css_child_only);
1871     if (!equal) return false;
1872 
1873     Inkscape::XML::Document *xml_doc = (*item)->getRepr()->document();
1874     Inkscape::XML::Node *new_span = xml_doc->createElement((*item)->getRepr()->name());
1875     if (prepend) {
1876         SPObject *prev = (*item)->getPrev();
1877         (*item)->parent->getRepr()->addChild(new_span, prev ? prev->getRepr() : nullptr);
1878     } else {
1879         (*item)->parent->getRepr()->addChild(new_span, (*item)->getRepr());
1880     }
1881     new_span->setAttribute("style", child->getRepr()->attribute("style"));
1882     move_child_nodes(child->getRepr(), new_span);
1883     Inkscape::GC::release(new_span);
1884     child->deleteObject();
1885     return true;
1886 }
1887 
1888 /**    redundant semi-nesting: <font a><font b>abc</font>def</font>
1889                                 -> <font b>abc</font><font>def</font>
1890 test this by applying a colour to a region, then a different colour to
1891 a partially-overlapping region. */
tidy_operator_redundant_semi_nesting(SPObject ** item,bool)1892 static bool tidy_operator_redundant_semi_nesting(SPObject **item, bool /*has_text_decoration*/)
1893 {
1894     if (!(*item)->hasChildren()) return false;
1895     if ((*item)->firstChild() == (*item)->lastChild()) return false;     // this is redundant nesting, done above
1896     if (redundant_semi_nesting_processor(item, (*item)->firstChild(), true))
1897         return true;
1898     if (redundant_semi_nesting_processor(item, (*item)->lastChild(), false))
1899         return true;
1900     return false;
1901 }
1902 
1903 
1904 /* tidy_operator_styled_whitespace commented out: not only did it have bugs,
1905  * but it did *not* preserve the rendering: spaces in different font sizes,
1906  * for instance, have different width, so moving them out of tspans changes
1907  * the document. cf https://bugs.launchpad.net/inkscape/+bug/1477723
1908 */
1909 
1910 #if 0
1911 /** helper for tidy_operator_styled_whitespace(), finds the last string object
1912 in a paragraph which is not \a not_obj. */
1913 static SPString* find_last_string_child_not_equal_to(SPObject *root, SPObject *not_obj)
1914 {
1915     for (SPObject *child = root->lastChild() ; child ; child = child->getPrev())
1916     {
1917         if (child == not_obj) continue;
1918         if (child->hasChildren()) {
1919             SPString *ret = find_last_string_child_not_equal_to(child, not_obj);
1920             if (ret) return ret;
1921         } else if (SP_IS_STRING(child))
1922             return SP_STRING(child);
1923     }
1924     return NULL;
1925 }
1926 
1927 /** whitespace-only spans:
1928   abc<font> </font>def
1929    -> abc<font></font> def
1930   abc<b><i>def</i> </b>ghi
1931    -> abc<b><i>def</i></b> ghi
1932 
1933   visible text-decoration changes on white spaces should not be subject
1934   to this sort of processing.  So
1935     abc<text-decoration-color>  </text-decoration-color>def
1936   is unchanged.
1937 */
1938 static bool tidy_operator_styled_whitespace(SPObject **item, bool has_text_decoration)
1939 {
1940     // any type of visible text decoration is OK as pure spaces, so do nothing here in that case.
1941     if (!SP_IS_STRING(*item) ||  has_text_decoration ) {
1942         return false;
1943     }
1944     Glib::ustring const &str = SP_STRING(*item)->string;
1945     for (Glib::ustring::const_iterator it = str.begin() ; it != str.end() ; ++it) {
1946         if (!g_unichar_isspace(*it)) {
1947             return false;
1948         }
1949     }
1950 
1951 
1952     SPObject *test_item = *item;
1953     SPString *next_string;
1954     for ( ; ; ) {  // find the next string
1955         next_string = sp_te_seek_next_string_recursive(test_item->getNext());
1956         if (next_string) {
1957             next_string->string.insert(0, str);
1958             break;
1959         }
1960         for ( ; ; ) {   // go up one item in the xml
1961             test_item = test_item->parent;
1962             if (is_line_break_object(test_item)) {
1963                 break;
1964             }
1965             if (SP_IS_FLOWTEXT(test_item)) {
1966                 return false;
1967             }
1968             SPObject *next = test_item->getNext();
1969             if (next) {
1970                 test_item = next;
1971                 break;
1972             }
1973         }
1974         if (is_line_break_object(test_item)) {  // no next string, see if there's a prev string
1975             next_string = find_last_string_child_not_equal_to(test_item, *item);
1976             if (next_string == NULL) {
1977                 return false;   // an empty paragraph
1978             }
1979             next_string->string = str + next_string->string;
1980             break;
1981         }
1982     }
1983     next_string->getRepr()->setContent(next_string->string.c_str());
1984     SPObject *delete_obj = *item;
1985     *item = (*item)->getNext();
1986     delete_obj->deleteObject();
1987     return true;
1988 }
1989 #endif
1990 
1991 /* possible tidy operators that are not yet implemented, either because
1992 they are difficult, occur infrequently, or because I'm not sure that the
1993 output is tidier in all cases:
1994     duplicate styles in line break elements: <div italic><para italic>abc</para></div>
1995                                               -> <div italic><para>abc</para></div>
1996     style inversion: <font a>abc<font b>def<font a>ghi</font>jkl</font>mno</font>
1997                       -> <font a>abc<font b>def</font>ghi<font b>jkl</font>mno</font>
1998     mistaken precedence: <font a,size 1>abc</font><size 1>def</size>
1999                           -> <size 1><font a>abc</font>def</size>
2000 */
2001 
2002 /** Recursively walks the xml tree calling a set of cleanup operations on
2003 every child. Returns true if any changes were made to the tree.
2004 
2005 All the tidy operators return true if they made changes, and alter their
2006 parameter to point to the next object that should be processed, or NULL.
2007 They must not significantly alter (ie delete) any ancestor elements of the
2008 one they are passed.
2009 
2010 It may be that some of the later tidy operators that I wrote are actually
2011 general cases of the earlier operators, and hence the special-case-only
2012 versions can be removed. I haven't analysed my work in detail to figure
2013 out if this is so. */
tidy_xml_tree_recursively(SPObject * root,bool has_text_decoration)2014 static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration)
2015 {
2016     gchar const *root_style = (root)->getRepr()->attribute("style");
2017     if(root_style && strstr(root_style,"text-decoration"))has_text_decoration = true;
2018     static bool (* const tidy_operators[])(SPObject**, bool) = {
2019         tidy_operator_empty_spans,
2020         tidy_operator_inexplicable_spans,
2021         tidy_operator_repeated_spans,
2022         tidy_operator_excessive_nesting,
2023         tidy_operator_redundant_double_nesting,
2024         tidy_operator_redundant_semi_nesting
2025     };
2026     bool changes = false;
2027 
2028     for (SPObject *child = root->firstChild() ; child != nullptr ; ) {
2029         if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child) || SP_IS_TREF(child)) {
2030             child = child->getNext();
2031             continue;
2032         }
2033         if (child->hasChildren()) {
2034             changes |= tidy_xml_tree_recursively(child, has_text_decoration);
2035         }
2036 
2037         unsigned i;
2038         for (i = 0 ; i < sizeof(tidy_operators) / sizeof(tidy_operators[0]) ; i++) {
2039             if (tidy_operators[i](&child, has_text_decoration)) {
2040                 changes = true;
2041                 break;
2042             }
2043         }
2044         if (i == sizeof(tidy_operators) / sizeof(tidy_operators[0])) {
2045             child = child->getNext();
2046         }
2047     }
2048     return changes;
2049 }
2050 
2051 /** Applies the given CSS fragment to the characters of the given text or
2052 flowtext object between \a start and \a end, creating or removing span
2053 elements as necessary and optimal. */
sp_te_apply_style(SPItem * text,Inkscape::Text::Layout::iterator const & start,Inkscape::Text::Layout::iterator const & end,SPCSSAttr const * css)2054 void sp_te_apply_style(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPCSSAttr const *css)
2055 {
2056     // in the comments in the code below, capital letters are inside the application region, lowercase are outside
2057     if (start == end) return;
2058     Inkscape::Text::Layout::iterator first, last;
2059     if (start < end) {
2060         first = start;
2061         last = end;
2062     } else {
2063         first = end;
2064         last = start;
2065     }
2066     Inkscape::Text::Layout const *layout = te_get_layout(text);
2067     SPObject *start_item = nullptr, *end_item = nullptr;
2068     Glib::ustring::iterator start_text_iter, end_text_iter;
2069     layout->getSourceOfCharacter(first, &start_item, &start_text_iter);
2070     layout->getSourceOfCharacter(last, &end_item, &end_text_iter);
2071     if (start_item == nullptr) {
2072         return;   // start is at end of text
2073     }
2074     if (is_line_break_object(start_item)) {
2075         start_item = start_item->getNext();
2076     }
2077     if (is_line_break_object(end_item)) {
2078         end_item = end_item->getNext();
2079     }
2080     if (end_item == nullptr) {
2081         end_item = text;
2082     }
2083 
2084 
2085     /* Special case: With a tref, we only want to change its style when the whole
2086      * string is selected, in which case the style can be applied directly to the
2087      * tref node.  If only part of the tref's string child is selected, just return. */
2088 
2089     if (!sp_tref_fully_contained(start_item, start_text_iter, end_item, end_text_iter)) {
2090 
2091         return;
2092     }
2093 
2094     /* stage 1: applying the style. Go up to the closest common ancestor of
2095     start and end and then semi-recursively apply the style to all the
2096     objects in between. The semi-recursion is because it's only necessary
2097     at the beginning and end; the style can just be applied to the root
2098     child in the middle.
2099     eg: <span>abcDEF</span><span>GHI</span><span>JKLmno</span>
2100     The recursion may involve creating new spans.
2101     */
2102     SPObject *common_ancestor = get_common_ancestor(text, start_item, end_item);
2103 
2104     // bug #168370 (consider parent transform and viewBox)
2105     // snipplet copied from desktop-style.cpp sp_desktop_apply_css_recursive(...)
2106     SPCSSAttr *css_set = sp_repr_css_attr_new();
2107     sp_repr_css_merge(css_set, const_cast<SPCSSAttr*>(css));
2108     {
2109         Geom::Affine const local(SP_ITEM(common_ancestor)->i2doc_affine());
2110         double const ex(local.descrim());
2111         if ( ( ex != 0. )
2112              && ( ex != 1. ) ) {
2113             sp_css_attr_scale(css_set, 1/ex);
2114         }
2115     }
2116 
2117     start_item = ascend_while_first(start_item, start_text_iter, common_ancestor);
2118     end_item = ascend_while_first(end_item, end_text_iter, common_ancestor);
2119     recursively_apply_style(common_ancestor, css_set, start_item, start_text_iter, end_item, end_text_iter, span_name_for_text_object(text));
2120     sp_repr_css_attr_unref(css_set);
2121 
2122     /* stage 2: cleanup the xml tree (of which there are multiple passes) */
2123     /* discussion: this stage requires a certain level of inventiveness because
2124     it's not clear what the best representation is in many cases. An ideal
2125     implementation would provide some sort of scoring function to rate the
2126     ugliness of a given xml tree and try to reduce said function, but providing
2127     the various possibilities to be rated is non-trivial. Instead, I have opted
2128     for a multi-pass technique which simply recognises known-ugly patterns and
2129     has matching routines for optimising the patterns it finds. It's reasonably
2130     easy to add new pattern matching processors. If everything gets disastrous
2131     and neither option can be made to work, a fallback could be to reduce
2132     everything to a single level of nesting and drop all pretense of
2133     roundtrippability. */
2134     bool has_text_decoration = false;
2135     gchar const *root_style = (text)->getRepr()->attribute("style");
2136     if(root_style && strstr(root_style,"text-decoration")) has_text_decoration = true;
2137     while (tidy_xml_tree_recursively(common_ancestor, has_text_decoration)){};
2138 
2139     // if we only modified subobjects this won't have been automatically sent
2140     text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
2141 }
2142 
is_part_of_text_subtree(SPObject * obj)2143 bool is_part_of_text_subtree (SPObject *obj)
2144 {
2145     return (SP_IS_TSPAN(obj)
2146             || SP_IS_TEXT(obj)
2147             || SP_IS_FLOWTEXT(obj)
2148             || SP_IS_FLOWTSPAN(obj)
2149             || SP_IS_FLOWDIV(obj)
2150             || SP_IS_FLOWPARA(obj)
2151             || SP_IS_FLOWLINE(obj)
2152             || SP_IS_FLOWREGIONBREAK(obj));
2153 }
2154 
is_top_level_text_object(SPObject * obj)2155 bool is_top_level_text_object (SPObject *obj)
2156 {
2157     return (SP_IS_TEXT(obj)
2158             || SP_IS_FLOWTEXT(obj));
2159 }
2160 
has_visible_text(SPObject * obj)2161 bool has_visible_text(SPObject *obj)
2162 {
2163     bool hasVisible = false;
2164 
2165     if (SP_IS_STRING(obj) && !SP_STRING(obj)->string.empty()) {
2166         hasVisible = true; // maybe we should also check that it's not all whitespace?
2167     } else {
2168         for (auto& child: obj->children) {
2169             if (has_visible_text(const_cast<SPObject *>(&child))) {
2170                 hasVisible = true;
2171                 break;
2172             }
2173         }
2174     }
2175 
2176     return hasVisible;
2177 }
2178 
2179 /*
2180   Local Variables:
2181   mode:c++
2182   c-file-style:"stroustrup"
2183   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
2184   indent-tabs-mode:nil
2185   fill-column:99
2186   End:
2187 */
2188 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
2189