#

# ElementTree

# $Id: ElementPath.py 1858 2004-06-17 21:31:41Z Fredrik $

#

# limited xpath support for element trees

#

# history:

# 2003-05-23 fl   created

# 2003-05-28 fl   added support for // etc

# 2003-08-27 fl   fixed parsing of periods in element names

#

# Copyright (c) 2003-2004 by Fredrik Lundh.  All rights reserved.

#

# fredrik@pythonware.com

# http://www.pythonware.com

#

# --------------------------------------------------------------------

# The ElementTree toolkit is

#

# Copyright (c) 1999-2004 by Fredrik Lundh

#

# By obtaining, using, and/or copying this software and/or its

# associated documentation, you agree that you have read, understood,

# and will comply with the following terms and conditions:

#

# Permission to use, copy, modify, and distribute this software and

# its associated documentation for any purpose and without fee is

# hereby granted, provided that the above copyright notice appears in

# all copies, and that both that copyright notice and this permission

# notice appear in supporting documentation, and that the name of

# Secret Labs AB or the author not be used in advertising or publicity

# pertaining to distribution of the software without specific, written

# prior permission.

#

# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD

# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-

# ABILITY AND FITNESS.  IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR

# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY

# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,

# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS

# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE

# OF THIS SOFTWARE.

# --------------------------------------------------------------------



# Licensed to PSF under a Contributor Agreement.

# See http://www.python.org/2.4/license for licensing details.



##

# Implementation module for XPath support.  There's usually no reason

# to import this module directly; the <b>ElementTree</b> does this for

# you, if needed.

##



import re



xpath_tokenizer = re.compile(

    "(::|\.\.|\(\)|[/.*:\[\]\(\)@=])|((?:\{[^}]+\})?[^/:\[\]\(\)@=\s]+)|\s+"

    ).findall



class xpath_descendant_or_self:

    pass



##

# Wrapper for a compiled XPath.



class Path:



    ##

    # Create an Path instance from an XPath expression.



    def __init__(self, path):

        tokens = xpath_tokenizer(path)

        # the current version supports 'path/path'-style expressions only

        self.path = []

        self.tag = None

        if tokens and tokens[0][0] == "/":

            raise SyntaxError("cannot use absolute path on element")

        while tokens:

            op, tag = tokens.pop(0)

            if tag or op == "*":

                self.path.append(tag or op)

            elif op == ".":

                pass

            elif op == "/":

                self.path.append(xpath_descendant_or_self())

                continue

            else:

                raise SyntaxError("unsupported path syntax (%s)" % op)

            if tokens:

                op, tag = tokens.pop(0)

                if op != "/":

                    raise SyntaxError(

                        "expected path separator (%s)" % (op or tag)

                        )

        if self.path and isinstance(self.path[-1], xpath_descendant_or_self):

            raise SyntaxError("path cannot end with //")

        if len(self.path) == 1 and isinstance(self.path[0], type("")):

            self.tag = self.path[0]



    ##

    # Find first matching object.



    def find(self, element):

        tag = self.tag

        if tag is None:

            nodeset = self.findall(element)

            if not nodeset:

                return None

            return nodeset[0]

        for elem in element:

            if elem.tag == tag:

                return elem

        return None



    ##

    # Find text for first matching object.



    def findtext(self, element, default=None):

        tag = self.tag

        if tag is None:

            nodeset = self.findall(element)

            if not nodeset:

                return default

            return nodeset[0].text or ""

        for elem in element:

            if elem.tag == tag:

                return elem.text or ""

        return default



    ##

    # Find all matching objects.



    def findall(self, element):

        nodeset = [element]

        index = 0

        while 1:

            try:

                path = self.path[index]

                index = index + 1

            except IndexError:

                return nodeset

            set = []

            if isinstance(path, xpath_descendant_or_self):

                try:

                    tag = self.path[index]

                    if not isinstance(tag, type("")):

                        tag = None

                    else:

                        index = index + 1

                except IndexError:

                    tag = None # invalid path

                for node in nodeset:

                    new = list(node.getiterator(tag))

                    if new and new[0] is node:

                        set.extend(new[1:])

                    else:

                        set.extend(new)

            else:

                for node in nodeset:

                    for node in node:

                        if path == "*" or node.tag == path:

                            set.append(node)

            if not set:

                return []

            nodeset = set



_cache = {}



##

# (Internal) Compile path.



def _compile(path):

    p = _cache.get(path)

    if p is not None:

        return p

    p = Path(path)

    if len(_cache) >= 100:

        _cache.clear()

    _cache[path] = p

    return p



##

# Find first matching object.



def find(element, path):

    return _compile(path).find(element)



##

# Find text for first matching object.



def findtext(element, path, default=None):

    return _compile(path).findtext(element, default)



##

# Find all matching objects.



def findall(element, path):

    return _compile(path).findall(element)

