Package translate :: Package storage :: Package placeables :: Module strelem
[hide private]
[frames] | no frames]

Source Code for Module translate.storage.placeables.strelem

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3  # 
  4  # Copyright 2009 Zuza Software Foundation 
  5  # 
  6  # This file is part of the Translate Toolkit. 
  7  # 
  8  # This program is free software; you can redistribute it and/or modify 
  9  # it under the terms of the GNU General Public License as published by 
 10  # the Free Software Foundation; either version 2 of the License, or 
 11  # (at your option) any later version. 
 12  # 
 13  # This program is distributed in the hope that it will be useful, 
 14  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 16  # GNU General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with this program; if not, see <http://www.gnu.org/licenses/>. 
 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 
28 29 30 -class ElementNotFoundError(ValueError):
31 pass
32
33 34 -class StringElem(object):
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 # INITIALIZERS #
57 - def __init__(self, sub=None, id=None, rid=None, xid=None, **kwargs):
58 if sub is None: 59 sub = [] 60 if isinstance(sub, (unicode, StringElem)): 61 sub = [sub] 62 63 for elem in sub: 64 if not isinstance(elem, (unicode, StringElem)): 65 raise ValueError(elem) 66 67 self.sub = sub 68 self.id = id 69 self.rid = rid 70 self.xid = xid 71 72 for key, value in kwargs.items(): 73 if hasattr(self, key): 74 raise ValueError('attribute already exists: %s' % (key)) 75 setattr(self, key, value) 76 77 self.prune()
78 79 # SPECIAL METHODS #
80 - def __add__(self, rhs):
81 """Emulate the C{unicode} class.""" 82 return unicode(self) + rhs
83
84 - def __contains__(self, item):
85 """Emulate the C{unicode} class.""" 86 return item in unicode(self)
87
88 - def __eq__(self, rhs):
89 """@returns: C{True} if (and only if) all members as well as sub-trees 90 are equal. False otherwise.""" 91 if not isinstance(rhs, StringElem): 92 return False 93 94 return self.id == rhs.id and \ 95 self.iseditable == rhs.iseditable and \ 96 self.istranslatable == rhs.istranslatable and \ 97 self.isvisible == rhs.isvisible and \ 98 self.rid == rhs.rid and \ 99 self.xid == rhs.xid and \ 100 len(self.sub) == len(rhs.sub) and \ 101 not [i for i in range(len(self.sub)) if self.sub[i] != rhs.sub[i]]
102
103 - def __ge__(self, rhs):
104 """Emulate the C{unicode} class.""" 105 return unicode(self) >= rhs
106
107 - def __getitem__(self, i):
108 """Emulate the C{unicode} class.""" 109 return unicode(self)[i]
110
111 - def __getslice__(self, i, j):
112 """Emulate the C{unicode} class.""" 113 return unicode(self)[i:j]
114
115 - def __gt__(self, rhs):
116 """Emulate the C{unicode} class.""" 117 return unicode(self) > rhs
118
119 - def __iter__(self):
120 """Create an iterator of this element's sub-elements.""" 121 for elem in self.sub: 122 yield elem
123
124 - def __le__(self, rhs):
125 """Emulate the C{unicode} class.""" 126 return unicode(self) <= rhs
127
128 - def __len__(self):
129 """Emulate the C{unicode} class.""" 130 return len(unicode(self))
131
132 - def __lt__(self, rhs):
133 """Emulate the C{unicode} class.""" 134 return unicode(self) < rhs
135
136 - def __mul__(self, rhs):
137 """Emulate the C{unicode} class.""" 138 return unicode(self) * rhs
139
140 - def __ne__(self, rhs):
141 return not self.__eq__(rhs)
142
143 - def __radd__(self, lhs):
144 """Emulate the C{unicode} class.""" 145 return self + lhs
146
147 - def __rmul__(self, lhs):
148 """Emulate the C{unicode} class.""" 149 return self * lhs
150
151 - def __repr__(self):
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
161 - def __str__(self):
162 if not self.isvisible: 163 return '' 164 return ''.join([unicode(elem).encode('utf-8') for elem in self.sub])
165
166 - def __unicode__(self):
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 # METHODS #
174 - def apply_to_strings(self, f):
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
183 - def copy(self):
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 #logging.debug('Copying instance of class %s' % (self.__class__.__name__)) 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
197 - def delete_elem(self, elem):
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
213 - def delete_range(self, start_index, end_index):
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 # If {start} is "between" elements, we use the one on the "right" 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 # If {end} is "between" elements, we use the one on the "left" 240 end['elem'] = end['elem'][0] 241 end['offset'] = end['offset'][0] 242 assert start['elem'].isleaf() and end['elem'].isleaf() 243 244 #logging.debug('FROM %s TO %s' % (start, end)) 245 246 # Ranges can be one of 3 types: 247 # 1) The entire string. 248 # 2) An entire element. 249 # 3) Restricted to a single element. 250 # 4) Spans multiple elements (start- and ending elements are not the same). 251 252 # Case 1: Entire string # 253 if start_index == 0 and end_index == len(self): 254 #logging.debug('Case 1: [%s]' % (unicode(self))) 255 removed = self.copy() 256 self.sub = [] 257 return removed, None, None 258 259 # Case 2: An entire element # 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 ##### FOR DEBUGGING ##### 263 #s = '' 264 #for e in self.flatten(): 265 # if e is start['elem']: 266 # s += '[' + unicode(e) + ']' 267 # else: 268 # s += unicode(e) 269 #logging.debug('Case 2: %s' % (s)) 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 # Filter out start['elem'] below with a list comprehension in stead 280 # of using parent.sub.remove(), becase list.remove() tests value 281 # and not identity, which is what we want here. This ensures that 282 # start['elem'] is removed and not the first element that is equal 283 # to it. 284 parent.sub = [i for i in parent.sub if i is not start['elem']] 285 return removed, parent, offset 286 287 # Case 3: Within a single element # 288 if start['elem'] is end['elem'] and start['elem'].iseditable: 289 ##### FOR DEBUGGING ##### 290 #s = '' 291 #for e in self.flatten(): 292 # if e is start['elem']: 293 # s += '%s[%s]%s' % ( 294 # e[:start['offset']], 295 # e[start['offset']:end['offset']], 296 # e[end['offset']:] 297 # ) 298 # else: 299 # s += unicode(e) 300 #logging.debug('Case 3: %s' % (s)) 301 ######################### 302 303 # XXX: This might not have the expected result if start['elem'] is a StringElem sub-class instance. 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 # Case 4: Across multiple elements # 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 #assert range_nodes[0] is start['elem'] and range_nodes[-1] is end['elem'] 326 #logging.debug("Nodes in delete range: %s" % (str(range_nodes))) 327 328 marked_nodes = [] # Contains nodes that have been marked for deletion (directly or inderectly (via parent)). 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 #logging.debug("Marking node: %s" % (subtree)) 335 marked_nodes.extend(subtree) # "subtree" does not include "node" 336 337 ##### FOR DEBUGGING ##### 338 #s = '' 339 #for e in self.flatten(): 340 # if e is start['elem']: 341 # s += '%s[%s' % (e[:start['offset']], e[start['offset']:]) 342 # elif e is end['elem']: 343 # s += '%s]%s' % (e[:end['offset']], e[end['offset']:]) 344 # else: 345 # s += unicode(e) 346 #logging.debug('Case 4: %s' % (s)) 347 ######################### 348 349 removed = self.copy() 350 351 # Save offsets before we start changing the tree 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
375 - def depth_first(self, filter=None):
376 """Returns a list of the nodes in the tree in depth-first order.""" 377 if filter is None or not callable(filter): 378 filter = lambda e: True 379 elems = [] 380 if filter(self): 381 elems.append(self) 382 383 for sub in self.sub: 384 if not isinstance(sub, StringElem): 385 continue 386 if sub.isleaf() and filter(sub): 387 elems.append(sub) 388 else: 389 elems.extend(sub.depth_first()) 390 return elems
391
392 - def encode(self, encoding=sys.getdefaultencoding()):
393 """More C{unicode} class emulation.""" 394 return unicode(self).encode(encoding)
395
396 - def elem_offset(self, elem):
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 # If we can't find the same instance element, settle for one that looks like it 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
424 - def elem_at_offset(self, offset):
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
439 - def find(self, x):
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
448 - def find_elems_with(self, x):
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
452 - def flatten(self, filter=None):
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
458 - def get_ancestor_where(self, child, criteria):
459 parent = self.get_parent_elem(child) 460 if parent is None or criteria(parent): 461 return parent 462 return self.get_ancestor_where(parent, criteria)
463
464 - def get_index_data(self, index):
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 # Check if there "index" is actually between elements 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
484 - def get_parent_elem(self, child):
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 # There are 4 general cases (including specific cases) where text can be inserted: 511 # 1) At the beginning of the string (self) 512 # 1.1) self.sub[0] is editable 513 # 1.2) self.sub[0] is not editable 514 # 2) At the end of the string (self) 515 # 3) In the middle of a node 516 # 4) Between two nodes 517 # 4.1) Neither of the nodes are editable 518 # 4.2) Both nodes are editable 519 # 4.3) Node at offset-1 is editable, node at offset is not 520 # 4.4) Node at offset is editable, node at offset-1 is not 521 522 oelem = self.elem_at_offset(offset) 523 524 # Case 1 # 525 if offset == 0: 526 # 1.1 # 527 if oelem.iseditable: 528 #logging.debug('Case 1.1') 529 oelem.sub.insert(0, checkleaf(oelem, text)) 530 oelem.prune() 531 return True 532 # 1.2 # 533 else: 534 #logging.debug('Case 1.2') 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 # Case 2 # 545 if offset >= len(self): 546 #logging.debug('Case 2') 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 # Case 3 # 557 if oelem is before: 558 if oelem.iseditable: 559 #logging.debug('Case 3') 560 eoffset = offset - self.elem_offset(oelem) 561 if oelem.isleaf(): 562 s = unicode(oelem) # Collapse all sibling strings into one 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 # And the only case left: Case 4 # 575 # 4.1 # 576 if not before.iseditable and not oelem.iseditable: 577 #logging.debug('Case 4.1') 578 # Neither are editable, so we add it as a sibling (to the right) of before 579 bparent = self.get_parent_elem(before) 580 # bparent cannot be a leaf (because it has before as a child), so we 581 # insert the text as StringElem(text) 582 bindex = bparent.sub.index(before) 583 bparent.sub.insert(bindex + 1, text) 584 return True 585 586 # 4.2 # 587 elif before.iseditable and oelem.iseditable: 588 #logging.debug('Case 4.2') 589 return before.insert(len(before)+1, text) # Reinterpret as a case 2 590 591 # 4.3 # 592 elif before.iseditable and not oelem.iseditable: 593 #logging.debug('Case 4.3') 594 return before.insert(len(before)+1, text) # Reinterpret as a case 2 595 596 # 4.4 # 597 elif not before.iseditable and oelem.iseditable: 598 #logging.debug('Case 4.4') 599 return oelem.insert(0, text) # Reinterpret as a case 1 600 601 return False
602
603 - def insert_between(self, left, right, text):
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 # This is an error because the cursor cannot be inside an element ("left is right"), 612 # if it has any other content. If an element has content, it will be at least directly 613 # left or directly right of the current cursor position. 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 #logging.debug('left%s.sub.append(%s)' % (repr(left), repr(text))) 622 left.sub.append(text) 623 return True 624 # XXX: The "in" keyword is *not* used below, because the "in" tests 625 # with __eq__ and not "is", as we do below. Testing for identity is 626 # intentional and required. 627 628 if left is None: 629 if self is right: 630 #logging.debug('self%s.sub.insert(0, %s)' % (repr(self), repr(text))) 631 self.sub.insert(0, text) 632 return True 633 parent = self.get_parent_elem(right) 634 if parent is not None: 635 #logging.debug('parent%s.sub.insert(0, %s)' % (repr(parent), repr(text))) 636 parent.sub.insert(0, text) 637 return True 638 return False 639 640 if right is None: 641 if self is left: 642 #logging.debug('self%s.sub.append(%s)' % (repr(self), repr(text))) 643 self.sub.append(text) 644 return True 645 parent = self.get_parent_elem(left) 646 if parent is not None: 647 #logging.debug('parent%s.sub.append(%s)' % (repr(parent), repr(text))) 648 parent.sub.append(text) 649 return True 650 return False 651 652 # The following two blocks handle the cases where one element 653 # "surrounds" another as its parent. In that way the parent would be 654 # "left" of its first child, like in the first case. 655 ischild = False 656 for sub in left.sub: 657 if right is sub: 658 ischild = True 659 break 660 if ischild: 661 #logging.debug('left%s.sub.insert(0, %s)' % (repr(left), repr(text))) 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 #logging.debug('right%s.sub.append(%s)' % (repr(right), repr(text))) 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 #logging.debug('parent%s.sub.insert(%d, %s)' % (repr(parent), idx, repr(text))) 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 #logging.debug('parent%s.sub.insert(%d, %s)' % (repr(parent), idx, repr(text))) 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
700 - def isleaf(self):
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
714 - def iter_depth_first(self, filter=None):
715 """Iterate through the nodes in the tree in dept-first order.""" 716 if filter is None or not callable(filter): 717 filter = lambda e: True 718 if filter(self): 719 yield self 720 for sub in self.sub: 721 if not isinstance(sub, StringElem): 722 continue 723 if sub.isleaf() and filter(sub): 724 yield sub 725 else: 726 for node in sub.iter_depth_first(): 727 if filter(node): 728 yield node
729
730 - def map(self, f, filter=None):
731 """Apply C{f} to all nodes for which C{filter} returned C{True} (optional).""" 732 if filter is not None and not callable(filter): 733 raise ValueError('filter is not callable or None') 734 if filter is None: 735 filter = lambda e: True 736 737 for elem in self.depth_first(): 738 if filter(elem): 739 f(elem)
740 741 @classmethod
742 - def parse(cls, pstr):
743 """Parse an instance of this class from the start of the given string. 744 This method should be implemented by any sub-class that wants to 745 parseable by L{translate.storage.placeables.parse}. 746 747 @type pstr: unicode 748 @param pstr: The string to parse into an instance of this class. 749 @returns: An instance of the current class, or C{None} if the 750 string not parseable by this class.""" 751 return cls(pstr)
752
753 - def print_tree(self, indent=0, verbose=False):
754 """Print the tree from the current instance's point in an indented 755 manner.""" 756 indent_prefix = " " * indent * 2 757 out = (u"%s%s [%s]" % (indent_prefix, self.__class__.__name__, unicode(self))).encode('utf-8') 758 if verbose: 759 out += u' ' + repr(self) 760 761 print out 762 763 for elem in self.sub: 764 if isinstance(elem, StringElem): 765 elem.print_tree(indent+1, verbose=verbose) 766 else: 767 print (u'%s%s[%s]' % (indent_prefix, indent_prefix, elem)).encode('utf-8')
768
769 - def prune(self):
770 """Remove unnecessary nodes to make the tree optimal.""" 771 changed = False 772 for elem in self.iter_depth_first(): 773 if len(elem.sub) == 1: 774 child = elem.sub[0] 775 # Symbolically: X->StringElem(leaf) => X(leaf) 776 # (where X is any sub-class of StringElem, but not StringElem) 777 if type(child) is StringElem and child.isleaf(): 778 elem.sub = child.sub 779 780 # Symbolically: StringElem->StringElem2->(leaves) => StringElem->(leaves) 781 if type(elem) is StringElem and type(child) is StringElem: 782 elem.sub = child.sub 783 changed = True 784 785 # Symbolically: StringElem->X(leaf) => X(leaf) 786 # (where X is any sub-class of StringElem, but not StringElem) 787 if type(elem) is StringElem and isinstance(child, StringElem) and type(child) is not StringElem: 788 parent = self.get_parent_elem(elem) 789 if parent is not None: 790 parent.sub[parent.sub.index(elem)] = child 791 changed = True 792 793 if type(elem) is StringElem and elem.isleaf(): 794 # Collapse all strings in this leaf into one string. 795 elem.sub = [u''.join(elem.sub)] 796 797 for i in reversed(range(len(elem.sub))): 798 # Remove empty strings or StringElem nodes 799 # (but not StringElem sub-class instances, because they might contain important (non-rendered) data. 800 if type(elem.sub[i]) in (StringElem, str, unicode) and len(elem.sub[i]) == 0: 801 del elem.sub[i] 802 continue 803 804 if type(elem.sub[i]) in (str, unicode) and not elem.isleaf(): 805 elem.sub[i] = StringElem(elem.sub[i]) 806 changed = True 807 808 # Merge sibling StringElem leaves 809 if not elem.isleaf(): 810 leafchanged = True 811 while leafchanged: 812 leafchanged = False 813 814 for i in range(len(elem.sub)-1): 815 lsub = elem.sub[i] 816 rsub = elem.sub[i+1] 817 818 if type(lsub) is StringElem and type(rsub) is StringElem: 819 changed = True 820 lsub.sub.extend(rsub.sub) 821 del elem.sub[i+1] 822 leafchanged = True 823 break 824 825 # If any changes were made, call prune() again to make sure that 826 # changes made later does not create situations fixed by earlier 827 # checks. 828 if changed: 829 self.prune()
830 831 # TODO: Write unit test for this method
832 - def remove_type(self, ptype):
833 """Replace nodes with type C{ptype} with base C{StringElem}s, containing 834 the same sub-elements. This is only applicable to elements below the 835 element tree root node.""" 836 for elem in self.iter_depth_first(): 837 if type(elem) is ptype: 838 parent = self.get_parent_elem(elem) 839 pindex = parent.sub.index(elem) 840 parent.sub[pindex] = StringElem( 841 sub=elem.sub, 842 id=elem.id, 843 xid=elem.xid, 844 rid=elem.rid 845 )
846
847 - def translate(self):
848 """Transform the sub-tree according to some class-specific needs. 849 This method should be either overridden in implementing sub-classes 850 or dynamically replaced by specific applications. 851 852 @returns: The transformed Unicode string representing the sub-tree. 853 """ 854 return self.copy()
855