1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """
22 Contains the base L{StringElem} class that represents a node in a parsed rich-
23 string tree. It is the base class of all placeables.
24 """
25
26 import logging
27 import sys
32
35 """
36 This class represents a sub-tree of a string parsed into a rich structure.
37 It is also the base class of all placeables.
38 """
39
40 renderer = None
41 """An optional function that returns the Unicode representation of the string."""
42 sub = []
43 """The sub-elements that make up this this string."""
44 has_content = True
45 """Whether this string can have sub-elements."""
46 iseditable = True
47 """Whether this string should be changable by the user. Not used at the moment."""
48 isfragile = False
49 """Whether this element should be deleted in its entirety when partially
50 deleted. Only checked when C{iseditable = False}"""
51 istranslatable = True
52 """Whether this string is translatable into other languages."""
53 isvisible = True
54 """Whether this string should be visible to the user. Not used at the moment."""
55
56
57 - def __init__(self, sub=None, id=None, rid=None, xid=None, **kwargs):
78
79
81 """Emulate the C{unicode} class."""
82 return unicode(self) + rhs
83
85 """Emulate the C{unicode} class."""
86 return item in unicode(self)
87
102
104 """Emulate the C{unicode} class."""
105 return unicode(self) >= rhs
106
108 """Emulate the C{unicode} class."""
109 return unicode(self)[i]
110
112 """Emulate the C{unicode} class."""
113 return unicode(self)[i:j]
114
116 """Emulate the C{unicode} class."""
117 return unicode(self) > rhs
118
120 """Create an iterator of this element's sub-elements."""
121 for elem in self.sub:
122 yield elem
123
125 """Emulate the C{unicode} class."""
126 return unicode(self) <= rhs
127
129 """Emulate the C{unicode} class."""
130 return len(unicode(self))
131
133 """Emulate the C{unicode} class."""
134 return unicode(self) < rhs
135
137 """Emulate the C{unicode} class."""
138 return unicode(self) * rhs
139
141 return not self.__eq__(rhs)
142
144 """Emulate the C{unicode} class."""
145 return self + lhs
146
148 """Emulate the C{unicode} class."""
149 return self * lhs
150
152 elemstr = ', '.join([repr(elem) for elem in self.sub])
153 return '<%(class)s(%(id)s%(rid)s%(xid)s[%(subs)s])>' % {
154 'class': self.__class__.__name__,
155 'id': self.id is not None and 'id="%s" ' % (self.id) or '',
156 'rid': self.rid is not None and 'rid="%s" ' % (self.rid) or '',
157 'xid': self.xid is not None and 'xid="%s" ' % (self.xid) or '',
158 'subs': elemstr
159 }
160
162 if not self.isvisible:
163 return ''
164 return ''.join([unicode(elem).encode('utf-8') for elem in self.sub])
165
167 if callable(self.renderer):
168 return self.renderer(self)
169 if not self.isvisible:
170 return u''
171 return u''.join([unicode(elem) for elem in self.sub])
172
173
175 """Apply C{f} to all actual strings in the tree.
176 @param f: Must take one (str or unicode) argument and return a
177 string or unicode."""
178 for elem in self.flatten():
179 for i in range(len(elem.sub)):
180 if isinstance(elem.sub[i], basestring):
181 elem.sub[i] = f(elem.sub[i])
182
184 """Returns a copy of the sub-tree.
185 This should be overridden in sub-classes with more data.
186
187 NOTE: C{self.renderer} is B{not} copied."""
188
189 cp = self.__class__(id=self.id, xid=self.xid, rid=self.rid)
190 for sub in self.sub:
191 if isinstance(sub, StringElem):
192 cp.sub.append(sub.copy())
193 else:
194 cp.sub.append(sub.__class__(sub))
195 return cp
196
198 if elem is self:
199 self.sub = []
200 return
201 parent = self.get_parent_elem(elem)
202 if parent is None:
203 raise ElementNotFoundError(repr(elem))
204 subidx = -1
205 for i in range(len(parent.sub)):
206 if parent.sub[i] is elem:
207 subidx = i
208 break
209 if subidx < 0:
210 raise ElementNotFoundError(repr(elem))
211 del parent.sub[subidx]
212
214 """Delete the text in the range given by the string-indexes
215 C{start_index} and C{end_index}.
216 Partial nodes will only be removed if they are editable.
217 @returns: A C{StringElem} representing the removed sub-string, the
218 parent node from which it was deleted as well as the offset at
219 which it was deleted from. C{None} is returned for the parent
220 value if the root was deleted. If the parent and offset values
221 are not C{None}, C{parent.insert(offset, deleted)} effectively
222 undoes the delete."""
223 if start_index == end_index:
224 return StringElem(), self, 0
225 if start_index > end_index:
226 raise IndexError('start_index > end_index: %d > %d' % (start_index, end_index))
227 if start_index < 0 or start_index > len(self):
228 raise IndexError('start_index: %d' % (start_index))
229 if end_index < 1 or end_index > len(self) + 1:
230 raise IndexError('end_index: %d' % (end_index))
231
232 start = self.get_index_data(start_index)
233 if isinstance(start['elem'], tuple):
234
235 start['elem'] = start['elem'][-1]
236 start['offset'] = start['offset'][-1]
237 end = self.get_index_data(end_index)
238 if isinstance(end['elem'], tuple):
239
240 end['elem'] = end['elem'][0]
241 end['offset'] = end['offset'][0]
242 assert start['elem'].isleaf() and end['elem'].isleaf()
243
244
245
246
247
248
249
250
251
252
253 if start_index == 0 and end_index == len(self):
254
255 removed = self.copy()
256 self.sub = []
257 return removed, None, None
258
259
260 if start['elem'] is end['elem'] and start['offset'] == 0 and end['offset'] == len(start['elem']) or \
261 (not start['elem'].iseditable and start['elem'].isfragile):
262
263
264
265
266
267
268
269
270
271
272 if start['elem'] is self and self.__class__ is StringElem:
273 removed = self.copy()
274 self.sub = []
275 return removed, None, None
276 removed = start['elem'].copy()
277 parent = self.get_parent_elem(start['elem'])
278 offset = parent.elem_offset(start['elem'])
279 parent.sub.remove(start['elem'])
280 return removed, parent, offset
281
282
283 if start['elem'] is end['elem'] and start['elem'].iseditable:
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299 newstr = u''.join(start['elem'].sub)
300 removed = StringElem(newstr[start['offset']:end['offset']])
301 newstr = newstr[:start['offset']] + newstr[end['offset']:]
302 parent = self.get_parent_elem(start['elem'])
303 if parent is None and start['elem'] is self:
304 parent = self
305 start['elem'].sub = [newstr]
306 self.prune()
307 return removed, start['elem'], start['offset']
308
309
310 range_nodes = self.depth_first()
311 startidx = 0
312 endidx = -1
313 for i in range(len(range_nodes)):
314 if range_nodes[i] is start['elem']:
315 startidx = i
316 elif range_nodes[i] is end['elem']:
317 endidx = i
318 break
319 range_nodes = range_nodes[startidx:endidx+1]
320
321
322 marked_nodes = []
323 for node in range_nodes[1:-1]:
324 if node in marked_nodes:
325 continue
326 subtree = node.depth_first()
327 if end['elem'] not in subtree:
328 marked_nodes.extend(subtree)
329
330
331
332
333
334
335
336
337
338
339
340
341
342 removed = self.copy()
343
344
345 start_offset = self.elem_offset(start['elem'])
346 end_offset = self.elem_offset(end['elem'])
347
348 for node in marked_nodes:
349 self.delete_elem(node)
350
351 if start['elem'] is not end['elem']:
352 if start_offset == start['index'] or (not start['elem'].iseditable and start['elem'].isfragile):
353 self.delete_elem(start['elem'])
354 elif start['elem'].iseditable:
355 start['elem'].sub = [ u''.join(start['elem'].sub)[:start['offset']] ]
356
357 if end_offset + len(end['elem']) == end['index'] or (not end['elem'].iseditable and end['elem'].isfragile):
358 self.delete_elem(end['elem'])
359 elif end['elem'].iseditable:
360 end['elem'].sub = [ u''.join(end['elem'].sub)[end['offset']:] ]
361
362 self.prune()
363 return removed, None, None
364
381
382 - def encode(self, encoding=sys.getdefaultencoding()):
383 """More C{unicode} class emulation."""
384 return unicode(self).encode(encoding)
385
387 """Find the offset of C{elem} in the current tree.
388 This cannot be reliably used if C{self.renderer} is used and even
389 less so if the rendering function renders the string differently
390 upon different calls. In Virtaal the C{StringElemGUI.index()} method
391 is used as replacement for this one.
392 @returns: The string index where element C{e} starts, or -1 if C{e}
393 was not found."""
394 offset = 0
395 for e in self.iter_depth_first():
396 if e is elem:
397 return offset
398 if e.isleaf():
399 offset += len(e)
400
401
402 offset = 0
403 for e in self.iter_depth_first():
404 if e.isleaf():
405 leafoffset = 0
406 for s in e.sub:
407 if unicode(s) == unicode(elem):
408 return offset + leafoffset
409 else:
410 leafoffset += len(unicode(s))
411 offset += len(e)
412 return -1
413
415 """Get the C{StringElem} in the tree that contains the string rendered
416 at the given offset."""
417 if offset < 0 or offset > len(self):
418 return None
419
420 length = 0
421 elem = None
422 for elem in self.flatten():
423 elem_len = len(elem)
424 if length <= offset < length+elem_len:
425 return elem
426 length += elem_len
427 return elem
428
430 """Find sub-string C{x} in this string tree and return the position
431 at which it starts."""
432 if isinstance(x, basestring):
433 return unicode(self).find(x)
434 if isinstance(x, StringElem):
435 return unicode(self).find(unicode(x))
436 return None
437
439 """Find all elements in the current sub-tree containing C{x}."""
440 return [elem for elem in self.flatten() if x in unicode(elem)]
441
443 """Flatten the tree by returning a depth-first search over the tree's leaves."""
444 if filter is None or not callable(filter):
445 filter = lambda e: True
446 return [elem for elem in self.iter_depth_first(lambda e: e.isleaf() and filter(e))]
447
453
455 """Get info about the specified range in the tree.
456 @returns: A dictionary with the following items:
457 * I{elem}: The element in which C{index} resides.
458 * I{index}: Copy of the C{index} parameter
459 * I{offset}: The offset of C{index} into C{'elem'}."""
460 info = {
461 'elem': self.elem_at_offset(index),
462 'index': index,
463 }
464 info['offset'] = info['index'] - self.elem_offset(info['elem'])
465
466
467 leftelem = self.elem_at_offset(index - 1)
468 if leftelem is not None and leftelem is not info['elem']:
469 info['elem'] = (leftelem, info['elem'])
470 info['offset'] = (len(leftelem), 0)
471
472 return info
473
475 """Searches the current sub-tree for and returns the parent of the
476 C{child} element."""
477 for elem in self.iter_depth_first():
478 if not isinstance(elem, StringElem):
479 continue
480 for sub in elem.sub:
481 if sub is child:
482 return elem
483 return None
484
485 - def insert(self, offset, text):
486 """Insert the given text at the specified offset of this string-tree's
487 string (Unicode) representation."""
488 if offset < 0 or offset > len(self) + 1:
489 raise IndexError('Index out of range: %d' % (offset))
490 if isinstance(text, (str, unicode)):
491 text = StringElem(text)
492 if not isinstance(text, StringElem):
493 raise ValueError('text must be of type StringElem')
494
495 def checkleaf(elem, text):
496 if elem.isleaf() and type(text) is StringElem and text.isleaf():
497 return unicode(text)
498 return text
499
500
501
502
503
504
505
506
507
508
509
510
511
512 oelem = self.elem_at_offset(offset)
513
514
515 if offset == 0:
516
517 if oelem.iseditable:
518
519 oelem.sub.insert(0, checkleaf(oelem, text))
520 oelem.prune()
521 return True
522
523 else:
524
525 oparent = self.get_ancestor_where(oelem, lambda x: x.iseditable)
526 if oparent is not None:
527 oparent.sub.insert(0, checkleaf(oparent, text))
528 return True
529 else:
530 self.sub.insert(0, checkleaf(self, text))
531 return True
532 return False
533
534
535 if offset >= len(self):
536
537 last = self.flatten()[-1]
538 parent = self.get_ancestor_where(last, lambda x: x.iseditable)
539 if parent is None:
540 parent = self
541 parent.sub.append(checkleaf(parent, text))
542 return True
543
544 before = self.elem_at_offset(offset-1)
545
546
547 if oelem is before:
548 if oelem.iseditable:
549
550 eoffset = offset - self.elem_offset(oelem)
551 if oelem.isleaf():
552 s = unicode(oelem)
553 head = s[:eoffset]
554 tail = s[eoffset:]
555 if type(text) is StringElem and text.isleaf():
556 oelem.sub = [head + unicode(text) + tail]
557 else:
558 oelem.sub = [StringElem(head), text, StringElem(tail)]
559 return True
560 else:
561 return oelem.insert(eoffset, text)
562 return False
563
564
565
566 if not before.iseditable and not oelem.iseditable:
567
568
569 bparent = self.get_parent_elem(before)
570
571
572 bindex = bparent.sub.index(before)
573 bparent.sub.insert(bindex + 1, text)
574 return True
575
576
577 elif before.iseditable and oelem.iseditable:
578
579 return before.insert(len(before)+1, text)
580
581
582 elif before.iseditable and not oelem.iseditable:
583
584 return before.insert(len(before)+1, text)
585
586
587 elif not before.iseditable and oelem.iseditable:
588
589 return oelem.insert(0, text)
590
591 return False
592
594 """Insert the given text between the two parameter C{StringElem}s."""
595 if not isinstance(left, StringElem) and left is not None:
596 raise ValueError('"left" is not a StringElem or None')
597 if not isinstance(right, StringElem) and right is not None:
598 raise ValueError('"right" is not a StringElem or None')
599 if left is right:
600 if left.sub:
601
602
603
604 raise ValueError('"left" and "right" refer to the same element and is not empty.')
605 if not left.iseditable:
606 return False
607 if isinstance(text, unicode):
608 text = StringElem(text)
609
610 if left is right:
611
612 left.sub.append(text)
613 return True
614
615
616
617
618 if left is None:
619 if self is right:
620
621 self.sub.insert(0, text)
622 return True
623 parent = self.get_parent_elem(right)
624 if parent is not None:
625
626 parent.sub.insert(0, text)
627 return True
628 return False
629
630 if right is None:
631 if self is left:
632
633 self.sub.append(text)
634 return True
635 parent = self.get_parent_elem(left)
636 if parent is not None:
637
638 parent.sub.append(text)
639 return True
640 return False
641
642
643
644
645 ischild = False
646 for sub in left.sub:
647 if right is sub:
648 ischild = True
649 break
650 if ischild:
651
652 left.sub.insert(0, text)
653 return True
654
655 ischild = False
656 for sub in right.sub:
657 if left is sub:
658 ischild = True
659 break
660 if ischild:
661
662 right.sub.append(text)
663 return True
664
665 parent = self.get_parent_elem(left)
666 if parent.iseditable:
667 idx = 1
668 for child in parent.sub:
669 if child is left:
670 break
671 idx += 1
672
673 parent.sub.insert(idx, text)
674 return True
675
676 parent = self.get_parent_elem(right)
677 if parent.iseditable:
678 idx = 0
679 for child in parent.sub:
680 if child is right:
681 break
682 idx += 1
683
684 parent.sub.insert(0, text)
685 return True
686
687 logging.debug('Could not insert between %s and %s... odd.' % (repr(left), repr(right)))
688 return False
689
691 """
692 Whether or not this instance is a leaf node in the C{StringElem} tree.
693
694 A node is a leaf node if it is a C{StringElem} (not a sub-class) and
695 contains only sub-elements of type C{str} or C{unicode}.
696
697 @rtype: bool
698 """
699 for e in self.sub:
700 if not isinstance(e, (str, unicode)):
701 return False
702 return True
703
719
720 - def map(self, f, filter=None):
721 """Apply C{f} to all nodes for which C{filter} returned C{True} (optional)."""
722 if filter is not None and not callable(filter):
723 raise ValueError('filter is not callable or None')
724 if filter is None:
725 filter = lambda e: True
726
727 for elem in self.depth_first():
728 if filter(elem):
729 f(elem)
730
731 @classmethod
733 """Parse an instance of this class from the start of the given string.
734 This method should be implemented by any sub-class that wants to
735 parseable by L{translate.storage.placeables.parse}.
736
737 @type pstr: unicode
738 @param pstr: The string to parse into an instance of this class.
739 @returns: An instance of the current class, or C{None} if the
740 string not parseable by this class."""
741 return cls(pstr)
742
744 """Print the tree from the current instance's point in an indented
745 manner."""
746 indent_prefix = " " * indent * 2
747 out = (u"%s%s [%s]" % (indent_prefix, self.__class__.__name__, unicode(self))).encode('utf-8')
748 if verbose:
749 out += u' ' + repr(self)
750
751 print out
752
753 for elem in self.sub:
754 if isinstance(elem, StringElem):
755 elem.print_tree(indent+1, verbose=verbose)
756 else:
757 print (u'%s%s[%s]' % (indent_prefix, indent_prefix, elem)).encode('utf-8')
758
809
810
812 """Replace nodes with type C{ptype} with base C{StringElem}s, containing
813 the same sub-elements. This is only applicable to elements below the
814 element tree root node."""
815 for elem in self.iter_depth_first():
816 if type(elem) is ptype:
817 parent = self.get_parent_elem(elem)
818 pindex = parent.sub.index(elem)
819 parent.sub[pindex] = StringElem(
820 sub=elem.sub,
821 id=elem.id,
822 xid=elem.xid,
823 rid=elem.rid
824 )
825
827 """Transform the sub-tree according to some class-specific needs.
828 This method should be either overridden in implementing sub-classes
829 or dynamically replaced by specific applications.
830
831 @returns: The transformed Unicode string representing the sub-tree.
832 """
833 return self.copy()
834