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):
58 if sub is None:
59 self.sub = []
60 elif isinstance(sub, (unicode, StringElem)):
61 self.sub = [sub]
62 else:
63 for elem in sub:
64 if not isinstance(elem, (unicode, StringElem)):
65 raise ValueError(elem)
66 self.sub = sub
67 self.prune()
68
69 self.id = id
70 self.rid = rid
71 self.xid = xid
72
73 for key, value in kwargs.items():
74 if hasattr(self, key):
75 raise ValueError('attribute already exists: %s' % (key))
76 setattr(self, key, value)
77
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
280
281
282
283
284 parent.sub = [i for i in parent.sub if i is not start['elem']]
285 return removed, parent, offset
286
287
288 if start['elem'] is end['elem'] and start['elem'].iseditable:
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304 newstr = u''.join(start['elem'].sub)
305 removed = StringElem(newstr[start['offset']:end['offset']])
306 newstr = newstr[:start['offset']] + newstr[end['offset']:]
307 parent = self.get_parent_elem(start['elem'])
308 if parent is None and start['elem'] is self:
309 parent = self
310 start['elem'].sub = [newstr]
311 self.prune()
312 return removed, start['elem'], start['offset']
313
314
315 range_nodes = self.depth_first()
316 startidx = 0
317 endidx = -1
318 for i in range(len(range_nodes)):
319 if range_nodes[i] is start['elem']:
320 startidx = i
321 elif range_nodes[i] is end['elem']:
322 endidx = i
323 break
324 range_nodes = range_nodes[startidx:endidx+1]
325
326
327
328 marked_nodes = []
329 for node in range_nodes[1:-1]:
330 if [n for n in marked_nodes if n is node]:
331 continue
332 subtree = node.depth_first()
333 if not [e for e in subtree if e is end['elem']]:
334
335 marked_nodes.extend(subtree)
336
337
338
339
340
341
342
343
344
345
346
347
348
349 removed = self.copy()
350
351
352 start_offset = self.elem_offset(start['elem'])
353 end_offset = self.elem_offset(end['elem'])
354
355 for node in marked_nodes:
356 try:
357 self.delete_elem(node)
358 except ElementNotFoundError, e:
359 pass
360
361 if start['elem'] is not end['elem']:
362 if start_offset == start['index'] or (not start['elem'].iseditable and start['elem'].isfragile):
363 self.delete_elem(start['elem'])
364 elif start['elem'].iseditable:
365 start['elem'].sub = [u''.join(start['elem'].sub)[:start['offset']]]
366
367 if end_offset + len(end['elem']) == end['index'] or (not end['elem'].iseditable and end['elem'].isfragile):
368 self.delete_elem(end['elem'])
369 elif end['elem'].iseditable:
370 end['elem'].sub = [u''.join(end['elem'].sub)[end['offset']:]]
371
372 self.prune()
373 return removed, None, None
374
391
392 - def encode(self, encoding=sys.getdefaultencoding()):
393 """More C{unicode} class emulation."""
394 return unicode(self).encode(encoding)
395
397 """Find the offset of C{elem} in the current tree.
398 This cannot be reliably used if C{self.renderer} is used and even
399 less so if the rendering function renders the string differently
400 upon different calls. In Virtaal the C{StringElemGUI.index()} method
401 is used as replacement for this one.
402 @returns: The string index where element C{e} starts, or -1 if C{e}
403 was not found."""
404 offset = 0
405 for e in self.iter_depth_first():
406 if e is elem:
407 return offset
408 if e.isleaf():
409 offset += len(e)
410
411
412 offset = 0
413 for e in self.iter_depth_first():
414 if e.isleaf():
415 leafoffset = 0
416 for s in e.sub:
417 if unicode(s) == unicode(elem):
418 return offset + leafoffset
419 else:
420 leafoffset += len(unicode(s))
421 offset += len(e)
422 return -1
423
425 """Get the C{StringElem} in the tree that contains the string rendered
426 at the given offset."""
427 if offset < 0 or offset > len(self):
428 return None
429
430 length = 0
431 elem = None
432 for elem in self.flatten():
433 elem_len = len(elem)
434 if length <= offset < length+elem_len:
435 return elem
436 length += elem_len
437 return elem
438
440 """Find sub-string C{x} in this string tree and return the position
441 at which it starts."""
442 if isinstance(x, basestring):
443 return unicode(self).find(x)
444 if isinstance(x, StringElem):
445 return unicode(self).find(unicode(x))
446 return None
447
449 """Find all elements in the current sub-tree containing C{x}."""
450 return [elem for elem in self.flatten() if x in unicode(elem)]
451
453 """Flatten the tree by returning a depth-first search over the tree's leaves."""
454 if filter is None or not callable(filter):
455 filter = lambda e: True
456 return [elem for elem in self.iter_depth_first(lambda e: e.isleaf() and filter(e))]
457
463
465 """Get info about the specified range in the tree.
466 @returns: A dictionary with the following items:
467 * I{elem}: The element in which C{index} resides.
468 * I{index}: Copy of the C{index} parameter
469 * I{offset}: The offset of C{index} into C{'elem'}."""
470 info = {
471 'elem': self.elem_at_offset(index),
472 'index': index,
473 }
474 info['offset'] = info['index'] - self.elem_offset(info['elem'])
475
476
477 leftelem = self.elem_at_offset(index - 1)
478 if leftelem is not None and leftelem is not info['elem']:
479 info['elem'] = (leftelem, info['elem'])
480 info['offset'] = (len(leftelem), 0)
481
482 return info
483
485 """Searches the current sub-tree for and returns the parent of the
486 C{child} element."""
487 for elem in self.iter_depth_first():
488 if not isinstance(elem, StringElem):
489 continue
490 for sub in elem.sub:
491 if sub is child:
492 return elem
493 return None
494
495 - def insert(self, offset, text):
496 """Insert the given text at the specified offset of this string-tree's
497 string (Unicode) representation."""
498 if offset < 0 or offset > len(self) + 1:
499 raise IndexError('Index out of range: %d' % (offset))
500 if isinstance(text, (str, unicode)):
501 text = StringElem(text)
502 if not isinstance(text, StringElem):
503 raise ValueError('text must be of type StringElem')
504
505 def checkleaf(elem, text):
506 if elem.isleaf() and type(text) is StringElem and text.isleaf():
507 return unicode(text)
508 return text
509
510
511
512
513
514
515
516
517
518
519
520
521
522 oelem = self.elem_at_offset(offset)
523
524
525 if offset == 0:
526
527 if oelem.iseditable:
528
529 oelem.sub.insert(0, checkleaf(oelem, text))
530 oelem.prune()
531 return True
532
533 else:
534
535 oparent = self.get_ancestor_where(oelem, lambda x: x.iseditable)
536 if oparent is not None:
537 oparent.sub.insert(0, checkleaf(oparent, text))
538 return True
539 else:
540 self.sub.insert(0, checkleaf(self, text))
541 return True
542 return False
543
544
545 if offset >= len(self):
546
547 last = self.flatten()[-1]
548 parent = self.get_ancestor_where(last, lambda x: x.iseditable)
549 if parent is None:
550 parent = self
551 parent.sub.append(checkleaf(parent, text))
552 return True
553
554 before = self.elem_at_offset(offset-1)
555
556
557 if oelem is before:
558 if oelem.iseditable:
559
560 eoffset = offset - self.elem_offset(oelem)
561 if oelem.isleaf():
562 s = unicode(oelem)
563 head = s[:eoffset]
564 tail = s[eoffset:]
565 if type(text) is StringElem and text.isleaf():
566 oelem.sub = [head + unicode(text) + tail]
567 else:
568 oelem.sub = [StringElem(head), text, StringElem(tail)]
569 return True
570 else:
571 return oelem.insert(eoffset, text)
572 return False
573
574
575
576 if not before.iseditable and not oelem.iseditable:
577
578
579 bparent = self.get_parent_elem(before)
580
581
582 bindex = bparent.sub.index(before)
583 bparent.sub.insert(bindex + 1, text)
584 return True
585
586
587 elif before.iseditable and oelem.iseditable:
588
589 return before.insert(len(before) + 1, text)
590
591
592 elif before.iseditable and not oelem.iseditable:
593
594 return before.insert(len(before) + 1, text)
595
596
597 elif not before.iseditable and oelem.iseditable:
598
599 return oelem.insert(0, text)
600
601 return False
602
604 """Insert the given text between the two parameter C{StringElem}s."""
605 if not isinstance(left, StringElem) and left is not None:
606 raise ValueError('"left" is not a StringElem or None')
607 if not isinstance(right, StringElem) and right is not None:
608 raise ValueError('"right" is not a StringElem or None')
609 if left is right:
610 if left.sub:
611
612
613
614 raise ValueError('"left" and "right" refer to the same element and is not empty.')
615 if not left.iseditable:
616 return False
617 if isinstance(text, unicode):
618 text = StringElem(text)
619
620 if left is right:
621
622 left.sub.append(text)
623 return True
624
625
626
627
628 if left is None:
629 if self is right:
630
631 self.sub.insert(0, text)
632 return True
633 parent = self.get_parent_elem(right)
634 if parent is not None:
635
636 parent.sub.insert(0, text)
637 return True
638 return False
639
640 if right is None:
641 if self is left:
642
643 self.sub.append(text)
644 return True
645 parent = self.get_parent_elem(left)
646 if parent is not None:
647
648 parent.sub.append(text)
649 return True
650 return False
651
652
653
654
655 ischild = False
656 for sub in left.sub:
657 if right is sub:
658 ischild = True
659 break
660 if ischild:
661
662 left.sub.insert(0, text)
663 return True
664
665 ischild = False
666 for sub in right.sub:
667 if left is sub:
668 ischild = True
669 break
670 if ischild:
671
672 right.sub.append(text)
673 return True
674
675 parent = self.get_parent_elem(left)
676 if parent.iseditable:
677 idx = 1
678 for child in parent.sub:
679 if child is left:
680 break
681 idx += 1
682
683 parent.sub.insert(idx, text)
684 return True
685
686 parent = self.get_parent_elem(right)
687 if parent.iseditable:
688 idx = 0
689 for child in parent.sub:
690 if child is right:
691 break
692 idx += 1
693
694 parent.sub.insert(0, text)
695 return True
696
697 logging.debug('Could not insert between %s and %s... odd.' % (repr(left), repr(right)))
698 return False
699
701 """
702 Whether or not this instance is a leaf node in the C{StringElem} tree.
703
704 A node is a leaf node if it is a C{StringElem} (not a sub-class) and
705 contains only sub-elements of type C{str} or C{unicode}.
706
707 @rtype: bool
708 """
709 for e in self.sub:
710 if not isinstance(e, (str, unicode)):
711 return False
712 return True
713
728
729 - def map(self, f, filter=None):
730 """Apply C{f} to all nodes for which C{filter} returned C{True} (optional)."""
731 if filter is not None and not callable(filter):
732 raise ValueError('filter is not callable or None')
733 if filter is None:
734 filter = lambda e: True
735
736 for elem in self.depth_first():
737 if filter(elem):
738 f(elem)
739
740 @classmethod
742 """Parse an instance of this class from the start of the given string.
743 This method should be implemented by any sub-class that wants to
744 parseable by L{translate.storage.placeables.parse}.
745
746 @type pstr: unicode
747 @param pstr: The string to parse into an instance of this class.
748 @returns: An instance of the current class, or C{None} if the
749 string not parseable by this class."""
750 return cls(pstr)
751
753 """Print the tree from the current instance's point in an indented
754 manner."""
755 indent_prefix = " " * indent * 2
756 out = (u"%s%s [%s]" % (indent_prefix, self.__class__.__name__, unicode(self))).encode('utf-8')
757 if verbose:
758 out += u' ' + repr(self)
759
760 print out
761
762 for elem in self.sub:
763 if isinstance(elem, StringElem):
764 elem.print_tree(indent + 1, verbose=verbose)
765 else:
766 print (u'%s%s[%s]' % (indent_prefix, indent_prefix, elem)).encode('utf-8')
767
769 """Remove unnecessary nodes to make the tree optimal."""
770 changed = False
771 for elem in self.iter_depth_first():
772 if len(elem.sub) == 1:
773 child = elem.sub[0]
774
775
776 if type(child) is StringElem and child.isleaf():
777 elem.sub = child.sub
778
779
780 if type(elem) is StringElem and type(child) is StringElem:
781 elem.sub = child.sub
782 changed = True
783
784
785
786 if type(elem) is StringElem and isinstance(child, StringElem) and type(child) is not StringElem:
787 parent = self.get_parent_elem(elem)
788 if parent is not None:
789 parent.sub[parent.sub.index(elem)] = child
790 changed = True
791
792 if type(elem) is StringElem and elem.isleaf():
793
794 elem.sub = [u''.join(elem.sub)]
795
796 for i in reversed(range(len(elem.sub))):
797
798
799 if type(elem.sub[i]) in (StringElem, str, unicode) and len(elem.sub[i]) == 0:
800 del elem.sub[i]
801 continue
802
803 if type(elem.sub[i]) in (str, unicode) and not elem.isleaf():
804 elem.sub[i] = StringElem(elem.sub[i])
805 changed = True
806
807
808 if not elem.isleaf():
809 leafchanged = True
810 while leafchanged:
811 leafchanged = False
812
813 for i in range(len(elem.sub)-1):
814 lsub = elem.sub[i]
815 rsub = elem.sub[i+1]
816
817 if type(lsub) is StringElem and type(rsub) is StringElem:
818 changed = True
819 lsub.sub.extend(rsub.sub)
820 del elem.sub[i+1]
821 leafchanged = True
822 break
823
824
825
826
827 if changed:
828 self.prune()
829
830
832 """Replace nodes with type C{ptype} with base C{StringElem}s, containing
833 the same sub-elements. This is only applicable to elements below the
834 element tree root node."""
835 for elem in self.iter_depth_first():
836 if type(elem) is ptype:
837 parent = self.get_parent_elem(elem)
838 pindex = parent.sub.index(elem)
839 parent.sub[pindex] = StringElem(
840 sub=elem.sub,
841 id=elem.id,
842 xid=elem.xid,
843 rid=elem.rid,
844 )
845
847 """Transform the sub-tree according to some class-specific needs.
848 This method should be either overridden in implementing sub-classes
849 or dynamically replaced by specific applications.
850
851 @returns: The transformed Unicode string representing the sub-tree.
852 """
853 return self.copy()
854