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);
882 if (source_obj == NULL) // end of text
883 source_obj = text->lastChild();
884 else if (SP_IS_STRING(source_obj))
885 source_obj = source_obj->parent;
887 SPStyle *style = SP_OBJECT_STYLE (source_obj);
889 // calculate real value
890 /* TODO: Consider calculating val unconditionally, i.e. drop the first `if' line, and
891 get rid of the `else val = 0.0'. Similarly below and in sp-string.cpp. */
892 if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
893 if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
894 val = style->font_size.computed * style->letter_spacing.value;
895 } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
896 val = style->font_size.computed * style->letter_spacing.value * 0.5;
897 } else { // unknown unit - should not happen
898 val = 0.0;
899 }
900 } else { // there's a real value in .computed, or it's zero
901 val = style->letter_spacing.computed;
902 }
904 if (start == end) {
905 while (!is_line_break_object(source_obj)) // move up the tree so we apply to the closest paragraph
906 source_obj = SP_OBJECT_PARENT(source_obj);
907 nb_let = sp_text_get_length(source_obj);
908 } else {
909 nb_let = abs(layout->iteratorToCharIndex(end) - layout->iteratorToCharIndex(start));
910 }
912 // divide increment by zoom and by the number of characters in the line,
913 // so that the entire line is expanded by by pixels, no matter what its length
914 gdouble const zoom = desktop->current_zoom();
915 gdouble const zby = (by
916 / (zoom * (nb_let > 1 ? nb_let - 1 : 1))
917 / NR::expansion(sp_item_i2doc_affine(SP_ITEM(source_obj))));
918 val += zby;
920 if (start == end) {
921 // set back value to entire paragraph
922 style->letter_spacing.normal = FALSE;
923 if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex
924 if (style->letter_spacing.unit == SP_CSS_UNIT_EM) {
925 style->letter_spacing.value = val / style->font_size.computed;
926 } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) {
927 style->letter_spacing.value = val / style->font_size.computed * 2;
928 }
929 } else {
930 style->letter_spacing.computed = val;
931 }
933 style->letter_spacing.set = TRUE;
934 } else {
935 // apply to selection only
936 SPCSSAttr *css = sp_repr_css_attr_new();
937 char string_val[40];
938 g_snprintf(string_val, sizeof(string_val), "%f", val);
939 sp_repr_css_set_property(css, "letter-spacing", string_val);
940 sp_te_apply_style(text, start, end, css);
941 sp_repr_css_attr_unref(css);
942 }
944 text->updateRepr();
945 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
946 }
948 void
949 sp_te_adjust_linespacing_screen (SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble by)
950 {
951 // TODO: use start and end iterators to delineate the area to be affected
952 g_return_if_fail (text != NULL);
953 g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text));
955 Inkscape::Text::Layout const *layout = te_get_layout(text);
956 SPStyle *style = SP_OBJECT_STYLE (text);
958 if (!style->line_height.set || style->line_height.inherit || style->line_height.normal) {
959 style->line_height.set = TRUE;
960 style->line_height.inherit = FALSE;
961 style->line_height.normal = FALSE;
962 style->line_height.unit = SP_CSS_UNIT_PERCENT;
963 style->line_height.value = style->line_height.computed = Inkscape::Text::Layout::LINE_HEIGHT_NORMAL;
964 }
966 unsigned line_count = layout->lineIndex(layout->end());
967 double all_lines_height = layout->characterAnchorPoint(layout->end())[NR::Y] - layout->characterAnchorPoint(layout->begin())[NR::Y];
968 double average_line_height = all_lines_height / (line_count == 0 ? 1 : line_count);
969 if (fabs(average_line_height) < 0.001) average_line_height = 0.001;
971 // divide increment by zoom and by the number of lines,
972 // so that the entire object is expanded by by pixels
973 gdouble zby = by / (desktop->current_zoom() * (line_count == 0 ? 1 : line_count));
975 // divide increment by matrix expansion
976 NR::Matrix t = sp_item_i2doc_affine (SP_ITEM(text));
977 zby = zby / NR::expansion(t);
979 switch (style->line_height.unit) {
980 case SP_CSS_UNIT_NONE:
981 default:
982 // multiplier-type units, stored in computed
983 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
984 else style->line_height.computed *= (average_line_height + zby) / average_line_height;
985 style->line_height.value = style->line_height.computed;
986 break;
987 case SP_CSS_UNIT_EM:
988 case SP_CSS_UNIT_EX:
989 case SP_CSS_UNIT_PERCENT:
990 // multiplier-type units, stored in value
991 if (fabs(style->line_height.value) < 0.001) style->line_height.value = by < 0.0 ? -0.001 : 0.001;
992 else style->line_height.value *= (average_line_height + zby) / average_line_height;
993 break;
994 // absolute-type units
995 case SP_CSS_UNIT_PX:
996 style->line_height.computed += zby;
997 style->line_height.value = style->line_height.computed;
998 break;
999 case SP_CSS_UNIT_PT:
1000 style->line_height.computed += zby * PT_PER_PX;
1001 style->line_height.value = style->line_height.computed;
1002 break;
1003 case SP_CSS_UNIT_PC:
1004 style->line_height.computed += zby * (PT_PER_PX / 12);
1005 style->line_height.value = style->line_height.computed;
1006 break;
1007 case SP_CSS_UNIT_MM:
1008 style->line_height.computed += zby * MM_PER_PX;
1009 style->line_height.value = style->line_height.computed;
1010 break;
1011 case SP_CSS_UNIT_CM:
1012 style->line_height.computed += zby * CM_PER_PX;
1013 style->line_height.value = style->line_height.computed;
1014 break;
1015 case SP_CSS_UNIT_IN:
1016 style->line_height.computed += zby * IN_PER_PX;
1017 style->line_height.value = style->line_height.computed;
1018 break;
1019 }
1020 text->updateRepr();
1021 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
1022 }
1025 /* ***************************************************************************************************/
1026 // S T Y L E A P P L I C A T I O N
1029 /** converts an iterator to a character index, mainly because ustring::substr()
1030 doesn't have a version that takes iterators as parameters. */
1031 static unsigned char_index_of_iterator(Glib::ustring const &string, Glib::ustring::const_iterator text_iter)
1032 {
1033 unsigned n = 0;
1034 for (Glib::ustring::const_iterator it = string.begin() ; it != string.end() && it != text_iter ; it++)
1035 n++;
1036 return n;
1037 }
1039 /** applies the given style string on top of the existing styles for \a item,
1040 as opposed to sp_style_merge_from_style_string which merges its parameter
1041 underneath the existing styles (ie ignoring already set properties). */
1042 static void overwrite_style_with_string(SPObject *item, gchar const *style_string)
1043 {
1044 SPStyle *new_style = sp_style_new();
1045 sp_style_merge_from_style_string(new_style, style_string);
1046 gchar const *item_style_string = SP_OBJECT_REPR(item)->attribute("style");
1047 if (item_style_string && *item_style_string)
1048 sp_style_merge_from_style_string(new_style, item_style_string);
1049 gchar *new_style_string = sp_style_write_string(new_style);
1050 sp_style_unref(new_style);
1051 SP_OBJECT_REPR(item)->setAttribute("style", new_style_string && *new_style_string ? new_style_string : NULL);
1052 g_free(new_style_string);
1053 }
1055 /** Returns true if the style of \a parent and the style of \a child are
1056 equivalent (and hence the children of both will appear the same). It is a
1057 limitation of the current implementation that \a parent must be a (not
1058 necessarily immediate) ancestor of \a child. */
1059 static bool objects_have_equal_style(SPObject const *parent, SPObject const *child)
1060 {
1061 // the obvious implementation of strcmp(style_write_all(parent), style_write_all(child))
1062 // will not work. Firstly because of an inheritance bug in style.cpp that has
1063 // implications too large for me to feel safe fixing, but mainly because the css spec
1064 // requires that the computed value is inherited, not the specified value.
1065 g_assert(parent->isAncestorOf(child));
1066 gchar *parent_style = sp_style_write_string(parent->style, SP_STYLE_FLAG_ALWAYS);
1067 // we have to write parent_style then read it again, because some properties format their values
1068 // differently depending on whether they're set or not (*cough*dash-offset*cough*)
1069 SPStyle *parent_spstyle = sp_style_new();
1070 sp_style_merge_from_style_string(parent_spstyle, parent_style);
1071 g_free(parent_style);
1072 parent_style = sp_style_write_string(parent_spstyle, SP_STYLE_FLAG_ALWAYS);
1073 sp_style_unref(parent_spstyle);
1075 Glib::ustring child_style_construction(parent_style);
1076 while (child != parent) {
1077 // FIXME: this assumes that child's style is only in style= whereas it can also be in css attributes!
1078 char const *style_text = SP_OBJECT_REPR(child)->attribute("style");
1079 if (style_text && *style_text) {
1080 child_style_construction += ';';
1081 child_style_construction += style_text;
1082 }
1083 child = SP_OBJECT_PARENT(child);
1084 }
1085 SPStyle *child_spstyle = sp_style_new();
1086 sp_style_merge_from_style_string(child_spstyle, child_style_construction.c_str());
1087 gchar *child_style = sp_style_write_string(child_spstyle, SP_STYLE_FLAG_ALWAYS);
1088 sp_style_unref(child_spstyle);
1089 bool equal = !strcmp(child_style, parent_style);
1090 g_free(child_style);
1091 g_free(parent_style);
1092 return equal;
1093 }
1095 /** returns true if \a first and \a second contain all the same attributes
1096 with the same values as each other. Note that we have to compare both
1097 forwards and backwards to make sure we don't miss any attributes that are
1098 in one but not the other. */
1099 static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second)
1100 {
1101 Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attrs = first->attributeList();
1102 for ( ; attrs ; attrs++) {
1103 gchar const *other_attr = second->attribute(g_quark_to_string(attrs->key));
1104 if (other_attr == NULL || strcmp(attrs->value, other_attr))
1105 return false;
1106 }
1107 attrs = second->attributeList();
1108 for ( ; attrs ; attrs++) {
1109 gchar const *other_attr = first->attribute(g_quark_to_string(attrs->key));
1110 if (other_attr == NULL || strcmp(attrs->value, other_attr))
1111 return false;
1112 }
1113 return true;
1114 }
1116 /** sets the given css attribute on this object and all its descendants.
1117 Annoyingly similar to sp_desktop_apply_css_recursive(), except without the
1118 transform stuff. */
1119 static void apply_css_recursive(SPObject *o, SPCSSAttr const *css)
1120 {
1121 sp_repr_css_change(SP_OBJECT_REPR(o), const_cast<SPCSSAttr*>(css), "style");
1123 for (SPObject *child = sp_object_first_child(SP_OBJECT(o)) ; child != NULL ; child = SP_OBJECT_NEXT(child) ) {
1124 if (sp_repr_css_property(const_cast<SPCSSAttr*>(css), "opacity", NULL) != NULL) {
1125 // Unset properties which are accumulating and thus should not be set recursively.
1126 // 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.
1127 SPCSSAttr *css_recurse = sp_repr_css_attr_new();
1128 sp_repr_css_merge(css_recurse, const_cast<SPCSSAttr*>(css));
1129 sp_repr_css_set_property(css_recurse, "opacity", NULL);
1130 apply_css_recursive(child, css_recurse);
1131 sp_repr_css_attr_unref(css_recurse);
1132 } else {
1133 apply_css_recursive(child, const_cast<SPCSSAttr*>(css));
1134 }
1135 }
1136 }
1138 /** applies the given style to all the objects at the given level and below
1139 which are between \a start_item and \a end_item, creating spans as necessary.
1140 If \a start_item or \a end_item are NULL then the style is applied to all
1141 objects to the beginning or end respectively. \a span_object_name is the
1142 name of the xml for a text span (ie tspan or flowspan). */
1143 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)
1144 {
1145 bool passed_start = start_item == NULL ? true : false;
1147 for (SPObject *child = common_ancestor->firstChild() ; child != NULL ; child = SP_OBJECT_NEXT(child)) {
1148 if (start_item == child)
1149 passed_start = true;
1151 if (passed_start) {
1152 if (end_item && child->isAncestorOf(end_item)) {
1153 recursively_apply_style(child, css, NULL, start_text_iter, end_item, end_text_iter, span_object_name);
1154 break;
1155 }
1156 // apply style
1158 // note that when adding stuff we must make sure that 'child' stays valid so the for loop keeps working.
1159 // often this means that new spans are created before child and child is modified only
1160 if (SP_IS_STRING(child)) {
1161 SPString *string_item = SP_STRING(child);
1162 bool surround_entire_string = true;
1164 Inkscape::XML::Node *child_span = sp_repr_new(span_object_name);
1165 sp_repr_css_set(child_span, const_cast<SPCSSAttr*>(css), "style"); // better hope that prototype wasn't nonconst for a good reason
1166 SPObject *prev_item = SP_OBJECT_PREV(child);
1167 Inkscape::XML::Node *prev_repr = prev_item ? SP_OBJECT_REPR(prev_item) : NULL;
1169 if (child == start_item || child == end_item) {
1170 surround_entire_string = false;
1171 if (start_item == end_item && start_text_iter != string_item->string.begin()) {
1172 // eg "abcDEFghi" -> "abc"<span>"DEF"</span>"ghi"
1173 unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
1174 unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
1176 Inkscape::XML::Node *text_before = sp_repr_new_text(string_item->string.substr(0, start_char_index).c_str());
1177 SP_OBJECT_REPR(common_ancestor)->addChild(text_before, prev_repr);
1178 SP_OBJECT_REPR(common_ancestor)->addChild(child_span, text_before);
1179 Inkscape::GC::release(text_before);
1180 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());
1181 child_span->appendChild(text_in_span);
1182 Inkscape::GC::release(text_in_span);
1183 SP_OBJECT_REPR(child)->setContent(string_item->string.substr(end_char_index).c_str());
1185 } else if (child == end_item) {
1186 // eg "ABCdef" -> <span>"ABC"</span>"def"
1187 // (includes case where start_text_iter == begin())
1188 // NB: we might create an empty string here. Doesn't matter, it'll get cleaned up later
1189 unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter);
1191 SP_OBJECT_REPR(common_ancestor)->addChild(child_span, prev_repr);
1192 Inkscape::XML::Node *text_in_span = sp_repr_new_text(string_item->string.substr(0, end_char_index).c_str());
1193 child_span->appendChild(text_in_span);
1194 Inkscape::GC::release(text_in_span);
1195 SP_OBJECT_REPR(child)->setContent(string_item->string.substr(end_char_index).c_str());
1197 } else if (start_text_iter != string_item->string.begin()) {
1198 // eg "abcDEF" -> "abc"<span>"DEF"</span>
1199 unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter);
1201 Inkscape::XML::Node *text_before = sp_repr_new_text(string_item->string.substr(0, start_char_index).c_str());
1202 SP_OBJECT_REPR(common_ancestor)->addChild(text_before, prev_repr);
1203 SP_OBJECT_REPR(common_ancestor)->addChild(child_span, text_before);
1204 Inkscape::GC::release(text_before);
1205 Inkscape::XML::Node *text_in_span = sp_repr_new_text(string_item->string.substr(start_char_index).c_str());
1206 child_span->appendChild(text_in_span);
1207 Inkscape::GC::release(text_in_span);
1208 child->deleteObject();
1209 child = sp_object_get_child_by_repr(common_ancestor, child_span);
1211 } else
1212 surround_entire_string = true;
1213 }
1214 if (surround_entire_string) {
1215 Inkscape::XML::Node *child_repr = SP_OBJECT_REPR(child);
1216 SP_OBJECT_REPR(common_ancestor)->addChild(child_span, child_repr);
1217 Inkscape::GC::anchor(child_repr);
1218 SP_OBJECT_REPR(common_ancestor)->removeChild(child_repr);
1219 child_span->appendChild(child_repr);
1220 Inkscape::GC::release(child_repr);
1221 child = sp_object_get_child_by_repr(common_ancestor, child_span);
1222 }
1223 Inkscape::GC::release(child_span);
1225 } else if (child != end_item) { // not a string and we're applying to the entire object. This is easy
1226 apply_css_recursive(child, css);
1227 }
1229 } else { // !passed_start
1230 if (child->isAncestorOf(start_item)) {
1231 recursively_apply_style(child, css, start_item, start_text_iter, end_item, end_text_iter, span_object_name);
1232 if (end_item && child->isAncestorOf(end_item))
1233 break; // only happens when start_item == end_item (I think)
1234 passed_start = true;
1235 }
1236 }
1238 if (end_item == child)
1239 break;
1240 }
1241 }
1243 /* if item is at the beginning of a tree it doesn't matter which element
1244 it points to so for neatness we would like it to point to the highest
1245 possible child of \a common_ancestor. There is no iterator return because
1246 a string can never be an ancestor.
1248 eg: <span><span>*ABC</span>DEFghi</span> where * is the \a item. We would
1249 like * to point to the inner span because we can apply style to that whole
1250 span. */
1251 static SPObject* ascend_while_first(SPObject *item, Glib::ustring::iterator text_iter, SPObject *common_ancestor)
1252 {
1253 if (item == common_ancestor)
1254 return item;
1255 if (SP_IS_STRING(item))
1256 if (text_iter != SP_STRING(item)->string.begin())
1257 return item;
1258 for ( ; ; ) {
1259 SPObject *parent = SP_OBJECT_PARENT(item);
1260 if (parent == common_ancestor)
1261 break;
1262 if (item != parent->firstChild())
1263 break;
1264 item = parent;
1265 }
1266 return item;
1267 }
1270 /** empty spans: abc<span></span>def
1271 -> abcdef */
1272 static bool tidy_operator_empty_spans(SPObject **item)
1273 {
1274 if ((*item)->hasChildren()) return false;
1275 if (is_line_break_object(*item)) return false;
1276 if (SP_IS_STRING(*item) && !SP_STRING(*item)->string.empty()) return false;
1277 SPObject *next = SP_OBJECT_NEXT(*item);
1278 (*item)->deleteObject();
1279 *item = next;
1280 return true;
1281 }
1283 /** inexplicable spans: abc<span style="">def</span>ghi
1284 -> "abc""def""ghi"
1285 the repeated strings will be merged by another operator. */
1286 static bool tidy_operator_inexplicable_spans(SPObject **item)
1287 {
1288 if (SP_IS_STRING(*item)) return false;
1289 if (is_line_break_object(*item)) return false;
1290 TextTagAttributes *attrs = attributes_for_object(*item);
1291 if (attrs && attrs->anyAttributesSet()) return false;
1292 if (!objects_have_equal_style(SP_OBJECT_PARENT(*item), *item)) return false;
1293 SPObject *next = *item;
1294 while ((*item)->hasChildren()) {
1295 Inkscape::XML::Node *repr = SP_OBJECT_REPR((*item)->firstChild());
1296 Inkscape::GC::anchor(repr);
1297 SP_OBJECT_REPR(*item)->removeChild(repr);
1298 SP_OBJECT_REPR(SP_OBJECT_PARENT(*item))->addChild(repr, SP_OBJECT_REPR(next));
1299 Inkscape::GC::release(repr);
1300 next = SP_OBJECT_NEXT(next);
1301 }
1302 (*item)->deleteObject();
1303 *item = next;
1304 return true;
1305 }
1307 /** repeated spans: <font a>abc</font><font a>def</font>
1308 -> <font a>abcdef</font> */
1309 static bool tidy_operator_repeated_spans(SPObject **item)
1310 {
1311 SPObject *first = *item;
1312 SPObject *second = SP_OBJECT_NEXT(first);
1313 if (second == NULL) return false;
1315 Inkscape::XML::Node *first_repr = SP_OBJECT_REPR(first);
1316 Inkscape::XML::Node *second_repr = SP_OBJECT_REPR(second);
1318 if (first_repr->type() != second_repr->type()) return false;
1320 if (SP_IS_STRING(first) && SP_IS_STRING(second)) {
1321 // also amalgamate consecutive SPStrings into one
1322 Glib::ustring merged_string = SP_STRING(first)->string + SP_STRING(second)->string;
1323 SP_OBJECT_REPR(first)->setContent(merged_string.c_str());
1324 second_repr->parent()->removeChild(second_repr);
1325 return true;
1326 }
1328 // merge consecutive spans with identical styles into one
1329 if (first_repr->type() != Inkscape::XML::ELEMENT_NODE) return false;
1330 if (strcmp(first_repr->name(), second_repr->name()) != 0) return false;
1331 if (is_line_break_object(second)) return false;
1332 gchar const *first_style = first_repr->attribute("style");
1333 gchar const *second_style = second_repr->attribute("style");
1334 if (!((first_style == NULL && second_style == NULL)
1335 || (first_style != NULL && second_style != NULL && !strcmp(first_style, second_style))))
1336 return false;
1338 // all our tests passed: do the merge
1339 TextTagAttributes *attributes_first = attributes_for_object(first);
1340 TextTagAttributes *attributes_second = attributes_for_object(second);
1341 if (attributes_first && attributes_second && attributes_second->anyAttributesSet()) {
1342 TextTagAttributes attributes_first_copy = *attributes_first;
1343 attributes_first->join(attributes_first_copy, *attributes_second, sp_text_get_length(first));
1344 }
1345 move_child_nodes(second_repr, first_repr);
1346 second_repr->parent()->removeChild(second_repr);
1347 return true;
1348 // *item is still the next object to process
1349 }
1351 /** redundant nesting: <font a><font b>abc</font></font>
1352 -> <font b>abc</font>
1353 excessive nesting: <font a><size 1>abc</size></font>
1354 -> <font a,size 1>abc</font> */
1355 static bool tidy_operator_excessive_nesting(SPObject **item)
1356 {
1357 if (!(*item)->hasChildren()) return false;
1358 if ((*item)->firstChild() != (*item)->lastChild()) return false;
1359 if (SP_IS_FLOWREGION((*item)->firstChild()) || SP_IS_FLOWREGIONEXCLUDE((*item)->firstChild()))
1360 return false;
1361 if (SP_IS_STRING((*item)->firstChild())) return false;
1362 if (is_line_break_object((*item)->firstChild())) return false;
1363 TextTagAttributes *attrs = attributes_for_object((*item)->firstChild());
1364 if (attrs && attrs->anyAttributesSet()) return false;
1365 gchar const *child_style = SP_OBJECT_REPR((*item)->firstChild())->attribute("style");
1366 if (child_style && *child_style)
1367 overwrite_style_with_string(*item, child_style);
1368 move_child_nodes(SP_OBJECT_REPR((*item)->firstChild()), SP_OBJECT_REPR(*item));
1369 (*item)->firstChild()->deleteObject();
1370 return true;
1371 }
1373 /** helper for tidy_operator_redundant_double_nesting() */
1374 static bool redundant_double_nesting_processor(SPObject **item, SPObject *child, bool prepend)
1375 {
1376 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child))
1377 return false;
1378 if (SP_IS_STRING(child)) return false;
1379 if (is_line_break_object(child)) return false;
1380 if (is_line_break_object(*item)) return false;
1381 TextTagAttributes *attrs = attributes_for_object(child);
1382 if (attrs && attrs->anyAttributesSet()) return false;
1383 if (!objects_have_equal_style(SP_OBJECT_PARENT(*item), child)) return false;
1385 Inkscape::XML::Node *insert_after_repr;
1386 if (prepend) insert_after_repr = SP_OBJECT_REPR(SP_OBJECT_PREV(*item));
1387 else insert_after_repr = SP_OBJECT_REPR(*item);
1388 while (SP_OBJECT_REPR(child)->childCount()) {
1389 Inkscape::XML::Node *move_repr = SP_OBJECT_REPR(child)->firstChild();
1390 Inkscape::GC::anchor(move_repr);
1391 SP_OBJECT_REPR(child)->removeChild(move_repr);
1392 SP_OBJECT_REPR(SP_OBJECT_PARENT(*item))->addChild(move_repr, insert_after_repr);
1393 Inkscape::GC::release(move_repr);
1394 insert_after_repr = move_repr; // I think this will stay valid long enough. It's garbage collected these days.
1395 }
1396 child->deleteObject();
1397 return true;
1398 }
1400 /** redundant double nesting: <font b><font a><font b>abc</font>def</font>ghi</font>
1401 -> <font b>abc<font a>def</font>ghi</font>
1402 this function does its work when the parameter is the <font a> tag in the
1403 example. You may note that this only does its work when the doubly-nested
1404 child is the first or last. The other cases are called 'style inversion'
1405 below, and I'm not yet convinced that the result of that operation will be
1406 tidier in all cases. */
1407 static bool tidy_operator_redundant_double_nesting(SPObject **item)
1408 {
1409 if (!(*item)->hasChildren()) return false;
1410 if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is excessive nesting, done above
1411 if (redundant_double_nesting_processor(item, (*item)->firstChild(), true))
1412 return true;
1413 if (redundant_double_nesting_processor(item, (*item)->lastChild(), false))
1414 return true;
1415 return false;
1416 }
1418 /** helper for tidy_operator_redundant_semi_nesting(). Checks a few things,
1419 then compares the styles for item+child versus just child. If they're equal,
1420 tidying is possible. */
1421 static bool redundant_semi_nesting_processor(SPObject **item, SPObject *child, bool prepend)
1422 {
1423 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child))
1424 return false;
1425 if (SP_IS_STRING(child)) return false;
1426 if (is_line_break_object(child)) return false;
1427 if (is_line_break_object(*item)) return false;
1428 TextTagAttributes *attrs = attributes_for_object(child);
1429 if (attrs && attrs->anyAttributesSet()) return false;
1430 attrs = attributes_for_object(*item);
1431 if (attrs && attrs->anyAttributesSet()) return false;
1433 SPCSSAttr *css_child_and_item = sp_repr_css_attr_new();
1434 SPCSSAttr *css_child_only = sp_repr_css_attr_new();
1435 gchar const *child_style = SP_OBJECT_REPR(child)->attribute("style");
1436 if (child_style && *child_style) {
1437 sp_repr_css_attr_add_from_string(css_child_and_item, child_style);
1438 sp_repr_css_attr_add_from_string(css_child_only, child_style);
1439 }
1440 gchar const *item_style = SP_OBJECT_REPR(*item)->attribute("style");
1441 if (item_style && *item_style) {
1442 sp_repr_css_attr_add_from_string(css_child_and_item, item_style);
1443 }
1444 bool equal = css_attrs_are_equal(css_child_only, css_child_and_item);
1445 sp_repr_css_attr_unref(css_child_and_item);
1446 sp_repr_css_attr_unref(css_child_only);
1447 if (!equal) return false;
1449 Inkscape::XML::Node *new_span = sp_repr_new(SP_OBJECT_REPR(*item)->name());
1450 if (prepend) {
1451 SPObject *prev = SP_OBJECT_PREV(*item);
1452 SP_OBJECT_REPR(SP_OBJECT_PARENT(*item))->addChild(new_span, prev ? SP_OBJECT_REPR(prev) : NULL);
1453 } else
1454 SP_OBJECT_REPR(SP_OBJECT_PARENT(*item))->addChild(new_span, SP_OBJECT_REPR(*item));
1455 new_span->setAttribute("style", SP_OBJECT_REPR(child)->attribute("style"));
1456 move_child_nodes(SP_OBJECT_REPR(child), new_span);
1457 Inkscape::GC::release(new_span);
1458 child->deleteObject();
1459 return true;
1460 }
1462 /** redundant semi-nesting: <font a><font b>abc</font>def</font>
1463 -> <font b>abc</font><font>def</font>
1464 test this by applying a colour to a region, then a different colour to
1465 a partially-overlapping region. */
1466 static bool tidy_operator_redundant_semi_nesting(SPObject **item)
1467 {
1468 if (!(*item)->hasChildren()) return false;
1469 if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is redundant nesting, done above
1470 if (redundant_semi_nesting_processor(item, (*item)->firstChild(), true))
1471 return true;
1472 if (redundant_semi_nesting_processor(item, (*item)->lastChild(), false))
1473 return true;
1474 return false;
1475 }
1477 /** helper for tidy_operator_styled_whitespace(), finds the last string object
1478 in a paragraph which is not \a not_obj. */
1479 static SPString* find_last_string_child_not_equal_to(SPObject *root, SPObject *not_obj)
1480 {
1481 for (SPObject *child = root->lastChild() ; child ; child = SP_OBJECT_PREV(child))
1482 {
1483 if (child == not_obj) continue;
1484 if (child->hasChildren()) {
1485 SPString *ret = find_last_string_child_not_equal_to(child, not_obj);
1486 if (ret) return ret;
1487 } else if (SP_IS_STRING(child))
1488 return SP_STRING(child);
1489 }
1490 return NULL;
1491 }
1493 /** whitespace-only spans: abc<font> </font>def
1494 -> abc<font></font> def
1495 abc<b><i>def</i> </b>ghi
1496 -> abc<b><i>def</i></b> ghi */
1497 static bool tidy_operator_styled_whitespace(SPObject **item)
1498 {
1499 if (!SP_IS_STRING(*item)) return false;
1500 Glib::ustring const &str = SP_STRING(*item)->string;
1501 for (Glib::ustring::const_iterator it = str.begin() ; it != str.end() ; ++it)
1502 if (!g_unichar_isspace(*it)) return false;
1504 SPObject *test_item = *item;
1505 SPString *next_string;
1506 for ( ; ; ) { // find the next string
1507 next_string = sp_te_seek_next_string_recursive(SP_OBJECT_NEXT(test_item));
1508 if (next_string) {
1509 next_string->string.insert(0, str);
1510 break;
1511 }
1512 for ( ; ; ) { // go up one item in the xml
1513 test_item = SP_OBJECT_PARENT(test_item);
1514 if (is_line_break_object(test_item)) break;
1515 SPObject *next = SP_OBJECT_NEXT(test_item);
1516 if (next) {
1517 test_item = next;
1518 break;
1519 }
1520 }
1521 if (is_line_break_object(test_item)) { // no next string, see if there's a prev string
1522 next_string = find_last_string_child_not_equal_to(test_item, *item);
1523 if (next_string == NULL) return false; // an empty paragraph
1524 next_string->string += str;
1525 break;
1526 }
1527 }
1528 SP_OBJECT_REPR(next_string)->setContent(next_string->string.c_str());
1529 SPObject *delete_obj = *item;
1530 *item = SP_OBJECT_NEXT(*item);
1531 delete_obj->deleteObject();
1532 return true;
1533 }
1535 /* possible tidy operators that are not yet implemented, either because
1536 they are difficult, occur infrequently, or because I'm not sure that the
1537 output is tidier in all cases:
1538 duplicate styles in line break elements: <div italic><para italic>abc</para></div>
1539 -> <div italic><para>abc</para></div>
1540 style inversion: <font a>abc<font b>def<font a>ghi</font>jkl</font>mno</font>
1541 -> <font a>abc<font b>def</font>ghi<font b>jkl</font>mno</font>
1542 mistaken precedence: <font a,size 1>abc</font><size 1>def</size>
1543 -> <size 1><font a>abc</font>def</size>
1544 */
1546 /** Recursively walks the xml tree calling a set of cleanup operations on
1547 every child. Returns true if any changes were made to the tree.
1549 All the tidy operators return true if they made changes, and alter their
1550 parameter to point to the next object that should be processed, or NULL.
1551 They must not significantly alter (ie delete) any ancestor elements of the
1552 one they are passed.
1554 It may be that some of the later tidy operators that I wrote are actually
1555 general cases of the earlier operators, and hence the special-case-only
1556 versions can be removed. I haven't analysed my work in detail to figure
1557 out if this is so. */
1558 static bool tidy_xml_tree_recursively(SPObject *root)
1559 {
1560 static bool (* const tidy_operators[])(SPObject**) = {
1561 tidy_operator_empty_spans,
1562 tidy_operator_inexplicable_spans,
1563 tidy_operator_repeated_spans,
1564 tidy_operator_excessive_nesting,
1565 tidy_operator_redundant_double_nesting,
1566 tidy_operator_redundant_semi_nesting,
1567 tidy_operator_styled_whitespace
1568 };
1569 bool changes = false;
1571 for (SPObject *child = root->firstChild() ; child != NULL ; ) {
1572 if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child)) {
1573 child = SP_OBJECT_NEXT(child);
1574 continue;
1575 }
1576 if (child->hasChildren())
1577 changes |= tidy_xml_tree_recursively(child);
1579 unsigned i;
1580 for (i = 0 ; i < sizeof(tidy_operators) / sizeof(tidy_operators[0]) ; i++) {
1581 if (tidy_operators[i](&child)) {
1582 changes = true;
1583 break;
1584 }
1585 }
1586 if (i == sizeof(tidy_operators) / sizeof(tidy_operators[0]))
1587 child = SP_OBJECT_NEXT(child);
1588 }
1589 return changes;
1590 }
1592 /** Applies the given CSS fragment to the characters of the given text or
1593 flowtext object between \a start and \a end, creating or removing span
1594 elements as necessary and optimal. */
1595 void sp_te_apply_style(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPCSSAttr const *css)
1596 {
1597 // in the comments in the code below, capital letters are inside the application region, lowercase are outside
1598 if (start == end) return;
1599 Inkscape::Text::Layout::iterator first, last;
1600 if (start < end) {
1601 first = start;
1602 last = end;
1603 } else {
1604 first = end;
1605 last = start;
1606 }
1607 Inkscape::Text::Layout const *layout = te_get_layout(text);
1608 SPObject *start_item, *end_item;
1609 Glib::ustring::iterator start_text_iter, end_text_iter;
1610 layout->getSourceOfCharacter(first, (void**)&start_item, &start_text_iter);
1611 layout->getSourceOfCharacter(last, (void**)&end_item, &end_text_iter);
1612 if (start_item == NULL)
1613 return; // start is at end of text
1614 if (is_line_break_object(start_item))
1615 start_item = SP_OBJECT_NEXT(start_item);
1616 if (is_line_break_object(end_item))
1617 end_item = SP_OBJECT_NEXT(end_item);
1618 if (end_item == NULL) end_item = text;
1620 /* stage 1: applying the style. Go up to the closest common ancestor of
1621 start and end and then semi-recursively apply the style to all the
1622 objects in between. The semi-recursion is because it's only necessary
1623 at the beginning and end; the style can just be applied to the root
1624 child in the middle.
1625 eg: <span>abcDEF</span><span>GHI</span><span>JKLmno</span>
1626 The recursion may involve creating new spans.
1627 */
1628 SPObject *common_ancestor = get_common_ancestor(text, start_item, end_item);
1629 start_item = ascend_while_first(start_item, start_text_iter, common_ancestor);
1630 end_item = ascend_while_first(end_item, end_text_iter, common_ancestor);
1631 recursively_apply_style(common_ancestor, css, start_item, start_text_iter, end_item, end_text_iter, span_name_for_text_object(text));
1633 /* stage 2: cleanup the xml tree (of which there are multiple passes) */
1634 /* discussion: this stage requires a certain level of inventiveness because
1635 it's not clear what the best representation is in many cases. An ideal
1636 implementation would provide some sort of scoring function to rate the
1637 ugliness of a given xml tree and try to reduce said function, but providing
1638 the various possibilities to be rated is non-trivial. Instead, I have opted
1639 for a multi-pass technique which simply recognises known-ugly patterns and
1640 has matching routines for optimising the patterns it finds. It's reasonably
1641 easy to add new pattern matching processors. If everything gets disastrous
1642 and neither option can be made to work, a fallback could be to reduce
1643 everything to a single level of nesting and drop all pretence of
1644 roundtrippability. */
1645 while (tidy_xml_tree_recursively(common_ancestor));
1647 // if we only modified subobjects this won't have been automatically sent
1648 text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
1649 }
1651 /*
1652 Local Variables:
1653 mode:c++
1654 c-file-style:"stroustrup"
1655 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1656 indent-tabs-mode:nil
1657 fill-column:99
1658 End:
1659 */
1660 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :