lexor.core.node — lexor 0.1.2c0 documentation

# 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

    class Node(object):
        """Primary datatype for the entire Document Object Model. """
        __slots__ = ('name', 'owner', 'parent', 'index',
                     'prev', 'next', 'child', 'level')

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

        def remove_children(self):
            """Remove all the child nodes. """
            for child in self.child:
                child.disconnect()
            del self.child[:]

        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()

        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

        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

        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

        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

        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

        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)

        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)

        def prepend_before(self, new_child):
            """Place `new_child` before the node. """
            self.parent.insert_before(self.index, new_child)

        def prepend_nodes_before(self, new_children):
            """Place `new_children` before the node. """
            self.parent.extend_before(self.index, new_children)

        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

        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)

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

        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

        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

        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

        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()

        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()

        def set_prev(self, node):
            """
            .. admonition:: Helper Method
                :class: caution

                Sets the ``prev`` attribute.
            """
            self.prev = node
            node.next = self

        def set_next(self, node):
            """
            .. admonition:: Helper Method
                :class: caution

                Sets the ``next`` attribute.
            """
            self.next = node
            node.prev = self

        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'

        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

        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