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
323 marked_nodes = []
324 for node in range_nodes[1:-1]:
325 if [n for n in marked_nodes if n is node]:
326 continue
327 subtree = node.depth_first()
328 if not [e for e in subtree if e is end['elem']]:
329
330 marked_nodes.extend(subtree)
331
332
333
334
335
336
337
338
339
340
341
342
343
344 removed = self.copy()
345
346
347 start_offset = self.elem_offset(start['elem'])
348 end_offset = self.elem_offset(end['elem'])
349
350 for node in marked_nodes:
351 try:
352 self.delete_elem(node)
353 except ElementNotFoundError, e:
354 pass
355
356 if start['elem'] is not end['elem']:
357 if start_offset == start['index'] or (not start['elem'].iseditable and start['elem'].isfragile):
358 self.delete_elem(start['elem'])
359 elif start['elem'].iseditable:
360 start['elem'].sub = [ u''.join(start['elem'].sub)[:start['offset']] ]
361
362 if end_offset + len(end['elem']) == end['index'] or (not end['elem'].iseditable and end['elem'].isfragile):
363 self.delete_elem(end['elem'])
364 elif end['elem'].iseditable:
365 end['elem'].sub = [ u''.join(end['elem'].sub)[end['offset']:] ]
366
367 self.prune()
368 return removed, None, None
369
386
387 - def encode(self, encoding=sys.getdefaultencoding()):
388 """More C{unicode} class emulation."""
389 return unicode(self).encode(encoding)
390
392 """Find the offset of C{elem} in the current tree.
393 This cannot be reliably used if C{self.renderer} is used and even
394 less so if the rendering function renders the string differently
395 upon different calls. In Virtaal the C{StringElemGUI.index()} method
396 is used as replacement for this one.
397 @returns: The string index where element C{e} starts, or -1 if C{e}
398 was not found."""
399 offset = 0
400 for e in self.iter_depth_first():
401 if e is elem:
402 return offset
403 if e.isleaf():
404 offset += len(e)
405
406
407 offset = 0
408 for e in self.iter_depth_first():
409 if e.isleaf():
410 leafoffset = 0
411 for s in e.sub:
412 if unicode(s) == unicode(elem):
413 return offset + leafoffset
414 else:
415 leafoffset += len(unicode(s))
416 offset += len(e)
417 return -1
418
420 """Get the C{StringElem} in the tree that contains the string rendered
421 at the given offset."""
422 if offset < 0 or offset > len(self):
423 return None
424
425 length = 0
426 elem = None
427 for elem in self.flatten():
428 elem_len = len(elem)
429 if length <= offset < length+elem_len:
430 return elem
431 length += elem_len
432 return elem
433
435 """Find sub-string C{x} in this string tree and return the position
436 at which it starts."""
437 if isinstance(x, basestring):
438 return unicode(self).find(x)
439 if isinstance(x, StringElem):
440 return unicode(self).find(unicode(x))
441 return None
442
444 """Find all elements in the current sub-tree containing C{x}."""
445 return [elem for elem in self.flatten() if x in unicode(elem)]
446
448 """Flatten the tree by returning a depth-first search over the tree's leaves."""
449 if filter is None or not callable(filter):
450 filter = lambda e: True
451 return [elem for elem in self.iter_depth_first(lambda e: e.isleaf() and filter(e))]
452
458
460 """Get info about the specified range in the tree.
461 @returns: A dictionary with the following items:
462 * I{elem}: The element in which C{index} resides.
463 * I{index}: Copy of the C{index} parameter
464 * I{offset}: The offset of C{index} into C{'elem'}."""
465 info = {
466 'elem': self.elem_at_offset(index),
467 'index': index,
468 }
469 info['offset'] = info['index'] - self.elem_offset(info['elem'])
470
471
472 leftelem = self.elem_at_offset(index - 1)
473 if leftelem is not None and leftelem is not info['elem']:
474 info['elem'] = (leftelem, info['elem'])
475 info['offset'] = (len(leftelem), 0)
476
477 return info
478
480 """Searches the current sub-tree for and returns the parent of the
481 C{child} element."""
482 for elem in self.iter_depth_first():
483 if not isinstance(elem, StringElem):
484 continue
485 for sub in elem.sub:
486 if sub is child:
487 return elem
488 return None
489
490 - def insert(self, offset, text):
491 """Insert the given text at the specified offset of this string-tree's
492 string (Unicode) representation."""
493 if offset < 0 or offset > len(self) + 1:
494 raise IndexError('Index out of range: %d' % (offset))
495 if isinstance(text, (str, unicode)):
496 text = StringElem(text)
497 if not isinstance(text, StringElem):
498 raise ValueError('text must be of type StringElem')
499
500 def checkleaf(elem, text):
501 if elem.isleaf() and type(text) is StringElem and text.isleaf():
502 return unicode(text)
503 return text
504
505
506
507
508
509
510
511
512
513
514
515
516
517 oelem = self.elem_at_offset(offset)
518
519
520 if offset == 0:
521
522 if oelem.iseditable:
523
524 oelem.sub.insert(0, checkleaf(oelem, text))
525 oelem.prune()
526 return True
527
528 else:
529
530 oparent = self.get_ancestor_where(oelem, lambda x: x.iseditable)
531 if oparent is not None:
532 oparent.sub.insert(0, checkleaf(oparent, text))
533 return True
534 else:
535 self.sub.insert(0, checkleaf(self, text))
536 return True
537 return False
538
539
540 if offset >= len(self):
541
542 last = self.flatten()[-1]
543 parent = self.get_ancestor_where(last, lambda x: x.iseditable)
544 if parent is None:
545 parent = self
546 parent.sub.append(checkleaf(parent, text))
547 return True
548
549 before = self.elem_at_offset(offset-1)
550
551
552 if oelem is before:
553 if oelem.iseditable:
554
555 eoffset = offset - self.elem_offset(oelem)
556 if oelem.isleaf():
557 s = unicode(oelem)
558 head = s[:eoffset]
559 tail = s[eoffset:]
560 if type(text) is StringElem and text.isleaf():
561 oelem.sub = [head + unicode(text) + tail]
562 else:
563 oelem.sub = [StringElem(head), text, StringElem(tail)]
564 return True
565 else:
566 return oelem.insert(eoffset, text)
567 return False
568
569
570
571 if not before.iseditable and not oelem.iseditable:
572
573
574 bparent = self.get_parent_elem(before)
575
576
577 bindex = bparent.sub.index(before)
578 bparent.sub.insert(bindex + 1, text)
579 return True
580
581
582 elif before.iseditable and oelem.iseditable:
583
584 return before.insert(len(before)+1, text)
585
586
587 elif before.iseditable and not oelem.iseditable:
588
589 return before.insert(len(before)+1, text)
590
591
592 elif not before.iseditable and oelem.iseditable:
593
594 return oelem.insert(0, text)
595
596 return False
597
599 """Insert the given text between the two parameter C{StringElem}s."""
600 if not isinstance(left, StringElem) and left is not None:
601 raise ValueError('"left" is not a StringElem or None')
602 if not isinstance(right, StringElem) and right is not None:
603 raise ValueError('"right" is not a StringElem or None')
604 if left is right:
605 if left.sub:
606
607
608
609 raise ValueError('"left" and "right" refer to the same element and is not empty.')
610 if not left.iseditable:
611 return False
612 if isinstance(text, unicode):
613 text = StringElem(text)
614
615 if left is right:
616
617 left.sub.append(text)
618 return True
619
620
621
622
623 if left is None:
624 if self is right:
625
626 self.sub.insert(0, text)
627 return True
628 parent = self.get_parent_elem(right)
629 if parent is not None:
630
631 parent.sub.insert(0, text)
632 return True
633 return False
634
635 if right is None:
636 if self is left:
637
638 self.sub.append(text)
639 return True
640 parent = self.get_parent_elem(left)
641 if parent is not None:
642
643 parent.sub.append(text)
644 return True
645 return False
646
647
648
649
650 ischild = False
651 for sub in left.sub:
652 if right is sub:
653 ischild = True
654 break
655 if ischild:
656
657 left.sub.insert(0, text)
658 return True
659
660 ischild = False
661 for sub in right.sub:
662 if left is sub:
663 ischild = True
664 break
665 if ischild:
666
667 right.sub.append(text)
668 return True
669
670 parent = self.get_parent_elem(left)
671 if parent.iseditable:
672 idx = 1
673 for child in parent.sub:
674 if child is left:
675 break
676 idx += 1
677
678 parent.sub.insert(idx, text)
679 return True
680
681 parent = self.get_parent_elem(right)
682 if parent.iseditable:
683 idx = 0
684 for child in parent.sub:
685 if child is right:
686 break
687 idx += 1
688
689 parent.sub.insert(0, text)
690 return True
691
692 logging.debug('Could not insert between %s and %s... odd.' % (repr(left), repr(right)))
693 return False
694
696 """
697 Whether or not this instance is a leaf node in the C{StringElem} tree.
698
699 A node is a leaf node if it is a C{StringElem} (not a sub-class) and
700 contains only sub-elements of type C{str} or C{unicode}.
701
702 @rtype: bool
703 """
704 for e in self.sub:
705 if not isinstance(e, (str, unicode)):
706 return False
707 return True
708
724
725 - def map(self, f, filter=None):
726 """Apply C{f} to all nodes for which C{filter} returned C{True} (optional)."""
727 if filter is not None and not callable(filter):
728 raise ValueError('filter is not callable or None')
729 if filter is None:
730 filter = lambda e: True
731
732 for elem in self.depth_first():
733 if filter(elem):
734 f(elem)
735
736 @classmethod
738 """Parse an instance of this class from the start of the given string.
739 This method should be implemented by any sub-class that wants to
740 parseable by L{translate.storage.placeables.parse}.
741
742 @type pstr: unicode
743 @param pstr: The string to parse into an instance of this class.
744 @returns: An instance of the current class, or C{None} if the
745 string not parseable by this class."""
746 return cls(pstr)
747
749 """Print the tree from the current instance's point in an indented
750 manner."""
751 indent_prefix = " " * indent * 2
752 out = (u"%s%s [%s]" % (indent_prefix, self.__class__.__name__, unicode(self))).encode('utf-8')
753 if verbose:
754 out += u' ' + repr(self)
755
756 print out
757
758 for elem in self.sub:
759 if isinstance(elem, StringElem):
760 elem.print_tree(indent+1, verbose=verbose)
761 else:
762 print (u'%s%s[%s]' % (indent_prefix, indent_prefix, elem)).encode('utf-8')
763
814
815
817 """Replace nodes with type C{ptype} with base C{StringElem}s, containing
818 the same sub-elements. This is only applicable to elements below the
819 element tree root node."""
820 for elem in self.iter_depth_first():
821 if type(elem) is ptype:
822 parent = self.get_parent_elem(elem)
823 pindex = parent.sub.index(elem)
824 parent.sub[pindex] = StringElem(
825 sub=elem.sub,
826 id=elem.id,
827 xid=elem.xid,
828 rid=elem.rid
829 )
830
832 """Transform the sub-tree according to some class-specific needs.
833 This method should be either overridden in implementing sub-classes
834 or dynamically replaced by specific applications.
835
836 @returns: The transformed Unicode string representing the sub-tree.
837 """
838 return self.copy()
839