translate.storage.placeables.strelem

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              parent.sub.remove(start['elem'])
    280              return removed, parent, offset
    281
    282          # Case 3: Within a single element #
    283          if start['elem'] is end['elem'] and start['elem'].iseditable:
    284              ##### FOR DEBUGGING #####
    285              #s = ''
    286              #for e in self.flatten():
    287              #    if e is start['elem']:
    288              #        s += '%s[%s]%s' % (
    289              #            e[:start['offset']],
    290              #            e[start['offset']:end['offset']],
    291              #            e[end['offset']:]
    292              #        )
    293              #    else:
    294              #        s += unicode(e)
    295              #logging.debug('Case 3: %s' % (s))
    296              #########################
    297
    298              # XXX: This might not have the expected result if start['elem'] is a StringElem sub-class instance.
    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          # Case 4: Across multiple elements #
    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          #assert range_nodes[0] is start['elem'] and range_nodes[-1] is end['elem']
    321          #logging.debug("Nodes in delete range: %s" % (str(range_nodes)))
    322
    323          marked_nodes = [] # Contains nodes that have been marked for deletion (directly or inderectly (via parent)).
    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                  #logging.debug("Marking node: %s" % (subtree))
    330                  marked_nodes.extend(subtree) # "subtree" does not include "node"
    331
    332          ##### FOR DEBUGGING #####
    333          #s = ''
    334          #for e in self.flatten():
    335          #    if e is start['elem']:
    336          #        s += '%s[%s' % (e[:start['offset']], e[start['offset']:])
    337          #    elif e is end['elem']:
    338          #        s += '%s]%s' % (e[:end['offset']], e[end['offset']:])
    339          #    else:
    340          #        s += unicode(e)
    341          #logging.debug('Case 4: %s' % (s))
    342          #########################
    343
    344          removed = self.copy()
    345
    346          # Save offsets before we start changing the tree
    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
    370 -    def depth_first(self, filter=None):
    371          """Returns a list of the nodes in the tree in depth-first order."""
    372          if filter is None or not callable(filter):
    373              filter = lambda e: True
    374          elems = []
    375          if filter(self):
    376              elems.append(self)
    377
    378          for sub in self.sub:
    379              if not isinstance(sub, StringElem):
    380                  continue
    381              if sub.isleaf() and filter(sub):
    382                  elems.append(sub)
    383              else:
    384                  elems.extend(sub.depth_first())
    385          return elems
    386
    387 -    def encode(self, encoding=sys.getdefaultencoding()):
    388          """More C{unicode} class emulation."""
    389          return unicode(self).encode(encoding)
    390
    391 -    def elem_offset(self, elem):
    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          # If we can't find the same instance element, settle for one that looks like it
    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
    419 -    def elem_at_offset(self, offset):
    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
    434 -    def find(self, x):
    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
    443 -    def find_elems_with(self, x):
    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
    447 -    def flatten(self, filter=None):
    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
    453 -    def get_ancestor_where(self, child, criteria):
    454          parent = self.get_parent_elem(child)
    455          if parent is None or criteria(parent):
    456              return parent
    457          return self.get_ancestor_where(parent, criteria)
    458
    459 -    def get_index_data(self, index):
    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          # Check if there "index" is actually between elements
    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
    479 -    def get_parent_elem(self, child):
    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          # There are 4 general cases (including specific cases) where text can be inserted:
    506          # 1) At the beginning of the string (self)
    507          # 1.1) self.sub[0] is editable
    508          # 1.2) self.sub[0] is not editable
    509          # 2) At the end of the string (self)
    510          # 3) In the middle of a node
    511          # 4) Between two nodes
    512          # 4.1) Neither of the nodes are editable
    513          # 4.2) Both nodes are editable
    514          # 4.3) Node at offset-1 is editable, node at offset is not
    515          # 4.4) Node at offset is editable, node at offset-1 is not
    516
    517          oelem = self.elem_at_offset(offset)
    518
    519          # Case 1 #
    520          if offset == 0:
    521              # 1.1 #
    522              if oelem.iseditable:
    523                  #logging.debug('Case 1.1')
    524                  oelem.sub.insert(0, checkleaf(oelem, text))
    525                  oelem.prune()
    526                  return True
    527              # 1.2 #
    528              else:
    529                  #logging.debug('Case 1.2')
    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          # Case 2 #
    540          if offset >= len(self):
    541              #logging.debug('Case 2')
    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          # Case 3 #
    552          if oelem is before:
    553              if oelem.iseditable:
    554                  #logging.debug('Case 3')
    555                  eoffset = offset - self.elem_offset(oelem)
    556                  if oelem.isleaf():
    557                      s = unicode(oelem) # Collapse all sibling strings into one
    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          # And the only case left: Case 4 #
    570          # 4.1 #
    571          if not before.iseditable and not oelem.iseditable:
    572              #logging.debug('Case 4.1')
    573              # Neither are editable, so we add it as a sibling (to the right) of before
    574              bparent = self.get_parent_elem(before)
    575              # bparent cannot be a leaf (because it has before as a child), so we
    576              # insert the text as StringElem(text)
    577              bindex = bparent.sub.index(before)
    578              bparent.sub.insert(bindex + 1, text)
    579              return True
    580
    581          # 4.2 #
    582          elif before.iseditable and oelem.iseditable:
    583              #logging.debug('Case 4.2')
    584              return before.insert(len(before)+1, text) # Reinterpret as a case 2
    585
    586          # 4.3 #
    587          elif before.iseditable and not oelem.iseditable:
    588              #logging.debug('Case 4.3')
    589              return before.insert(len(before)+1, text) # Reinterpret as a case 2
    590
    591          # 4.4 #
    592          elif not before.iseditable and oelem.iseditable:
    593              #logging.debug('Case 4.4')
    594              return oelem.insert(0, text) # Reinterpret as a case 1
    595
    596          return False
    597
    598 -    def insert_between(self, left, right, text):
    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                  # This is an error because the cursor cannot be inside an element ("left is right"),
    607                  # if it has any other content. If an element has content, it will be at least directly
    608                  # left or directly right of the current cursor position.
    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              #logging.debug('left%s.sub.append(%s)' % (repr(left), repr(text)))
    617              left.sub.append(text)
    618              return True
    619          # XXX: The "in" keyword is *not* used below, because the "in" tests
    620          # with __eq__ and not "is", as we do below. Testing for identity is
    621          # intentional and required.
    622
    623          if left is None:
    624              if self is right:
    625                  #logging.debug('self%s.sub.insert(0, %s)' % (repr(self), repr(text)))
    626                  self.sub.insert(0, text)
    627                  return True
    628              parent = self.get_parent_elem(right)
    629              if parent is not None:
    630                  #logging.debug('parent%s.sub.insert(0, %s)' % (repr(parent), repr(text)))
    631                  parent.sub.insert(0, text)
    632                  return True
    633              return False
    634
    635          if right is None:
    636              if self is left:
    637                  #logging.debug('self%s.sub.append(%s)' % (repr(self), repr(text)))
    638                  self.sub.append(text)
    639                  return True
    640              parent = self.get_parent_elem(left)
    641              if parent is not None:
    642                  #logging.debug('parent%s.sub.append(%s)' % (repr(parent), repr(text)))
    643                  parent.sub.append(text)
    644                  return True
    645              return False
    646
    647          # The following two blocks handle the cases where one element
    648          # "surrounds" another as its parent. In that way the parent would be
    649          # "left" of its first child, like in the first case.
    650          ischild = False
    651          for sub in left.sub:
    652              if right is sub:
    653                  ischild = True
    654                  break
    655          if ischild:
    656              #logging.debug('left%s.sub.insert(0, %s)' % (repr(left), repr(text)))
    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              #logging.debug('right%s.sub.append(%s)' % (repr(right), repr(text)))
    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              #logging.debug('parent%s.sub.insert(%d, %s)' % (repr(parent), idx, repr(text)))
    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              #logging.debug('parent%s.sub.insert(%d, %s)' % (repr(parent), idx, repr(text)))
    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
    695 -    def isleaf(self):
    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
    709 -    def iter_depth_first(self, filter=None):
    710          """Iterate through the nodes in the tree in dept-first order."""
    711          if filter is None or not callable(filter):
    712              filter = lambda e: True
    713          if filter(self):
    714              yield self
    715          for sub in self.sub:
    716              if not isinstance(sub, StringElem):
    717                  continue
    718              if sub.isleaf() and filter(sub):
    719                  yield sub
    720              else:
    721                  for node in sub.iter_depth_first():
    722                      if filter(node):
    723                          yield node
    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
    737 -    def parse(cls, pstr):
    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
    748 -    def print_tree(self, indent=0, verbose=False):
    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
    764 -    def prune(self):
    765          """Remove unnecessary nodes to make the tree optimal."""
    766          for elem in self.iter_depth_first():
    767              if len(elem.sub) == 1:
    768                  child = elem.sub[0]
    769                  # Symbolically: X->StringElem(leaf) => X(leaf)
    770                  #   (where X is any sub-class of StringElem, but not StringElem)
    771                  if type(child) is StringElem and child.isleaf():
    772                      elem.sub = child.sub
    773
    774                  # Symbolically: StringElem->StringElem2->(leaves) => StringElem->(leaves)
    775                  if type(elem) is StringElem and type(child) is StringElem:
    776                      elem.sub = child.sub
    777
    778                  # Symbolically: StringElem->X(leaf) => X(leaf)
    779                  #   (where X is any sub-class of StringElem, but not StringElem)
    780                  if type(elem) is StringElem and isinstance(child, StringElem) and type(child) is not StringElem:
    781                      parent = self.get_parent_elem(elem)
    782                      if parent is not None:
    783                          parent.sub[parent.sub.index(elem)] = child
    784
    785              if type(elem) is StringElem and elem.isleaf():
    786                  # Collapse all strings in this leaf into one string.
    787                  elem.sub = [u''.join(elem.sub)]
    788
    789              for i in reversed(range(len(elem.sub))):
    790                  # Remove empty strings or StringElem nodes
    791                  # (but not StringElem sub-class instances, because they might contain important (non-rendered) data.
    792                  if type(elem.sub[i]) in (StringElem, str, unicode) and len(elem.sub[i]) == 0:
    793                      del elem.sub[i]
    794                      continue
    795
    796                  if type(elem.sub[i]) in (str, unicode) and not elem.isleaf():
    797                      elem.sub[i] = StringElem(elem.sub[i])
    798
    799              # Merge sibling StringElem leaves
    800              if not elem.isleaf():
    801                  changed = True
    802                  while changed:
    803                      changed = False
    804
    805                      for i in range(len(elem.sub)-1):
    806                          lsub = elem.sub[i]
    807                          rsub = elem.sub[i+1]
    808
    809                          if type(lsub) is StringElem and type(rsub) is StringElem:
    810                              lsub.sub.extend(rsub.sub)
    811                              del elem.sub[i+1]
    812                              changed = True
    813                              break
    814
    815      # TODO: Write unit test for this method
    816 -    def remove_type(self, ptype):
    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
    831 -    def translate(self):
    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