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              # 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