1 /*
2 * Parent class for text and flowtext
3 *
4 * Authors:
5 * bulia byak
6 * Richard Hughes
7 *
8 * Copyright (C) 2004-5 authors
9 *
10 * Released under GNU GPL, read the file 'COPYING' for more information
11 */
13 #ifdef HAVE_CONFIG_H
14 # include "config.h"
15 #endif
17 #include "desktop.h"
18 #include "style.h"
19 #include "unit-constants.h"
21 #include "xml/repr.h"
22 #include "xml/attribute-record.h"
24 #include "sp-textpath.h"
25 #include "sp-flowtext.h"
26 #include "sp-flowdiv.h"
27 #include "sp-flowregion.h"
28 #include "sp-tspan.h"
30 #include "text-editing.h"
32 static bool tidy_xml_tree_recursively(SPObject *root);
34 Inkscape::Text::Layout const * te_get_layout (SPItem const *item)
35 {
36 if (SP_IS_TEXT(item)) {
37 return &(SP_TEXT(item)->layout);
38 } else if (SP_IS_FLOWTEXT (item)) {
39 return &(SP_FLOWTEXT(item)->layout);
40 }
41 return NULL;
42 }
44 static void te_update_layout_now (SPItem *item)
45 {
46 if (SP_IS_TEXT(item))
47 SP_TEXT(item)->rebuildLayout();
48 else if (SP_IS_FLOWTEXT (item))
49 SP_FLOWTEXT(item)->rebuildLayout();
50 }
52 /** Returns true if there are no visible characters on the canvas */
53 bool
54 sp_te_output_is_empty (SPItem const *item)
55 {
56 Inkscape::Text::Layout const *layout = te_get_layout(item);
57 return layout->begin() == layout->end();
58 }
60 /** Returns true if the user has typed nothing in the text box */
61 bool
62 sp_te_input_is_empty (SPObject const *item)
63 {
64 if (SP_IS_STRING(item)) return SP_STRING(item)->string.empty();
65 for (SPObject const *child = item->firstChild() ; child ; child = SP_OBJECT_NEXT(child))
66 if (!sp_te_input_is_empty(child)) return false;
67 return true;
68 }
70 Inkscape::Text::Layout::iterator
71 sp_te_get_position_by_coords (SPItem const *item, NR::Point &i_p)
72 {
73 NR::Matrix im=sp_item_i2d_affine (item);
74 im = im.inverse();
76 NR::Point p = i_p * im;
77 Inkscape::Text::Layout const *layout = te_get_layout(item);
78 return layout->getNearestCursorPositionTo(p);
79 }
81 std::vector<NR::Point> sp_te_create_selection_quads(SPItem const *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, NR::Matrix const &transform)
82 {
83 if (start == end)
84 return std::vector<NR::Point>();
85 Inkscape::Text::Layout const *layout = te_get_layout(item);
86 if (layout == NULL)
87 return std::vector<NR::Point>();
89 return layout->createSelectionShape(start, end, transform);
90 }
92 void
93 sp_te_get_cursor_coords (SPItem const *item, Inkscape::Text::Layout::iterator const &position, NR::Point &p0, NR::Point &p1)
94 {
95 Inkscape::Text::Layout const *layout = te_get_layout(item);
96 double height, rotation;
97 layout->queryCursorShape(position, &p0, &height, &rotation);
98 p1 = NR::Point(p0[NR::X] + height * sin(rotation), p0[NR::Y] - height * cos(rotation));
99 }
101 SPStyle const * sp_te_style_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position)
102 {
103 Inkscape::Text::Layout const *layout = te_get_layout(text);
104 if (layout == NULL)
105 return NULL;
106 SPObject const *pos_obj = NULL;
107 layout->getSourceOfCharacter(position, (void**)&pos_obj);
108 if (pos_obj == NULL) pos_obj = text;
109 while (SP_OBJECT_STYLE(pos_obj) == NULL)
110 pos_obj = SP_OBJECT_PARENT(pos_obj); // SPStrings don't have style
111 return SP_OBJECT_STYLE(pos_obj);
112 }
114 /*
115 * for debugging input
116 *
117 char * dump_hexy(const gchar * utf8)
118 {
119 static char buffer[1024];
121 buffer[0]='\0';
122 for (const char *ptr=utf8; *ptr; ptr++) {
123 sprintf(buffer+strlen(buffer),"x%02X",(unsigned char)*ptr);
124 }
125 return buffer;
126 }
127 */
129 Inkscape::Text::Layout::iterator sp_te_replace(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, gchar const *utf8)
130 {
131 Inkscape::Text::Layout::iterator new_start = sp_te_delete(item, start, end);
132 return sp_te_insert(item, new_start, utf8);
133 }
136 /* ***************************************************************************************************/
137 // I N S E R T I N G T E X T
139 static bool is_line_break_object(SPObject const *object)
140 {
141 return SP_IS_TEXT(object)
142 || (SP_IS_TSPAN(object) && SP_TSPAN(object)->role != SP_TSPAN_ROLE_UNSPECIFIED)
143 || SP_IS_TEXTPATH(object)
144 || SP_IS_FLOWDIV(object)
145 || SP_IS_FLOWPARA(object)
146 || SP_IS_FLOWLINE(object)
147 || SP_IS_FLOWREGIONBREAK(object);
148 }
150 /** returns the attributes for an object, or NULL if it isn't a text,
151 tspan or textpath. */
152 static TextTagAttributes* attributes_for_object(SPObject *object)
153 {
154 if (SP_IS_TSPAN(object))
155 return &SP_TSPAN(object)->attributes;
156 if (SP_IS_TEXT(object))
157 return &SP_TEXT(object)->attributes;
158 if (SP_IS_TEXTPATH(object))
159 return &SP_TEXTPATH(object)->attributes;
160 return NULL;
161 }
163 static const char * span_name_for_text_object(SPObject const *object)
164 {
165 if (SP_IS_TEXT(object)) return "svg:tspan";
166 else if (SP_IS_FLOWTEXT(object)) return "svg:flowSpan";
167 return NULL;
168 }
170 /** Recursively gets the length of all the SPStrings at or below the given
171 \a item. Also adds 1 for each line break encountered. */
172 unsigned sp_text_get_length(SPObject const *item)
173 {
174 unsigned length = 0;
176 if (SP_IS_STRING(item)) return SP_STRING(item)->string.length();
177 if (is_line_break_object(item)) length++;
178 for (SPObject const *child = item->firstChild() ; child ; child = SP_OBJECT_NEXT(child)) {
179 if (SP_IS_STRING(child)) length += SP_STRING(child)->string.length();
180 else length += sp_text_get_length(child);
181 }
182 return length;
183 }
185 static Inkscape::XML::Node* duplicate_node_without_children(Inkscape::XML::Node const *old_node)
186 {
187 switch (old_node->type()) {
188 case Inkscape::XML::ELEMENT_NODE: {
189 Inkscape::XML::Node *new_node = sp_repr_new(old_node->name());
190 Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attributes = old_node->attributeList();
191 GQuark const id_key = g_quark_from_string("id");
192 for ( ; attributes ; attributes++) {
193 if (attributes->key == id_key) continue;
194 new_node->setAttribute(g_quark_to_string(attributes->key), attributes->value);
195 }
196 return new_node;
197 }
199 case Inkscape::XML::TEXT_NODE:
200 return sp_repr_new_text(old_node->content());
202 case Inkscape::XML::COMMENT_NODE:
203 return sp_repr_new_comment(old_node->content());
205 case Inkscape::XML::DOCUMENT_NODE:
206 return NULL; // this had better never happen
207 }
208 return NULL;
209 }
211 /** returns the sum of the (recursive) lengths of all the SPStrings prior
212 to \a item at the same level. */
213 static unsigned sum_sibling_text_lengths_before(SPObject const *item)
214 {
215 unsigned char_index = 0;
216 for (SPObject *sibling = SP_OBJECT_PARENT(item)->firstChild() ; sibling && sibling != item ; sibling = SP_OBJECT_NEXT(sibling))
217 char_index += sp_text_get_length(sibling);
218 return char_index;
219 }
221 /** splits the attributes for the first object at the given \a char_index
222 and moves the ones after that point into \a second_item. */
223 static void split_attributes(SPObject *first_item, SPObject *second_item, unsigned char_index)
224 {
225 TextTagAttributes *first_attrs = attributes_for_object(first_item);
226 TextTagAttributes *second_attrs = attributes_for_object(second_item);
227 if (first_attrs && second_attrs)
228 first_attrs->split(char_index, second_attrs);
229 }
231 /** recursively divides the XML node tree into two objects: the original will
232 contain all objects up to and including \a split_obj and the returned value
233 will be the new leaf which represents the copy of \a split_obj and extends
234 down the tree with new elements all the way to the common root which is the
235 parent of the first line break node encountered.
236 */
237 static SPObject* split_text_object_tree_at(SPObject *split_obj, unsigned char_index)
238 {
239 if (is_line_break_object(split_obj)) {
240 Inkscape::XML::Node *new_node = duplicate_node_without_children(SP_OBJECT_REPR(split_obj));
241 SP_OBJECT_REPR(SP_OBJECT_PARENT(split_obj))->addChild(new_node, SP_OBJECT_REPR(split_obj));
242 Inkscape::GC::release(new_node);
243 split_attributes(split_obj, SP_OBJECT_NEXT(split_obj), char_index);
244 return SP_OBJECT_NEXT(split_obj);
245 }
247 unsigned char_count_before = sum_sibling_text_lengths_before(split_obj);
248 SPObject *duplicate_obj = split_text_object_tree_at(SP_OBJECT_PARENT(split_obj), char_index + char_count_before);
249 // copy the split node
250 Inkscape::XML::Node *new_node = duplicate_node_without_children(SP_OBJECT_REPR(split_obj));
251 SP_OBJECT_REPR(duplicate_obj)->appendChild(new_node);
252 Inkscape::GC::release(new_node);
254 // sort out the copied attributes (x/y/dx/dy/rotate)
255 split_attributes(split_obj, duplicate_obj->firstChild(), char_index);
257 // then move all the subsequent nodes
258 split_obj = SP_OBJECT_NEXT(split_obj);
259 while (split_obj) {
260 Inkscape::XML::Node *move_repr = SP_OBJECT_REPR(split_obj);
261 SPObject *next_obj = SP_OBJECT_NEXT(split_obj); // this is about to become invalidated by removeChild()
262 Inkscape::GC::anchor(move_repr);
263 SP_OBJECT_REPR(SP_OBJECT_PARENT(split_obj))->removeChild(move_repr);
264 SP_OBJECT_REPR(duplicate_obj)->appendChild(move_repr);
265 Inkscape::GC::release(move_repr);
267 split_obj = next_obj;
268 }
269 return duplicate_obj->firstChild();
270 }
272 /** inserts a new line break at the given position in a text or flowtext
273 object. If the position is in the middle of a span, the XML tree must be
274 chopped in two such that the line can be created at the root of the text
275 element. Returns an iterator pointing just after the inserted break. */
276 Inkscape::Text::Layout::iterator sp_te_insert_line (SPItem *item, Inkscape::Text::Layout::iterator const &position)
277 {
278 // Disable newlines in a textpath; TODO: maybe on Enter in a textpath, separate it into two
279 // texpaths attached to the same path, with a vertical shift
280 if (SP_IS_TEXT_TEXTPATH (item))
281 return position;
283 Inkscape::Text::Layout const *layout = te_get_layout(item);
284 SPObject *split_obj;
285 Glib::ustring::iterator split_text_iter;
286 if (position == layout->end())
287 split_obj = NULL;
288 else
289 layout->getSourceOfCharacter(position, (void**)&split_obj, &split_text_iter);
291 if (split_obj == NULL || is_line_break_object(split_obj)) {
292 if (split_obj == NULL) split_obj = item->lastChild();
293 if (split_obj) {
294 Inkscape::XML::Node *new_node = duplicate_node_without_children(SP_OBJECT_REPR(split_obj));
295 SP_OBJECT_REPR(SP_OBJECT_PARENT(split_obj))->addChild(new_node, SP_OBJECT_REPR(split_obj));
296 Inkscape::GC::release(new_node);
297 }
298 } else if (SP_IS_STRING(split_obj)) {
299 Glib::ustring *string = &SP_STRING(split_obj)->string;
300 unsigned char_index = 0;
301 for (Glib::ustring::iterator it = string->begin() ; it != split_text_iter ; it++)
302 char_index++;
303 // we need to split the entire text tree into two
304 SPString *new_string = SP_STRING(split_text_object_tree_at(split_obj, char_index));
305 SP_OBJECT_REPR(new_string)->setContent(&*split_text_iter.base()); // a little ugly
306 string->erase(split_text_iter, string->end());
307 SP_OBJECT_REPR(split_obj)->setContent(string->c_str());
308 // TODO: if the split point was at the beginning of a span we have a whole load of empty elements to clean up
309 } else {
310 // TODO
311 // I think the only case to put here is arbitrary gaps, which nobody uses yet
312 }
313 item->updateRepr(SP_OBJECT_REPR(item),SP_OBJECT_WRITE_EXT);
314 unsigned char_index = layout->iteratorToCharIndex(position);
315 te_update_layout_now(item);
316 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
317 return layout->charIndexToIterator(char_index + 1);
318 }
320 /** finds the first SPString after the given position, including children, excluding parents */
321 static SPString* sp_te_seek_next_string_recursive(SPObject *start_obj)
322 {
323 while (start_obj) {
324 if (start_obj->hasChildren()) {
325 SPString *found_string = sp_te_seek_next_string_recursive(start_obj->firstChild());
326 if (found_string) return found_string;
327 }
328 if (SP_IS_STRING(start_obj)) return SP_STRING(start_obj);
329 start_obj = SP_OBJECT_NEXT(start_obj);
330 if (is_line_break_object(start_obj))
331 break; // don't cross line breaks
332 }
333 return NULL;
334 }
336 /** inserts the given characters into the given string and inserts
337 corresponding new x/y/dx/dy/rotate attributes into all its parents. */
338 static void insert_into_spstring(SPString *string_item, Glib::ustring::iterator iter_at, gchar const *utf8)
339 {
340 unsigned char_index = 0;
341 unsigned char_count = g_utf8_strlen(utf8, -1);
342 Glib::ustring *string = &SP_STRING(string_item)->string;
344 for (Glib::ustring::iterator it = string->begin() ; it != iter_at ; it++)
345 char_index++;
346 string->replace(iter_at, iter_at, utf8);
348 SPObject *parent_item = string_item;
349 for ( ; ; ) {
350 char_index += sum_sibling_text_lengths_before(parent_item);
351 parent_item = SP_OBJECT_PARENT(parent_item);
352 TextTagAttributes *attributes = attributes_for_object(parent_item);
353 if (!attributes) break;
354 attributes->insert(char_index, char_count);
355 }
356 }
358 /** Inserts the given text into a text or flowroot object. Line breaks
359 cannot be inserted using this function, see sp_te_insert_line(). Returns
360 an iterator pointing just after the inserted text. */
361 Inkscape::Text::Layout::iterator
362 sp_te_insert(SPItem *item, Inkscape::Text::Layout::iterator const &position, gchar const *utf8)
363 {
364 if (!g_utf8_validate(utf8,-1,NULL)) {
365 g_warning("Trying to insert invalid utf8");
366 return position;
367 }
369 Inkscape::Text::Layout const *layout = te_get_layout(item);
370 SPObject *source_obj;
371 Glib::ustring::iterator iter_text;
372 // we want to insert after the previous char, not before the current char.
373 // it makes a difference at span boundaries
374 Inkscape::Text::Layout::iterator it_prev_char = position;
375 bool cursor_at_start = !it_prev_char.prevCharacter();
376 bool cursor_at_end = position == layout->end();
377 layout->getSourceOfCharacter(it_prev_char, (void**)&source_obj, &iter_text);
378 if (SP_IS_STRING(source_obj)) {
379 // the simple case
380 if (!cursor_at_start) iter_text++;
381 SPString *string_item = SP_STRING(source_obj);
382 insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : iter_text, utf8);
383 } else {
384 // the not-so-simple case where we're at a line break or other control char; add to the next child/sibling SPString
385 if (cursor_at_start) {
386 source_obj = item;
387 if (source_obj->hasChildren()) {
388 source_obj = source_obj->firstChild();
389 if (SP_IS_FLOWTEXT(item)) {
390 while (SP_IS_FLOWREGION(source_obj) || SP_IS_FLOWREGIONEXCLUDE(source_obj))
391 source_obj = SP_OBJECT_NEXT(source_obj);
392 if (source_obj == NULL)
393 source_obj = item;
394 }
395 }
396 if (source_obj == item && SP_IS_FLOWTEXT(item)) {
397 Inkscape::XML::Node *para = sp_repr_new("svg:flowPara");
398 SP_OBJECT_REPR(item)->appendChild(para);
399 source_obj = item->lastChild();
400 }
401 } else
402 source_obj = SP_OBJECT_NEXT(source_obj);
404 if (source_obj) { // never fails
405 SPString *string_item = sp_te_seek_next_string_recursive(source_obj);
406 if (string_item == NULL) {
407 // need to add an SPString in this (pathological) case
408 Inkscape::XML::Node *rstring = sp_repr_new_text("");
409 SP_OBJECT_REPR(source_obj)->addChild(rstring, NULL);
410 Inkscape::GC::release(rstring);
411 g_assert(SP_IS_STRING(source_obj->firstChild()));
412 string_item = SP_STRING(source_obj->firstChild());
413 }
414 insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : string_item->string.begin(), utf8);
415 }
416 }
418 item->updateRepr(SP_OBJECT_REPR(item),SP_OBJECT_WRITE_EXT);
419 unsigned char_index = layout->iteratorToCharIndex(position);
420 te_update_layout_now(item);
421 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
422 return layout->charIndexToIterator(char_index + g_utf8_strlen(utf8, -1));
423 }
426 /* ***************************************************************************************************/
427 // D E L E T I N G T E X T
429 /** moves all the children of \a from_repr to \a to_repr, either before
430 the existing children or after them. Order is maintained. The empty
431 \a from_repr is not deleted. */
432 static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend = false)
433 {
434 while (from_repr->childCount()) {
435 Inkscape::XML::Node *child = prepend ? from_repr->lastChild() : from_repr->firstChild();
436 Inkscape::GC::anchor(child);
437 from_repr->removeChild(child);
438 if (prepend) to_repr->addChild(child, NULL);
439 else to_repr->appendChild(child);
440 Inkscape::GC::release(child);
441 }
442 }
444 /** returns the object in the tree which is the closest ancestor of both
445 \a one and \a two. It will never return anything higher than \a text. */
446 static SPObject* get_common_ancestor(SPObject *text, SPObject *one, SPObject *two)
447 {
448 if (one == NULL || two == NULL)
449 return text;
450 SPObject *common_ancestor = one;
451 if (SP_IS_STRING(common_ancestor))
452 common_ancestor = SP_OBJECT_PARENT(common_ancestor);
453 while (!(common_ancestor == two || common_ancestor->isAncestorOf(two))) {
454 g_assert(common_ancestor != text);
455 common_ancestor = SP_OBJECT_PARENT(common_ancestor);
456 }
457 return common_ancestor;
458 }
460 /** positions \a para_obj and \a text_iter to be pointing at the end
461 of the last string in the last leaf object of \a para_obj. If the last
462 leaf is not an SPString then \a text_iter will be unchanged. */
463 static void move_to_end_of_paragraph(SPObject **para_obj, Glib::ustring::iterator *text_iter)
464 {
465 while ((*para_obj)->hasChildren())
466 *para_obj = (*para_obj)->lastChild();
467 if (SP_IS_STRING(*para_obj))
468 *text_iter = SP_STRING(*para_obj)->string.end();
469 }
471 /** delete the line break pointed to by \a item by merging its children into
472 the next suitable object and deleting \a item. Returns the object after the
473 ones that have just been moved and sets \a next_is_sibling accordingly. */
474 static SPObject* delete_line_break(SPObject *root, SPObject *item, bool *next_is_sibling)
475 {
476 Inkscape::XML::Node *this_repr = SP_OBJECT_REPR(item);
477 SPObject *next_item = NULL;
478 unsigned moved_char_count = sp_text_get_length(item) - 1; // the -1 is because it's going to count the line break
480 /* some sample cases (the div is the item to be deleted, the * represents where to put the new span):
481 <div></div><p>*text</p>
482 <p><div></div>*text</p>
483 <p><div></div></p><p>*text</p>
484 */
485 Inkscape::XML::Node *new_span_repr = sp_repr_new(span_name_for_text_object(root));
487 if (gchar const *a = this_repr->attribute("dx"))
488 new_span_repr->setAttribute("dx", a);
489 if (gchar const *a = this_repr->attribute("dy"))
490 new_span_repr->setAttribute("dy", a);
491 if (gchar const *a = this_repr->attribute("rotate"))
492 new_span_repr->setAttribute("rotate", a);
494 SPObject *following_item = item;
495 while (SP_OBJECT_NEXT(following_item) == NULL) {
496 following_item = SP_OBJECT_PARENT(following_item);
497 g_assert(following_item != root);
498 }
499 following_item = SP_OBJECT_NEXT(following_item);
501 SPObject *new_parent_item;
502 if (SP_IS_STRING(following_item)) {
503 new_parent_item = SP_OBJECT_PARENT(following_item);
504 SP_OBJECT_REPR(new_parent_item)->addChild(new_span_repr, SP_OBJECT_PREV(following_item) ? SP_OBJECT_REPR(SP_OBJECT_PREV(following_item)) : NULL);
505 next_item = following_item;
506 *next_is_sibling = true;
507 } else {
508 new_parent_item = following_item;
509 next_item = new_parent_item->firstChild();
510 *next_is_sibling = true;
511 if (next_item == NULL) {
512 next_item = new_parent_item;
513 *next_is_sibling = false;
514 }
515 SP_OBJECT_REPR(new_parent_item)->addChild(new_span_repr, NULL);
516 }
518 // work around a bug in sp_style_write_difference() which causes the difference
519 // not to be written if the second param has a style set which the first does not
520 // by causing the first param to have everything set
521 SPCSSAttr *dest_node_attrs = sp_repr_css_attr(SP_OBJECT_REPR(new_parent_item), "style");
522 SPCSSAttr *this_node_attrs = sp_repr_css_attr(this_repr, "style");
523 SPCSSAttr *this_node_attrs_inherited = sp_repr_css_attr_inherited(this_repr, "style");
524 Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attrs = dest_node_attrs->attributeList();
525 for ( ; attrs ; attrs++) {
526 gchar const *key = g_quark_to_string(attrs->key);
527 gchar const *this_attr = this_node_attrs_inherited->attribute(key);
528 if ((this_attr == NULL || strcmp(attrs->value, this_attr)) && this_node_attrs->attribute(key) == NULL)
529 this_node_attrs->setAttribute(key, this_attr);
530 }
531 sp_repr_css_attr_unref(this_node_attrs_inherited);
532 sp_repr_css_attr_unref(this_node_attrs);
533 sp_repr_css_attr_unref(dest_node_attrs);
534 sp_repr_css_change(new_span_repr, this_node_attrs, "style");
536 TextTagAttributes *attributes = attributes_for_object(new_parent_item);
537 if (attributes)
538 attributes->insert(0, moved_char_count);
539 move_child_nodes(this_repr, new_span_repr);
540 this_repr->parent()->removeChild(this_repr);
541 return next_item;
542 }
544 /** erases the given characters from the given string and deletes the
545 corresponding x/y/dx/dy/rotate attributes from all its parents. */
546 static void erase_from_spstring(SPString *string_item, Glib::ustring::iterator iter_from, Glib::ustring::iterator iter_to)
547 {
548 unsigned char_index = 0;
549 unsigned char_count = 0;
550 Glib::ustring *string = &SP_STRING(string_item)->string;
552 for (Glib::ustring::iterator it = string->begin() ; it != iter_from ; it++)
553 char_index++;
554 for (Glib::ustring::iterator it = iter_from ; it != iter_to ; it++)
555 char_count++;
556 string->erase(iter_from, iter_to);
557 SP_OBJECT_REPR(string_item)->setContent(string->c_str());
559 SPObject *parent_item = string_item;
560 for ( ; ; ) {
561 char_index += sum_sibling_text_lengths_before(parent_item);
562 parent_item = SP_OBJECT_PARENT(parent_item);
563 TextTagAttributes *attributes = attributes_for_object(parent_item);
564 if (attributes == NULL) break;
566 attributes->erase(char_index, char_count);
567 attributes->writeTo(SP_OBJECT_REPR(parent_item));
568 }
569 }
571 /* Deletes the given characters from a text or flowroot object. This is
572 quite a complicated operation, partly due to the cleanup that is done if all
573 the text in a subobject has been deleted, and partly due to the difficulty
574 of figuring out what is a line break and how to delete one. Returns the
575 lesser of \a start and \a end, because that is where the cursor should be
576 put after the deletion is done. */
577 Inkscape::Text::Layout::iterator
578 sp_te_delete (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end)
579 {
580 if (start == end) return start;
581 Inkscape::Text::Layout::iterator first, last;
582 if (start < end) {
583 first = start;
584 last = end;
585 } else {
586 first = end;
587 last = start;
588 }
589 Inkscape::Text::Layout const *layout = te_get_layout(item);
590 SPObject *start_item, *end_item;
591 Glib::ustring::iterator start_text_iter, end_text_iter;
592 layout->getSourceOfCharacter(first, (void**)&start_item, &start_text_iter);
593 layout->getSourceOfCharacter(last, (void**)&end_item, &end_text_iter);
594 if (start_item == NULL)
595 return first; // start is at end of text
596 if (is_line_break_object(start_item))
597 move_to_end_of_paragraph(&start_item, &start_text_iter);
598 if (end_item == NULL) {
599 end_item = item->lastChild();
600 move_to_end_of_paragraph(&end_item, &end_text_iter);
601 }
602 else if (is_line_break_object(end_item))
603 move_to_end_of_paragraph(&end_item, &end_text_iter);
605 SPObject *common_ancestor = get_common_ancestor(item, start_item, end_item);
607 if (start_item == end_item) {
608 // the quick case where we're deleting stuff all from the same string
609 if (SP_IS_STRING(start_item)) { // always true (if it_start != it_end anyway)
610 erase_from_spstring(SP_STRING(start_item), start_text_iter, end_text_iter);
611 }
612 } else {
613 SPObject *sub_item = start_item;
614 // walk the tree from start_item to end_item, deleting as we go
615 while (sub_item != item) {
616 if (sub_item == end_item) {
617 if (SP_IS_STRING(sub_item)) {
618 Glib::ustring *string = &SP_STRING(sub_item)->string;
619 erase_from_spstring(SP_STRING(sub_item), string->begin(), end_text_iter);
620 }
621 break;
622 }
623 if (SP_IS_STRING(sub_item)) {
624 SPString *string = SP_STRING(sub_item);
625 if (sub_item == start_item)
626 erase_from_spstring(string, start_text_iter, string->string.end());
627 else
628 erase_from_spstring(string, string->string.begin(), string->string.end());
629 }
630 // walk to the next item in the tree
631 if (sub_item->hasChildren())
632 sub_item = sub_item->firstChild();
633 else {
634 SPObject *next_item;
635 do {
636 bool is_sibling = true;
637 next_item = SP_OBJECT_NEXT(sub_item);
638 if (next_item == NULL) {
639 next_item = SP_OBJECT_PARENT(sub_item);
640 is_sibling = false;
641 }
643 if (is_line_break_object(sub_item))
644 next_item = delete_line_break(item, sub_item, &is_sibling);
646 sub_item = next_item;
647 if (is_sibling) break;
648 // no more siblings, go up a parent
649 } while (sub_item != item && sub_item != end_item);
650 }
651 }
652 }
654 while (tidy_xml_tree_recursively(common_ancestor));
655 te_update_layout_now(item);
656 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
657 layout->validateIterator(&first);
658 return first;
659 }
662 /* ***************************************************************************************************/
663 // P L A I N T E X T F U N C T I O N S
665 /** Gets a text-only representation of the given text or flowroot object,
666 replacing line break elements with '\n'. */
667 static void sp_te_get_ustring_multiline(SPObject const *root, Glib::ustring *string, bool *pending_line_break)
668 {
669 if (*pending_line_break)
670 *string += '\n';
671 for (SPObject const *child = root->firstChild() ; child ; child = SP_OBJECT_NEXT(child)) {
672 if (SP_IS_STRING(child))
673 *string += SP_STRING(child)->string;
674 else
675 sp_te_get_ustring_multiline(child, string, pending_line_break);
676 }
677 if (!SP_IS_TEXT(root) && !SP_IS_TEXTPATH(root) && is_line_break_object(root))
678 *pending_line_break = true;
679 }
681 /** Gets a text-only representation of the given text or flowroot object,
682 replacing line break elements with '\n'. The return value must be free()d. */
683 gchar *
684 sp_te_get_string_multiline (SPItem const *text)
685 {
686 Glib::ustring string;
687 bool pending_line_break = false;
689 if (!SP_IS_TEXT(text) && !SP_IS_FLOWTEXT(text)) return NULL;
690 sp_te_get_ustring_multiline(text, &string, &pending_line_break);
691 if (string.empty()) return NULL;
692 return strdup(string.data());
693 }
695 /** Gets a text-only representation of the characters in a text or flowroot
696 object from \a start to \a end only. Line break elements are replaced with
697 '\n'. */
698 Glib::ustring
699 sp_te_get_string_multiline (SPItem const *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end)
700 {
701 if (start == end) return "";
702 Inkscape::Text::Layout::iterator first, last;
703 if (start < end) {
704 first = start;
705 last = end;
706 } else {
707 first = end;
708 last = start;
709 }
710 Inkscape::Text::Layout const *layout = te_get_layout(text);
711 Glib::ustring result;
712 // not a particularly fast piece of code. I'll optimise it if people start to notice.
713 for ( ; first < last ; first.nextCharacter()) {
714 SPObject *char_item;
715 Glib::ustring::iterator text_iter;
716 layout->getSourceOfCharacter(first, (void**)&char_item, &text_iter);
717 if (SP_IS_STRING(char_item))
718 result += *text_iter;
719 else
720 result += '\n';
721 }
722 return result;
723 }
725 void
726 sp_te_set_repr_text_multiline(SPItem *text, gchar const *str)
727 {
728 g_return_if_fail (text != NULL);
729 g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
731 Inkscape::XML::Node *repr;
732 SPObject *object;
733 bool is_textpath = false;
734 if (SP_IS_TEXT_TEXTPATH (text)) {
735 repr = SP_OBJECT_REPR (sp_object_first_child(SP_OBJECT (text)));
736 object = sp_object_first_child(SP_OBJECT (text));
737 is_textpath = true;
738 } else {
739 repr = SP_OBJECT_REPR (text);
740 object = SP_OBJECT (text);
741 }
743 if (!str) str = "";
744 gchar *content = g_strdup (str);
746 repr->setContent("");
747 SPObject *child = object->firstChild();
748 while (child) {
749 SPObject *next = SP_OBJECT_NEXT(child);
750 if (!SP_IS_FLOWREGION(child) && !SP_IS_FLOWREGIONEXCLUDE(child))
751 repr->removeChild(SP_OBJECT_REPR(child));
752 child = next;
753 }
755 gchar *p = content;
756 while (p) {
757 gchar *e = strchr (p, '\n');
758 if (is_textpath) {
759 if (e) *e = ' '; // no lines for textpath, replace newlines with spaces
760 } else {
761 if (e) *e = '\0';
762 Inkscape::XML::Node *rtspan;
763 if (SP_IS_TEXT(text)) { // create a tspan for each line
764 rtspan = sp_repr_new ("svg:tspan");
765 rtspan->setAttribute("sodipodi:role", "line");
766 } else { // create a flowPara for each line
767 rtspan = sp_repr_new ("svg:flowPara");
768 }
769 Inkscape::XML::Node *rstr = sp_repr_new_text(p);
770 rtspan->addChild(rstr, NULL);
771 Inkscape::GC::release(rstr);
772 repr->appendChild(rtspan);
773 Inkscape::GC::release(rtspan);
774 }
775 p = (e) ? e + 1 : NULL;
776 }
777 if (is_textpath) {
778 Inkscape::XML::Node *rstr = sp_repr_new_text(content);
779 repr->addChild(rstr, NULL);
780 Inkscape::GC::release(rstr);
781 }
783 g_free (content);
784 }
786 /* ***************************************************************************************************/
787 // K E R N I N G A N D S P A C I N G
789 /** Returns the attributes block and the character index within that block
790 which represents the iterator \a position. */
791 static TextTagAttributes*
792 text_tag_attributes_at_position(SPItem *item, Inkscape::Text::Layout::iterator const &position, unsigned *char_index)
793 {
794 if (item == NULL || char_index == NULL || !SP_IS_TEXT(item))
795 return NULL; // flowtext doesn't support kerning yet
796 SPText *text = SP_TEXT(item);
798 SPObject *source_item;
799 Glib::ustring::iterator source_text_iter;
800 text->layout.getSourceOfCharacter(position, (void**)&source_item, &source_text_iter);
802 if (!SP_IS_STRING(source_item)) return NULL;
803 Glib::ustring *string = &SP_STRING(source_item)->string;
804 *char_index = sum_sibling_text_lengths_before(source_item);
805 for (Glib::ustring::iterator it = string->begin() ; it != source_text_iter ; it++)
806 ++*char_index;
808 return attributes_for_object(SP_OBJECT_PARENT(source_item));
809 }
811 void
812 sp_te_adjust_kerning_screen (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, NR::Point by)
813 {
814 // divide increment by zoom
815 // divide increment by matrix expansion
816 gdouble factor = 1 / desktop->current_zoom();
817 NR::Matrix t = sp_item_i2doc_affine(item);
818 factor = factor / NR::expansion(t);
819 by = factor * by;
821 unsigned char_index;
822 TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index);
823 if (attributes) attributes->addToDxDy(char_index, by);
824 if (start != end) {
825 attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index);
826 if (attributes) attributes->addToDxDy(char_index, -by);
827 }
829 item->updateRepr();
830 item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
831 }
833 void
834 sp_te_adjust_rotation_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble pixels)
835 {
836 // divide increment by zoom
837 // divide increment by matrix expansion
838 gdouble factor = 1 / desktop->current_zoom();
839 NR::Matrix t = sp_item_i2doc_affine(text);
840 factor = factor / NR::expansion(t);
841 SPObject *source_item;
842 Inkscape::Text::Layout const *layout = te_get_layout(text);
843 if (layout == NULL) return;
844 layout->getSourceOfCharacter(std::min(start, end), (void**)&source_item);
845 if (source_item == NULL) return;
846 gdouble degrees = (180/M_PI) * atan2(pixels, SP_OBJECT_PARENT(source_item)->style->font_size.computed / factor);
848 sp_te_adjust_rotation(text, start, end, desktop, degrees);
849 }
851 void
852 sp_te_adjust_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble degrees)
853 {
854 unsigned char_index;
855 TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index);
856 if (attributes == NULL) return;
858 if (start != end) {
859 for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) {
860 attributes = text_tag_attributes_at_position(text, it, &char_index);
861 if (attributes) attributes->addToRotate(char_index, degrees);
862 }
863 } else
864 attributes->addToRotate(char_index, degrees);
866 text->updateRepr();
867 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
868 }
870 void
871 sp_te_adjust_tspan_letterspacing_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble by)
872 {
873 g_return_if_fail (text != NULL);
874 g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
876 Inkscape::Text::Layout const *layout = te_get_layout(text);
878 gdouble val;
879 SPObject *source_obj;
880 unsigned nb_let;
881 layout->getSourceOfCharacter(std::min(start, end), (void**)&source_obj);
883 if (source_obj == NULL) { // end of text
884 source_obj = text->lastChild();
885 }
886 if (SP_IS_STRING(source_obj)) {
887 source_obj = source_obj->parent;
888 }
890 SPStyle *style = SP_OBJECT_STYLE (source_obj);
892 // calculate real value
893 /* TODO: Consider calculating val unconditionally, i.e. drop the first `if' line, and
894 get rid of the `else val = 0.0'. Similarly below and in sp-string.cpp. */
895 if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
896 if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
897 val = style->font_size.computed * style->letter_spacing.value;
898 } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
899 val = style->font_size.computed * style->letter_spacing.value * 0.5;
900 } else { // unknown unit - should not happen
901 val = 0.0;
902 }
903 } else { // there's a real value in .computed, or it's zero
904 val = style->letter_spacing.computed;
905 }
907 if (start == end) {
908 while (!is_line_break_object(source_obj)) // move up the tree so we apply to the closest paragraph
909 source_obj = SP_OBJECT_PARENT(source_obj);
910 nb_let = sp_text_get_length(source_obj);
911 } else {
912 nb_let = abs(layout->iteratorToCharIndex(end) - layout->iteratorToCharIndex(start));
913 }
915 // divide increment by zoom and by the number of characters in the line,
916 // so that the entire line is expanded by by pixels, no matter what its length
917 gdouble const zoom = desktop->current_zoom();
918 gdouble const zby = (by
919 / (zoom * (nb_let > 1 ? nb_let - 1 : 1))
920 / NR::expansion(sp_item_i2doc_affine(SP_ITEM(source_obj))));
921 val += zby;
923 if (start == end) {
924 // set back value to entire paragraph
925 style->letter_spacing.normal = FALSE;
926 if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
927 if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
928 style->letter_spacing.value = val / style->font_size.computed;
929 } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
930 style->letter_spacing.value = val / style->font_size.computed * 2;
931 }
932 } else {
933 style->letter_spacing.computed = val;
934 }
936 style->letter_spacing.set = TRUE;
937 } else {
938 // apply to selection only
939 SPCSSAttr *css = sp_repr_css_attr_new();
940 char string_val[40];
941 g_snprintf(string_val, sizeof(string_val), "%f", val);
942 sp_repr_css_set_property(css, "letter-spacing", string_val);
943 sp_te_apply_style(text, start, end, css);
944 sp_repr_css_attr_unref(css);
945 }
947 text->updateRepr();
948 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
949 }
951 void
952 sp_te_adjust_linespacing_screen (SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble by)
953 {
954 // TODO: use start and end iterators to delineate the area to be affected
955 g_return_if_fail (text != NULL);
956 g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
958 Inkscape::Text::Layout const *layout = te_get_layout(text);
959 SPStyle *style = SP_OBJECT_STYLE (text);
961 if (!style->line_height.set || style->line_height.inherit || style->line_height.normal) {
962 style->line_height.set = TRUE;
963 style->line_height.inherit = FALSE;
964 style->line_height.normal = FALSE;
965 style->line_height.unit = SP_CSS_UNIT_PERCENT;
966 style->line_height.value = style->line_height.computed = Inkscape::Text::Layout::LINE_HEIGHT_NORMAL;
967 }
969 unsigned line_count = layout->lineIndex(layout->end());
970 double all_lines_height = layout->characterAnchorPoint(layout->end())[NR::Y] - layout->characterAnchorPoint(layout->begin())[NR::Y];
971 double average_line_height = all_lines_height / (line_count == 0 ? 1 : line_count);
972 if (fabs(average_line_height) < 0.001) average_line_height = 0.001;
974 // divide increment by zoom and by the number of lines,
975 // so that the entire object is expanded by by pixels
976 gdouble zby = by / (desktop->current_zoom() * (line_count == 0 ? 1 : line_count));
978 // divide increment by matrix expansion
979 NR::Matrix t = sp_item_i2doc_affine (SP_ITEM(text));
980 zby = zby / NR::expansion(t);
982 switch (style->line_height.unit) {
983 case SP_CSS_UNIT_NONE:
984 default:
985 // multiplier-type units, stored in computed
986 if (fabs(style->line_height.computed) < 0.001) style->line_height.computed = by < 0.0 ? -0.001 : 0.001; // the formula below could get stuck at zero
987 else style->line_height.computed *= (average_line_height + zby) / average_line_height;
988 style->line_height.value = style->line_height.computed;
989 break;
990 case SP_CSS_UNIT_EM:
991 case SP_CSS_UNIT_EX:
992 case SP_CSS_UNIT_PERCENT:
993 // multiplier-type units, stored in value
994 if (fabs(style->line_height.value) < 0.001) style->line_height.value = by < 0.0 ? -0.001 : 0.001;
995 else style->line_height.value *= (average_line_height + zby) / average_line_height;
996 break;
997 // absolute-type units
998 case SP_CSS_UNIT_PX:
999 style->line_height.computed += zby;
1000 style->line_height.value = style->line_height.computed;
1001 break;
1002 case SP_CSS_UNIT_PT:
1003 style->line_height.computed += zby * PT_PER_PX;
1004 style->line_height.value = style->line_height.computed;
1005 break;
1006 case SP_CSS_UNIT_PC:
1007 style->line_height.computed += zby * (PT_PER_PX / 12);
1008 style->line_height.value = style->line_height.computed;
1009 break;
1010 case SP_CSS_UNIT_MM:
1011 style->line_height.computed += zby * MM_PER_PX;
1012 style->line_height.value = style->line_height.computed;
1013 break;
1014 case SP_CSS_UNIT_CM:
1015 style->line_height.computed += zby * CM_PER_PX;
1016 style->line_height.value = style->line_height.computed;
1017 break;
1018 case SP_CSS_UNIT_IN:
1019 style->line_height.computed += zby * IN_PER_PX;
1020 style->line_height.value = style->line_height.computed;
1021 break;
1022 }
1023 text->updateRepr();
1024 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
1025 }
1028 /* ***************************************************************************************************/
1029 // S T Y L E A P P L I C A T I O N
1032 /** converts an iterator to a character index, mainly because ustring::substr()
1033 doesn't have a version that takes iterators as parameters. */
1034 static unsigned char_index_of_iterator(Glib::ustring const &string, Glib::ustring::const_iterator text_iter)
1035 {
1036 unsigned n = 0;
1037 for (Glib::ustring::const_iterator it = string.begin() ; it != string.end() && it != text_iter ; it++)
1038 n++;
1039 return n;
1040 }
1042 /** applies the given style string on top of the existing styles for \a item,
1043 as opposed to sp_style_merge_from_style_string which merges its parameter
1044 underneath the existing styles (ie ignoring already set properties). */
1045 static void overwrite_style_with_string(SPObject *item, gchar const *style_string)
1046 {
1047 SPStyle *new_style = sp_style_new();
1048 sp_style_merge_from_style_string(new_style, style_string);
1049 gchar const *item_style_string = SP_OBJECT_REPR(item)->attribute("style");
1050 if (item_style_string && *item_style_string)
1051 sp_style_merge_from_style_string(new_style, item_style_string);
1052 gchar *new_style_string = sp_style_write_string(new_style);
1053 sp_style_unref(new_style);
1054 SP_OBJECT_REPR(item)->setAttribute("style", new_style_string && *new_style_string ? new_style_string : NULL);
1055 g_free(new_style_string);
1056 }
1058 /** Returns true if the style of \a parent and the style of \a child are
1059 equivalent (and hence the children of both will appear the same). It is a
1060 limitation of the current implementation that \a parent must be a (not
1061 necessarily immediate) ancestor of \a child. */
1062 static bool objects_have_equal_style(SPObject const *parent, SPObject const *child)
1063 {
1064 // the obvious implementation of strcmp(style_write_all(parent), style_write_all(child))
1065 // will not work. Firstly because of an inheritance bug in style.cpp that has
1066 // implications too large for me to feel safe fixing, but mainly because the css spec
1067 // requires that the computed value is inherited, not the specified value.
1068 g_assert(parent->isAncestorOf(child));
1069 gchar *parent_style = sp_style_write_string(parent->style, SP_STYLE_FLAG_ALWAYS);
1070 // we have to write parent_style then read it again, because some properties format their values
1071 // differently depending on whether they're set or not (*cough*dash-offset*cough*)
1072 SPStyle *parent_spstyle = sp_style_new();
1073 sp_style_merge_from_style_string(parent_spstyle, parent_style);
1074 g_free(parent_style);
1075 parent_style = sp_style_write_string(parent_spstyle, SP_STYLE_FLAG_ALWAYS);
1076 sp_style_unref(parent_spstyle);
1078 Glib::ustring child_style_construction(parent_style);
1079 while (child != parent) {
1080 // FIXME: this assumes that child's style is only in style= whereas it can also be in css attributes!
1081 char const *style_text = SP_OBJECT_REPR(child)->attribute("style");
1082 if (style_text && *style_text) {
1083 child_style_construction += ';';
1084 child_style_construction += style_text;
1085 }
1086 child = SP_OBJECT_PARENT(child);
1087 }
1088 SPStyle *child_spstyle = sp_style_new();
1089 sp_style_merge_from_style_string(child_spstyle, child_style_construction.c_str());
1090 gchar *child_style = sp_style_write_string(child_spstyle, SP_STYLE_FLAG_ALWAYS);
1091 sp_style_unref(child_spstyle);
1092 bool equal = !strcmp(child_style, parent_style);
1093 g_free(child_style);
1094 g_free(parent_style);
1095 return equal;
1096 }
1098 /** returns true if \a first and \a second contain all the same attributes
1099 with the same values as each other. Note that we have to compare both
1100 forwards and backwards to make sure we don't miss any attributes that are
1101 in one but not the other. */
1102 static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second)
1103 {
1104 Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attrs = first->attributeList();
1105 for ( ; attrs ; attrs++) {
1106 gchar const *other_attr = second->attribute(g_quark_to_string(attrs->key));
1107 if (other_attr == NULL || strcmp(attrs->value, other_attr))
1108 return false;
1109 }
1110 attrs = second->attributeList();
1111 for ( ; attrs ; attrs++) {
1112 gchar const *other_attr = first->attribute(g_quark_to_string(attrs->key));
1113 if (other_attr == NULL || strcmp(attrs->value, other_attr))
1114 return false;
1115 }
1116 return true;
1117 }
1119 /** sets the given css attribute on this object and all its descendants.
1120 Annoyingly similar to sp_desktop_apply_css_recursive(), except without the
1121 transform stuff. */
1122 static void apply_css_recursive(SPObject *o, SPCSSAttr const *css)
1123 {
1124 sp_repr_css_change(SP_OBJECT_REPR(o), const_cast<SPCSSAttr*>(css), "style");
1126 for (SPObject *child = sp_object_first_child(SP_OBJECT(o)) ; child != NULL ; child = SP_OBJECT_NEXT(child) ) {
1127 if (sp_repr_css_property(const_cast<SPCSSAttr*>(css), "opacity", NULL) != NULL) {
1128 // Unset properties which are accumulating and thus should not be set recursively.
1129 // 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.
1130 SPCSSAttr *css_recurse = sp_repr_css_attr_new();
1131 sp_repr_css_merge(css_recurse, const_cast<SPCSSAttr*>(css));
1132 sp_repr_css_set_property(css_recurse, "opacity", NULL);
1133 apply_css_recursive(child, css_recurse);
1134 sp_repr_css_attr_unref(css_recurse);
1135 } else {
1136 apply_css_recursive(child, const_cast<SPCSSAttr*>(css));
1137 }
1138 }
1139 }
1141 /** applies the given style to all the objects at the given level and below
1142 which are between \a start_item and \a end_item, creating spans as necessary.
1143 If \a start_item or \a end_item are NULL then the style is applied to all
1144 objects to the beginning or end respectively. \a span_object_name is the
1145 name of the xml for a text span (ie tspan or flowspan). */
1146 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)
1147 {
1148 bool passed_start = start_item == NULL ? true : false;
1150 for (SPObject *child = common_ancestor->firstChild() ; child != NULL ; child = SP_OBJECT_NEXT(child)) {
1151 if (start_item == child)
1152 passed_start = true;
1154 if (passed_start) {
1155 if (end_item && child->isAncestorOf(end_item)) {
1156 recursively_apply_style(child, css, NULL, start_text_iter, end_item, end_text_iter, span_object_name);
1157 break;
1158 }
1159 // apply style
1161 // note that when adding stuff we must make sure that 'child' stays valid so the for loop keeps working.
1162 // often this means that new spans are created before child and child is modified only
1163 if (SP_IS_STRING(child)) {
1164 SPString *string_item = SP_STRING(child);
1165 bool surround_entire_string = true;
1167 Inkscape::XML::Node *child_span = sp_repr_new(span_object_name);
1168 sp_repr_css_set(child_span, const_cast<SPCSSAttr*>(css), "style"); // better hope that prototype wasn't nonconst for a good reason
1169 SPObject *prev_item = SP_OBJECT_PREV(child);
1170 Inkscape::XML::Node *prev_repr = prev_item ? SP_OBJECT_REPR(prev_item) : NULL;
1172 if (child == start_item || child == end_item) {
1173 surround_entire_string = false;
1174 if (start_item == end_item && start_text_iter != string_item->string.begin()) {
1175 // eg "abcDEFghi" -> "abc"<span>"DEF"</span>"ghi"
1176 unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
1177 unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
1179 Inkscape::XML::Node *text_before = sp_repr_new_text(string_item->string.substr(0, start_char_index).c_str());
1180 SP_OBJECT_REPR(common_ancestor)->addChild(text_before, prev_repr);
1181 SP_OBJECT_REPR(common_ancestor)->addChild(child_span, text_before);
1182 Inkscape::GC::release(text_before);
1183 Inkscape::XML::Node *text_in_span = sp_repr_new_text(string_item->string.substr(start_char_index, end_char_index - start_char_index).c_str());
1184 child_span->appendChild(text_in_span);
1185 Inkscape::GC::release(text_in_span);
1186 SP_OBJECT_REPR(child)->setContent(string_item->string.substr(end_char_index).c_str());
1188 } else if (child == end_item) {
1189 // eg "ABCdef" -> <span>"ABC"</span>"def"
1190 // (includes case where start_text_iter == begin())
1191 // NB: we might create an empty string here. Doesn't matter, it'll get cleaned up later
1192 unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
1194 SP_OBJECT_REPR(common_ancestor)->addChild(child_span, prev_repr);
1195 Inkscape::XML::Node *text_in_span = sp_repr_new_text(string_item->string.substr(0, end_char_index).c_str());
1196 child_span->appendChild(text_in_span);
1197 Inkscape::GC::release(text_in_span);
1198 SP_OBJECT_REPR(child)->setContent(string_item->string.substr(end_char_index).c_str());
1200 } else if (start_text_iter != string_item->string.begin()) {
1201 // eg "abcDEF" -> "abc"<span>"DEF"</span>
1202 unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
1204 Inkscape::XML::Node *text_before = sp_repr_new_text(string_item->string.substr(0, start_char_index).c_str());
1205 SP_OBJECT_REPR(common_ancestor)->addChild(text_before, prev_repr);
1206 SP_OBJECT_REPR(common_ancestor)->addChild(child_span, text_before);
1207 Inkscape::GC::release(text_before);
1208 Inkscape::XML::Node *text_in_span = sp_repr_new_text(string_item->string.substr(start_char_index).c_str());
1209 child_span->appendChild(text_in_span);
1210 Inkscape::GC::release(text_in_span);
1211 child->deleteObject();
1212 child = sp_object_get_child_by_repr(common_ancestor, child_span);
1214 } else
1215 surround_entire_string = true;
1216 }
1217 if (surround_entire_string) {
1218 Inkscape::XML::Node *child_repr = SP_OBJECT_REPR(child);
1219 SP_OBJECT_REPR(common_ancestor)->addChild(child_span, child_repr);
1220 Inkscape::GC::anchor(child_repr);
1221 SP_OBJECT_REPR(common_ancestor)->removeChild(child_repr);
1222 child_span->appendChild(child_repr);
1223 Inkscape::GC::release(child_repr);
1224 child = sp_object_get_child_by_repr(common_ancestor, child_span);
1225 }
1226 Inkscape::GC::release(child_span);
1228 } else if (child != end_item) { // not a string and we're applying to the entire object. This is easy
1229 apply_css_recursive(child, css);
1230 }
1232 } else { // !passed_start
1233 if (child->isAncestorOf(start_item)) {
1234 recursively_apply_style(child, css, start_item, start_text_iter, end_item, end_text_iter, span_object_name);
1235 if (end_item && child->isAncestorOf(end_item))
1236 break; // only happens when start_item == end_item (I think)
1237 passed_start = true;
1238 }
1239 }
1241 if (end_item == child)
1242 break;
1243 }
1244 }
1246 /* if item is at the beginning of a tree it doesn't matter which element
1247 it points to so for neatness we would like it to point to the highest
1248 possible child of \a common_ancestor. There is no iterator return because
1249 a string can never be an ancestor.
1251 eg: <span><span>*ABC</span>DEFghi</span> where * is the \a item. We would
1252 like * to point to the inner span because we can apply style to that whole
1253 span. */
1254 static SPObject* ascend_while_first(SPObject *item, Glib::ustring::iterator text_iter, SPObject *common_ancestor)
1255 {
1256 if (item == common_ancestor)
1257 return item;
1258 if (SP_IS_STRING(item))
1259 if (text_iter != SP_STRING(item)->string.begin())
1260 return item;
1261 for ( ; ; ) {
1262 SPObject *parent = SP_OBJECT_PARENT(item);
1263 if (parent == common_ancestor)
1264 break;
1265 if (item != parent->firstChild())
1266 break;
1267 item = parent;
1268 }
1269 return item;
1270 }
1273 /** empty spans: abc<span></span>def
1274 -> abcdef */
1275 static bool tidy_operator_empty_spans(SPObject **item)
1276 {
1277 if ((*item)->hasChildren()) return false;
1278 if (is_line_break_object(*item)) return false;
1279 if (SP_IS_STRING(*item) && !SP_STRING(*item)->string.empty()) return false;
1280 SPObject *next = SP_OBJECT_NEXT(*item);
1281 (*item)->deleteObject();
1282 *item = next;
1283 return true;
1284 }
1286 /** inexplicable spans: abc<span style="">def</span>ghi
1287 -> "abc""def""ghi"
1288 the repeated strings will be merged by another operator. */
1289 static bool tidy_operator_inexplicable_spans(SPObject **item)
1290 {
1291 if (SP_IS_STRING(*item)) return false;
1292 if (is_line_break_object(*item)) return false;
1293 TextTagAttributes *attrs = attributes_for_object(*item);
1294 if (attrs && attrs->anyAttributesSet()) return false;
1295 if (!objects_have_equal_style(SP_OBJECT_PARENT(*item), *item)) return false;
1296 SPObject *next = *item;
1297 while ((*item)->hasChildren()) {
1298 Inkscape::XML::Node *repr = SP_OBJECT_REPR((*item)->firstChild());
1299 Inkscape::GC::anchor(repr);
1300 SP_OBJECT_REPR(*item)->removeChild(repr);
1301 SP_OBJECT_REPR(SP_OBJECT_PARENT(*item))->addChild(repr, SP_OBJECT_REPR(next));
1302 Inkscape::GC::release(repr);
1303 next = SP_OBJECT_NEXT(next);
1304 }
1305 (*item)->deleteObject();
1306 *item = next;
1307 return true;
1308 }
1310 /** repeated spans: <font a>abc</font><font a>def</font>
1311 -> <font a>abcdef</font> */
1312 static bool tidy_operator_repeated_spans(SPObject **item)
1313 {
1314 SPObject *first = *item;
1315 SPObject *second = SP_OBJECT_NEXT(first);
1316 if (second == NULL) return false;
1318 Inkscape::XML::Node *first_repr = SP_OBJECT_REPR(first);
1319 Inkscape::XML::Node *second_repr = SP_OBJECT_REPR(second);
1321 if (first_repr->type() != second_repr->type()) return false;
1323 if (SP_IS_STRING(first) && SP_IS_STRING(second)) {
1324 // also amalgamate consecutive SPStrings into one
1325 Glib::ustring merged_string = SP_STRING(first)->string + SP_STRING(second)->string;
1326 SP_OBJECT_REPR(first)->setContent(merged_string.c_str());
1327 second_repr->parent()->removeChild(second_repr);
1328 return true;
1329 }
1331 // merge consecutive spans with identical styles into one
1332 if (first_repr->type() != Inkscape::XML::ELEMENT_NODE) return false;
1333 if (strcmp(first_repr->name(), second_repr->name()) != 0) return false;
1334 if (is_line_break_object(second)) return false;
1335 gchar const *first_style = first_repr->attribute("style");
1336 gchar const *second_style = second_repr->attribute("style");
1337 if (!((first_style == NULL && second_style == NULL)
1338 || (first_style != NULL && second_style != NULL && !strcmp(first_style, second_style))))
1339 return false;
1341 // all our tests passed: do the merge
1342 TextTagAttributes *attributes_first = attributes_for_object(first);
1343 TextTagAttributes *attributes_second = attributes_for_object(second);
1344 if (attributes_first && attributes_second && attributes_second->anyAttributesSet()) {
1345 TextTagAttributes attributes_first_copy = *attributes_first;
1346 attributes_first->join(attributes_first_copy, *attributes_second, sp_text_get_length(first));
1347 }
1348 move_child_nodes(second_repr, first_repr);
1349 second_repr->parent()->removeChild(second_repr);
1350 return true;
1351 // *item is still the next object to process
1352 }
1354 /** redundant nesting: <font a><font b>abc</font></font>
1355 -> <font b>abc</font>
1356 excessive nesting: <font a><size 1>abc</size></font>
1357 -> <font a,size 1>abc</font> */
1358 static bool tidy_operator_excessive_nesting(SPObject **item)
1359 {
1360 if (!(*item)->hasChildren()) return false;
1361 if ((*item)->firstChild() != (*item)->lastChild()) return false;
1362 if (SP_IS_FLOWREGION((*item)->firstChild()) || SP_IS_FLOWREGIONEXCLUDE((*item)->firstChild()))
1363 return false;
1364 if (SP_IS_STRING((*item)->firstChild())) return false;
1365 if (is_line_break_object((*item)->firstChild())) return false;
1366 TextTagAttributes *attrs = attributes_for_object((*item)->firstChild());
1367 if (attrs && attrs->anyAttributesSet()) return false;
1368 gchar const *child_style = SP_OBJECT_REPR((*item)->firstChild())->attribute("style");
1369 if (child_style && *child_style)
1370 overwrite_style_with_string(*item, child_style);
1371 move_child_nodes(SP_OBJECT_REPR((*item)->firstChild()), SP_OBJECT_REPR(*item));
1372 (*item)->firstChild()->deleteObject();
1373 return true;
1374 }
1376 /** helper for tidy_operator_redundant_double_nesting() */
1377 static bool redundant_double_nesting_processor(SPObject **item, SPObject *child, bool prepend)
1378 {
1379 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child))
1380 return false;
1381 if (SP_IS_STRING(child)) return false;
1382 if (is_line_break_object(child)) return false;
1383 if (is_line_break_object(*item)) return false;
1384 TextTagAttributes *attrs = attributes_for_object(child);
1385 if (attrs && attrs->anyAttributesSet()) return false;
1386 if (!objects_have_equal_style(SP_OBJECT_PARENT(*item), child)) return false;
1388 Inkscape::XML::Node *insert_after_repr;
1389 if (prepend) insert_after_repr = SP_OBJECT_REPR(SP_OBJECT_PREV(*item));
1390 else insert_after_repr = SP_OBJECT_REPR(*item);
1391 while (SP_OBJECT_REPR(child)->childCount()) {
1392 Inkscape::XML::Node *move_repr = SP_OBJECT_REPR(child)->firstChild();
1393 Inkscape::GC::anchor(move_repr);
1394 SP_OBJECT_REPR(child)->removeChild(move_repr);
1395 SP_OBJECT_REPR(SP_OBJECT_PARENT(*item))->addChild(move_repr, insert_after_repr);
1396 Inkscape::GC::release(move_repr);
1397 insert_after_repr = move_repr; // I think this will stay valid long enough. It's garbage collected these days.
1398 }
1399 child->deleteObject();
1400 return true;
1401 }
1403 /** redundant double nesting: <font b><font a><font b>abc</font>def</font>ghi</font>
1404 -> <font b>abc<font a>def</font>ghi</font>
1405 this function does its work when the parameter is the <font a> tag in the
1406 example. You may note that this only does its work when the doubly-nested
1407 child is the first or last. The other cases are called 'style inversion'
1408 below, and I'm not yet convinced that the result of that operation will be
1409 tidier in all cases. */
1410 static bool tidy_operator_redundant_double_nesting(SPObject **item)
1411 {
1412 if (!(*item)->hasChildren()) return false;
1413 if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is excessive nesting, done above
1414 if (redundant_double_nesting_processor(item, (*item)->firstChild(), true))
1415 return true;
1416 if (redundant_double_nesting_processor(item, (*item)->lastChild(), false))
1417 return true;
1418 return false;
1419 }
1421 /** helper for tidy_operator_redundant_semi_nesting(). Checks a few things,
1422 then compares the styles for item+child versus just child. If they're equal,
1423 tidying is possible. */
1424 static bool redundant_semi_nesting_processor(SPObject **item, SPObject *child, bool prepend)
1425 {
1426 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child))
1427 return false;
1428 if (SP_IS_STRING(child)) return false;
1429 if (is_line_break_object(child)) return false;
1430 if (is_line_break_object(*item)) return false;
1431 TextTagAttributes *attrs = attributes_for_object(child);
1432 if (attrs && attrs->anyAttributesSet()) return false;
1433 attrs = attributes_for_object(*item);
1434 if (attrs && attrs->anyAttributesSet()) return false;
1436 SPCSSAttr *css_child_and_item = sp_repr_css_attr_new();
1437 SPCSSAttr *css_child_only = sp_repr_css_attr_new();
1438 gchar const *child_style = SP_OBJECT_REPR(child)->attribute("style");
1439 if (child_style && *child_style) {
1440 sp_repr_css_attr_add_from_string(css_child_and_item, child_style);
1441 sp_repr_css_attr_add_from_string(css_child_only, child_style);
1442 }
1443 gchar const *item_style = SP_OBJECT_REPR(*item)->attribute("style");
1444 if (item_style && *item_style) {
1445 sp_repr_css_attr_add_from_string(css_child_and_item, item_style);
1446 }
1447 bool equal = css_attrs_are_equal(css_child_only, css_child_and_item);
1448 sp_repr_css_attr_unref(css_child_and_item);
1449 sp_repr_css_attr_unref(css_child_only);
1450 if (!equal) return false;
1452 Inkscape::XML::Node *new_span = sp_repr_new(SP_OBJECT_REPR(*item)->name());
1453 if (prepend) {
1454 SPObject *prev = SP_OBJECT_PREV(*item);
1455 SP_OBJECT_REPR(SP_OBJECT_PARENT(*item))->addChild(new_span, prev ? SP_OBJECT_REPR(prev) : NULL);
1456 } else
1457 SP_OBJECT_REPR(SP_OBJECT_PARENT(*item))->addChild(new_span, SP_OBJECT_REPR(*item));
1458 new_span->setAttribute("style", SP_OBJECT_REPR(child)->attribute("style"));
1459 move_child_nodes(SP_OBJECT_REPR(child), new_span);
1460 Inkscape::GC::release(new_span);
1461 child->deleteObject();
1462 return true;
1463 }
1465 /** redundant semi-nesting: <font a><font b>abc</font>def</font>
1466 -> <font b>abc</font><font>def</font>
1467 test this by applying a colour to a region, then a different colour to
1468 a partially-overlapping region. */
1469 static bool tidy_operator_redundant_semi_nesting(SPObject **item)
1470 {
1471 if (!(*item)->hasChildren()) return false;
1472 if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is redundant nesting, done above
1473 if (redundant_semi_nesting_processor(item, (*item)->firstChild(), true))
1474 return true;
1475 if (redundant_semi_nesting_processor(item, (*item)->lastChild(), false))
1476 return true;
1477 return false;
1478 }
1480 /** helper for tidy_operator_styled_whitespace(), finds the last string object
1481 in a paragraph which is not \a not_obj. */
1482 static SPString* find_last_string_child_not_equal_to(SPObject *root, SPObject *not_obj)
1483 {
1484 for (SPObject *child = root->lastChild() ; child ; child = SP_OBJECT_PREV(child))
1485 {
1486 if (child == not_obj) continue;
1487 if (child->hasChildren()) {
1488 SPString *ret = find_last_string_child_not_equal_to(child, not_obj);
1489 if (ret) return ret;
1490 } else if (SP_IS_STRING(child))
1491 return SP_STRING(child);
1492 }
1493 return NULL;
1494 }
1496 /** whitespace-only spans: abc<font> </font>def
1497 -> abc<font></font> def
1498 abc<b><i>def</i> </b>ghi
1499 -> abc<b><i>def</i></b> ghi */
1500 static bool tidy_operator_styled_whitespace(SPObject **item)
1501 {
1502 if (!SP_IS_STRING(*item)) return false;
1503 Glib::ustring const &str = SP_STRING(*item)->string;
1504 for (Glib::ustring::const_iterator it = str.begin() ; it != str.end() ; ++it)
1505 if (!g_unichar_isspace(*it)) return false;
1507 SPObject *test_item = *item;
1508 SPString *next_string;
1509 for ( ; ; ) { // find the next string
1510 next_string = sp_te_seek_next_string_recursive(SP_OBJECT_NEXT(test_item));
1511 if (next_string) {
1512 next_string->string.insert(0, str);
1513 break;
1514 }
1515 for ( ; ; ) { // go up one item in the xml
1516 test_item = SP_OBJECT_PARENT(test_item);
1517 if (is_line_break_object(test_item)) break;
1518 SPObject *next = SP_OBJECT_NEXT(test_item);
1519 if (next) {
1520 test_item = next;
1521 break;
1522 }
1523 }
1524 if (is_line_break_object(test_item)) { // no next string, see if there's a prev string
1525 next_string = find_last_string_child_not_equal_to(test_item, *item);
1526 if (next_string == NULL) return false; // an empty paragraph
1527 next_string->string += str;
1528 break;
1529 }
1530 }
1531 SP_OBJECT_REPR(next_string)->setContent(next_string->string.c_str());
1532 SPObject *delete_obj = *item;
1533 *item = SP_OBJECT_NEXT(*item);
1534 delete_obj->deleteObject();
1535 return true;
1536 }
1538 /* possible tidy operators that are not yet implemented, either because
1539 they are difficult, occur infrequently, or because I'm not sure that the
1540 output is tidier in all cases:
1541 duplicate styles in line break elements: <div italic><para italic>abc</para></div>
1542 -> <div italic><para>abc</para></div>
1543 style inversion: <font a>abc<font b>def<font a>ghi</font>jkl</font>mno</font>
1544 -> <font a>abc<font b>def</font>ghi<font b>jkl</font>mno</font>
1545 mistaken precedence: <font a,size 1>abc</font><size 1>def</size>
1546 -> <size 1><font a>abc</font>def</size>
1547 */
1549 /** Recursively walks the xml tree calling a set of cleanup operations on
1550 every child. Returns true if any changes were made to the tree.
1552 All the tidy operators return true if they made changes, and alter their
1553 parameter to point to the next object that should be processed, or NULL.
1554 They must not significantly alter (ie delete) any ancestor elements of the
1555 one they are passed.
1557 It may be that some of the later tidy operators that I wrote are actually
1558 general cases of the earlier operators, and hence the special-case-only
1559 versions can be removed. I haven't analysed my work in detail to figure
1560 out if this is so. */
1561 static bool tidy_xml_tree_recursively(SPObject *root)
1562 {
1563 static bool (* const tidy_operators[])(SPObject**) = {
1564 tidy_operator_empty_spans,
1565 tidy_operator_inexplicable_spans,
1566 tidy_operator_repeated_spans,
1567 tidy_operator_excessive_nesting,
1568 tidy_operator_redundant_double_nesting,
1569 tidy_operator_redundant_semi_nesting,
1570 tidy_operator_styled_whitespace
1571 };
1572 bool changes = false;
1574 for (SPObject *child = root->firstChild() ; child != NULL ; ) {
1575 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child)) {
1576 child = SP_OBJECT_NEXT(child);
1577 continue;
1578 }
1579 if (child->hasChildren())
1580 changes |= tidy_xml_tree_recursively(child);
1582 unsigned i;
1583 for (i = 0 ; i < sizeof(tidy_operators) / sizeof(tidy_operators[0]) ; i++) {
1584 if (tidy_operators[i](&child)) {
1585 changes = true;
1586 break;
1587 }
1588 }
1589 if (i == sizeof(tidy_operators) / sizeof(tidy_operators[0]))
1590 child = SP_OBJECT_NEXT(child);
1591 }
1592 return changes;
1593 }
1595 /** Applies the given CSS fragment to the characters of the given text or
1596 flowtext object between \a start and \a end, creating or removing span
1597 elements as necessary and optimal. */
1598 void sp_te_apply_style(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPCSSAttr const *css)
1599 {
1600 // in the comments in the code below, capital letters are inside the application region, lowercase are outside
1601 if (start == end) return;
1602 Inkscape::Text::Layout::iterator first, last;
1603 if (start < end) {
1604 first = start;
1605 last = end;
1606 } else {
1607 first = end;
1608 last = start;
1609 }
1610 Inkscape::Text::Layout const *layout = te_get_layout(text);
1611 SPObject *start_item, *end_item;
1612 Glib::ustring::iterator start_text_iter, end_text_iter;
1613 layout->getSourceOfCharacter(first, (void**)&start_item, &start_text_iter);
1614 layout->getSourceOfCharacter(last, (void**)&end_item, &end_text_iter);
1615 if (start_item == NULL)
1616 return; // start is at end of text
1617 if (is_line_break_object(start_item))
1618 start_item = SP_OBJECT_NEXT(start_item);
1619 if (is_line_break_object(end_item))
1620 end_item = SP_OBJECT_NEXT(end_item);
1621 if (end_item == NULL) end_item = text;
1623 /* stage 1: applying the style. Go up to the closest common ancestor of
1624 start and end and then semi-recursively apply the style to all the
1625 objects in between. The semi-recursion is because it's only necessary
1626 at the beginning and end; the style can just be applied to the root
1627 child in the middle.
1628 eg: <span>abcDEF</span><span>GHI</span><span>JKLmno</span>
1629 The recursion may involve creating new spans.
1630 */
1631 SPObject *common_ancestor = get_common_ancestor(text, start_item, end_item);
1632 start_item = ascend_while_first(start_item, start_text_iter, common_ancestor);
1633 end_item = ascend_while_first(end_item, end_text_iter, common_ancestor);
1634 recursively_apply_style(common_ancestor, css, start_item, start_text_iter, end_item, end_text_iter, span_name_for_text_object(text));
1636 /* stage 2: cleanup the xml tree (of which there are multiple passes) */
1637 /* discussion: this stage requires a certain level of inventiveness because
1638 it's not clear what the best representation is in many cases. An ideal
1639 implementation would provide some sort of scoring function to rate the
1640 ugliness of a given xml tree and try to reduce said function, but providing
1641 the various possibilities to be rated is non-trivial. Instead, I have opted
1642 for a multi-pass technique which simply recognises known-ugly patterns and
1643 has matching routines for optimising the patterns it finds. It's reasonably
1644 easy to add new pattern matching processors. If everything gets disastrous
1645 and neither option can be made to work, a fallback could be to reduce
1646 everything to a single level of nesting and drop all pretence of
1647 roundtrippability. */
1648 while (tidy_xml_tree_recursively(common_ancestor));
1650 // if we only modified subobjects this won't have been automatically sent
1651 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
1652 }
1654 /*
1655 Local Variables:
1656 mode:c++
1657 c-file-style:"stroustrup"
1658 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1659 indent-tabs-mode:nil
1660 fill-column:99
1661 End:
1662 */
1663 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :