Source code for lexor.core.node

"""
This module defines the basic object of the document object model
(DOM).

"""
import os
import sys
from cStringIO import StringIO
LC = sys.modules['lexor.core']
# pylint: disable=too-many-public-methods
# pylint: disable=too-many-instance-attributes


def _write_node_info(node, strf):
    """`Node` helper function to write the node information in
    __repr__. """
    strf.write('%s%s' % ('    '*node.level, node.name))
    if not isinstance(node, LC.Element):
        strf.write('[0x%x]' % id(node))
    else:
        att = ' '.join(['%s="%s"' % (k, v) for k, v in node.items()])
        strf.write('[0x%x' % id(node))
        if att != '':
            strf.write(' %s]' % att)
        else:
            strf.write(']')
    if node.name == '#document':
        strf.write(': (%s:%s:%s)' % (node.uri, node.lang, node.style))
    else:
        strf.write(':')
    direction = 'r'
    if isinstance(node, LC.CharacterData):
        strf.write(' %r' % node.data)
    elif node.child:
        direction = 'd'
    strf.write('\n')
    return direction


def _set_owner_and_level(node, owner, level):
    """Helper method for increase_child_level. """
    if node.owner is not owner:
        if isinstance(node, LC.Element) and 'id' in node:
            try:
                del node.owner.id_dict[node['id']]
            except (KeyError, AttributeError):
                pass
            if owner:
                owner.id_dict[node['id']] = node
        node.owner = owner
    node.level = level
    if node.name == '#document':
        node.level = level - 1


[docs]class Node(object): """Primary datatype for the entire Document Object Model. """ __slots__ = ('name', 'owner', 'parent', 'index', 'prev', 'next', 'child', 'level')
[docs] def __init__(self): """Initializes all data descriptors to ``None``. Each descriptor has an associated `READ-ONLY` property. Read the comment on each property to see what each descriptor represents. """ self.name = None self.owner = None self.parent = None self.index = None self.prev = None self.next = None self.child = None self.level = 0
@property
[docs] def node_name(self): """ .. admonition:: Read-Only Property :class: note The name of this node. Its value depends on the node type. This property is associated with the attribute ``name``. """ return self.name
@property
[docs] def owner_document(self): """ .. admonition:: Read-Only Property :class: note The :class:`~lexor.core.elements.Document` in which this node resides. This property is associated with the attribute ``owner``. """ return self.owner
@property
[docs] def parent_node(self): """ .. admonition:: Read-Only Property :class: note The parent of this node. If the node has been just created or removed from a ``Document`` then this property is ``None``. This property is associated with the attribute ``parent``. """ return self.parent
@property
[docs] def node_index(self): """ .. admonition:: Read-Only Property :class: note The number of preceding siblings. >>> x is x.parent_node[x.node_index] True This property is associated with the attribute ``index``. """ return self.index
@property
[docs] def node_level(self): """ .. admonition:: Read-Only Property :class: note The nodes level of containtment in a :class:`~lexor.core.elements.Document` object. This property is associated with the attribute ``level``. """ return self.level
@property
[docs] def element_index(self): """ .. admonition:: Read-Only Property :class: note The number of preceding element siblings.""" index = 0 crt = self while crt.prev is not None: crt = crt.prev if isinstance(crt, LC.Element): index += 1 return index
@property
[docs] def previous_sibling(self): """ .. admonition:: Read-Only Property :class: note The node immediately preceding this node. If this property is not `None` then >>> x.previous_sibling <==> x.parent_node[x.node_index - 1] This property is associated with the attribute ``prev``.""" return self.prev
@property
[docs] def next_sibling(self): """ .. admonition:: Read-Only Property :class: note The node immediately following this node. If this property is not `None` then >>> x.next_sibling <==> x.parent_node[x.node_index + 1] This property is associated with the attribute ``next``.""" return self.next
@property
[docs] def previous_element(self): """ .. admonition:: Read-Only Property :class: note The last sibling :class:`~lexor.core.elements.Element` preceding this node. """ crt = self while crt.prev is not None: crt = crt.prev if isinstance(crt, LC.Element): return crt return None
@property
[docs] def next_element(self): """ .. admonition:: Read-Only Property :class: note The sibling :class:`~lexor.core.elements.Element` after this node. """ crt = self while crt.next is not None: crt = crt.next if isinstance(crt, LC.Element): return crt return None
[docs] def remove_children(self): """Remove all the child nodes. """ for child in self.child: child.disconnect() del self.child[:]
[docs] def __repr__(self): """>>> x.__repr__() == repr(x) True """ strf = StringIO() crt = self direction = _write_node_info(crt, strf) if direction is not 'd': return strf.getvalue() while True: if direction is 'd': crt = crt.child[0] elif direction is 'r': if crt.next is None: direction = 'u' continue crt = crt.next elif direction is 'u': if crt.parent is self: break if crt.parent.next is None: crt = crt.parent continue crt = crt.parent.next direction = _write_node_info(crt, strf) return strf.getvalue()
[docs] def __str__(self): """>>> x.__str__() == str(x) True """ if self.owner is None: style = 'default' lang = 'xml' else: style = self.owner.style lang = self.owner.lang writer = LC.Writer(lang, style) if self.owner is not None and self.owner.defaults is not None: for var, val in self.owner.defaults.iteritems(): writer.defaults[var] = os.path.expandvars(str(val)) writer.write(self) val = str(writer) writer.close() return val
[docs] def insert_before(self, index, new_child): """Inserts `new_child` to the list of children just before the child specified by `index`. """ if not isinstance(new_child, Node): new_child = LC.Text(str(new_child)) elif isinstance(new_child, LC.DocumentFragment): msg = "Use extend_before for LC.DocumentFragment Nodes." raise TypeError(msg) index = self.insert_node_before(index, new_child) while index < len(self.child): self[index].index = index index += 1 return self
[docs] def extend_before(self, index, new_children): """Inserts the contents of an iterable containing nodes just before the child specified by `index`. The following are equivalent: >>> while doc: node.parent.insert_before(index, doc[0]) >>> node.extend_before(index, doc) The second form, however, has a more efficient reindexing method. """ if isinstance(new_children, (list, LC.DocumentFragment)): for node in new_children: if node.name == '#document' and node.temporary: if self.owner: self.owner.meta.update(node.meta) node.meta = dict() while node: index = self.insert_node_before(index, node[0]) else: index = self.insert_node_before(index, node) else: if new_children.name == '#document': if new_children.temporary and self.owner: self.owner.meta.update(new_children.meta) new_children.meta = dict() while new_children: index = self.insert_node_before(index, new_children[0]) while index < len(self.child): self[index].index = index index += 1 return self
[docs] def append_child(self, new_child): """Adds the node `new_child` to the end of the list of children of this node. If the node is a :class:`~lexor.core.elements.DocumentFragment` then it appends its child nodes. Returns the calling node. """ if not isinstance(new_child, Node): new_child = LC.Text(str(new_child)) elif isinstance(new_child, LC.DocumentFragment): msg = "Use extend_children for `DocumentFragment` Nodes." raise TypeError(msg) self.append_child_node(new_child) return self
[docs] def extend_children(self, new_children): """Extend the list of children by appending children from an iterable containing nodes. """ if isinstance(new_children, (list, LC.DocumentFragment)): for node in new_children: if node.name == '#document' and node.temporary: if self.owner: self.owner.meta.update(node.meta) node.meta = dict() while node: self.append_child_node(node[0]) else: self.append_child_node(node) else: if new_children.name == '#document': if new_children.temporary and self.owner: self.owner.meta.update(new_children.meta) new_children.meta = dict() while new_children: self.append_child_node(new_children[0]) return self
[docs] def append_after(self, new_child): """Place `new_child` after the node. """ if self.index+1 == len(self.parent): self.parent.append_child(new_child) else: self.parent.insert_before(self.index+1, new_child)
[docs] def append_nodes_after(self, new_children): """Place `new_children` after the node. """ if self.index+1 == len(self.parent): self.parent.extend_children(new_children) else: self.parent.extend_before(self.index+1, new_children)
[docs] def prepend_before(self, new_child): """Place `new_child` before the node. """ self.parent.insert_before(self.index, new_child)
[docs] def prepend_nodes_before(self, new_children): """Place `new_children` before the node. """ self.parent.extend_before(self.index, new_children)
[docs] def normalize(self): """Removes empty :class:`~lexor.core.elements.Text` nodes, and joins adjacent :class:`~lexor.core.elements.Text` nodes.""" if not self.child: return self crt = self.child[0] while crt is not None: if isinstance(crt, LC.Text): if crt.data == '': nextnode = crt.next del crt.parent[crt.index] crt = nextnode elif isinstance(crt.next, LC.Text): marked_node = crt.next start = marked_node.index while isinstance(marked_node, LC.Text): crt.data += marked_node.data end = marked_node.index marked_node = marked_node.next crt = marked_node del self[start:end+1] else: crt = crt.next else: crt = crt.next return self
[docs] def __len__(self): """Return the number of child nodes. >>> x.__len__() == len(x) True """ if self.child is None: return 0 return len(self.child)
[docs] def __getitem__(self, i): """Return the `i`-th child of this node. >>> x.__getitem__(i) <==> x[i] >>> x.__getitem__(slice(i, j)) <==> x[i:j] >>> x.__getitem__(slice(i, j, dt)) <==> x[i:j:dt] When using a slice, the ``__getitem__`` function will return a list with references to the requested nodes. """ return self.child[i]
def _get_indices(self, i): """PRIVATE-METHOD: Returns a slice and the range of indices to be replaced. `i` is assumed to be a slice or an int. """ if isinstance(i, int): if i < 0: i += len(self.child) i = slice(i, i + 1) else: i = slice(i, i + 1) return i, xrange(*i.indices(len(self.child)))
[docs] def __delitem__(self, index): """Delete child nodes. >>> x.__delitem__(index) <==> del x[index] >>> x.__delitem__(slice(i, j)) <==> del x[i:j] >>> x.__delitem__(slice(i, j, dt)) <==> del x[i:j:dt] """ index, indices = self._get_indices(index) if index.step is None or index.step > 0: indices = reversed(indices) for index in indices: self.child[index].disconnect() del self.child[index] if index > 0: try: self.child[index].set_prev(self.child[index - 1]) except IndexError: self.child[index - 1].next = None elif self.child: self.child[index].prev = None for i in xrange(index, len(self.child)): self.child[i].index = i
[docs] def __setitem__(self, index, node): """Replace child nodes. >>> x.__setitem__(index) = node <==> x[index] = node >>> x.__setitem__(slice(i, j)) = dfrag <==> x[i:j] = dfrag >>> x.__setitem__(slice(i, j, dt)) = dfrag <==> x[i:j:dt] = dfrag When using slices the nodes to be assigned to the indices need to be contained in a :class:`~lexor.core.elements.DocumentFragment` node. This function does not support insertion as the regular slice for list does. To insert use a node use :meth:`insert_before` or :meth:`append_after`.""" indices = self._get_indices(index) if not isinstance(node, Node): raise TypeError("items must be Nodes") if node is self: raise TypeError("A node cannot have itself as a child.") if not isinstance(node, LC.DocumentFragment): # Better take a look at this, DocFrag needs more development nodes = LC.DocumentFragment(node) else: nodes = node if len(indices) != len(nodes): msg = "attempt to assign sequence of size %d to extended" \ "slice of size %d" % (len(nodes), len(indices)) raise ValueError(msg) for i in xrange(len(nodes)): index = indices[i] node = nodes[i] if node.parent is self: raise ValueError("Node is already the child at index %d" % node.index) if node.parent is not None: del node.parent[node.index] # Disconnect the current Node self.child[index].disconnect() # Assign and connect the new Node self.child[index] = node node.set_parent(self, index) try: node.set_next(self.child[index + 1]) except IndexError: pass try: node.set_prev(self.child[index - 1]) except IndexError: pass return node
[docs] def get_nodes_by_name(self, name): """Return a ``list`` of child nodes that have the given `name`. """ nodes = [] if not self.child: return nodes crt = self direction = 'd' while True: if direction is 'd': crt = crt.child[0] elif direction is 'r': if crt.next is None: direction = 'u' continue crt = crt.next elif direction is 'u': if crt.parent is self: break if crt.parent.next is None: crt = crt.parent continue crt = crt.parent.next if crt.name == name: nodes.append(crt) if crt.child: direction = 'd' else: direction = 'r' return nodes
[docs] def set_parent(self, parent, index): """ .. admonition:: Helper Method :class: caution Modifies the parent node and takes care of the child node levels. """ self.parent = parent self.index = index if self.name in ['#document', '#document-fragment']: self.level = parent.level else: self.level = parent.level + 1 self.owner = parent.owner if self.owner and isinstance(self, LC.Element) and 'id' in self: self.owner.id_dict[self['id']] = self self.increase_child_level()
[docs] def disconnect(self): """ .. admonition:: Helper Method :class: caution Reset its attributes. """ if self.owner and isinstance(self, LC.Element): try: del self.owner.id_dict[self['id']] except (KeyError, AttributeError): pass self.owner = None self.parent = None self.index = None self.prev = None self.next = None if self.name in ['#document', '#document-fragment']: self.level = -1 else: self.level = 0 self.increase_child_level()
[docs] def set_prev(self, node): """ .. admonition:: Helper Method :class: caution Sets the ``prev`` attribute. """ self.prev = node node.next = self
[docs] def set_next(self, node): """ .. admonition:: Helper Method :class: caution Sets the ``next`` attribute. """ self.next = node node.prev = self
[docs] def increase_child_level(self): """ .. admonition:: Helper Method :class: caution Sets the level of the child nodes. """ if self.child: crt = self direction = 'd' level = self.level owner = self.owner else: return while True: if direction is 'd': level += 1 crt = crt.child[0] elif direction is 'r': if crt.next is None: direction = 'u' continue crt = crt.next elif direction is 'u': level -= 1 if crt.parent is self: break if crt.parent.next is None: crt = crt.parent continue crt = crt.parent.next _set_owner_and_level(crt, owner, level) if crt.child: direction = 'd' else: direction = 'r'
[docs] def append_child_node(self, new_child): """ .. admonition:: Helper Method :class: caution Use this method to insert a node at a the end of the child list. See :meth:`append_child` and :meth:`extend_children` to see this method in action. """ if new_child.parent is not None: del new_child.parent[new_child.index] self.child.append(new_child) new_child.set_parent(self, len(self.child) - 1) try: new_child.set_prev(self.child[-2]) except IndexError: pass
[docs] def insert_node_before(self, index, new_child): """ .. admonition:: Helper Method :class: caution Insert a `new_child` at a given `index`. See :meth:`insert_before` and :meth:`extend_before` to see this method in action.""" if new_child.parent is not None: del new_child.parent[new_child.index] self.child.insert(index, new_child) new_child.set_parent(self, index) if index > 0: new_child.set_prev(self.child[index-1]) try: new_child.set_next(self.child[index+1]) except IndexError: pass self[index].index = index index += 1 return index