Edgewall Software

source: branches/stable/0.4.x/genshi/path.py

Last change on this file was 517, checked in by cmlenz, 17 years ago

Try to use proper reStructuredText for docstrings throughout.

  • Property svn:eol-style set to native
File size: 39.7 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Basic support for evaluating XPath expressions against streams.
15
16>>> from genshi.input import XML
17>>> doc = XML('''<doc>
18...  <items count="2">
19...       <item status="new">
20...         <summary>Foo</summary>
21...       </item>
22...       <item status="closed">
23...         <summary>Bar</summary>
24...       </item>
25...   </items>
26... </doc>''')
27>>> print doc.select('items/item[@status="closed"]/summary/text()')
28Bar
29
30Because the XPath engine operates on markup streams (as opposed to tree
31structures), it only implements a subset of the full XPath 1.0 language.
32"""
33
34from math import ceil, floor
35import re
36
37from genshi.core import Stream, Attrs, Namespace, QName
38from genshi.core import START, END, TEXT, COMMENT, PI
39
40__all__ = ['Path', 'PathSyntaxError']
41__docformat__ = 'restructuredtext en'
42
43
44class Axis(object):
45    """Defines constants for the various supported XPath axes."""
46
47    ATTRIBUTE = 'attribute'
48    CHILD = 'child'
49    DESCENDANT = 'descendant'
50    DESCENDANT_OR_SELF = 'descendant-or-self'
51    SELF = 'self'
52
53    def forname(cls, name):
54        """Return the axis constant for the given name, or `None` if no such
55        axis was defined.
56        """
57        return getattr(cls, name.upper().replace('-', '_'), None)
58    forname = classmethod(forname)
59
60
61ATTRIBUTE = Axis.ATTRIBUTE
62CHILD = Axis.CHILD
63DESCENDANT = Axis.DESCENDANT
64DESCENDANT_OR_SELF = Axis.DESCENDANT_OR_SELF
65SELF = Axis.SELF
66
67
68class Path(object):
69    """Implements basic XPath support on streams.
70   
71    Instances of this class represent a "compiled" XPath expression, and provide
72    methods for testing the path against a stream, as well as extracting a
73    substream matching that path.
74    """
75
76    def __init__(self, text, filename=None, lineno=-1):
77        """Create the path object from a string.
78       
79        :param text: the path expression
80        """
81        self.source = text
82        self.paths = PathParser(text, filename, lineno).parse()
83
84    def __repr__(self):
85        paths = []
86        for path in self.paths:
87            steps = []
88            for axis, nodetest, predicates in path:
89                steps.append('%s::%s' % (axis, nodetest))
90                for predicate in predicates:
91                    steps[-1] += '[%s]' % predicate
92            paths.append('/'.join(steps))
93        return '<%s "%s">' % (self.__class__.__name__, '|'.join(paths))
94
95    def select(self, stream, namespaces=None, variables=None):
96        """Returns a substream of the given stream that matches the path.
97       
98        If there are no matches, this method returns an empty stream.
99       
100        >>> from genshi.input import XML
101        >>> xml = XML('<root><elem><child>Text</child></elem></root>')
102       
103        >>> print Path('.//child').select(xml)
104        <child>Text</child>
105       
106        >>> print Path('.//child/text()').select(xml)
107        Text
108       
109        :param stream: the stream to select from
110        :param namespaces: (optional) a mapping of namespace prefixes to URIs
111        :param variables: (optional) a mapping of variable names to values
112        :return: the substream matching the path, or an empty stream
113        """
114        if namespaces is None:
115            namespaces = {}
116        if variables is None:
117            variables = {}
118        stream = iter(stream)
119        def _generate():
120            test = self.test()
121            for event in stream:
122                result = test(event, namespaces, variables)
123                if result is True:
124                    yield event
125                    if event[0] is START:
126                        depth = 1
127                        while depth > 0:
128                            subevent = stream.next()
129                            if subevent[0] is START:
130                                depth += 1
131                            elif subevent[0] is END:
132                                depth -= 1
133                            yield subevent
134                            test(subevent, namespaces, variables,
135                                 updateonly=True)
136                elif result:
137                    yield result
138        return Stream(_generate())
139
140    def test(self, ignore_context=False):
141        """Returns a function that can be used to track whether the path matches
142        a specific stream event.
143       
144        The function returned expects the positional arguments ``event``,
145        ``namespaces`` and ``variables``. The first is a stream event, while the
146        latter two are a mapping of namespace prefixes to URIs, and a mapping
147        of variable names to values, respectively. In addition, the function
148        accepts an ``updateonly`` keyword argument that default to ``False``. If
149        it is set to ``True``, the function only updates its internal state,
150        but does not perform any tests or return a result.
151       
152        If the path matches the event, the function returns the match (for
153        example, a `START` or `TEXT` event.) Otherwise, it returns ``None``.
154       
155        >>> from genshi.input import XML
156        >>> xml = XML('<root><elem><child id="1"/></elem><child id="2"/></root>')
157        >>> test = Path('child').test()
158        >>> for event in xml:
159        ...     if test(event, {}, {}):
160        ...         print event[0], repr(event[1])
161        START (QName(u'child'), Attrs([(QName(u'id'), u'2')]))
162        """
163        paths = [(p, len(p), [0], [], [0] * len(p)) for p in [
164            (ignore_context and [_DOTSLASHSLASH] or []) + p for p in self.paths
165        ]]
166
167        def _test(event, namespaces, variables, updateonly=False):
168            kind, data, pos = event[:3]
169            retval = None
170            for steps, size, cursors, cutoff, counter in paths:
171                # Manage the stack that tells us "where we are" in the stream
172                if kind is END:
173                    if cursors:
174                        cursors.pop()
175                    continue
176                elif kind is START:
177                    cursors.append(cursors and cursors[-1] or 0)
178
179                if updateonly or retval or not cursors:
180                    continue
181                cursor = cursors[-1]
182                depth = len(cursors)
183
184                if cutoff and depth + int(kind is not START) > cutoff[0]:
185                    continue
186
187                ctxtnode = not ignore_context and kind is START \
188                                              and depth == 2
189                matched = None
190                while 1:
191                    # Fetch the next location step
192                    axis, nodetest, predicates = steps[cursor]
193
194                    # If this is the start event for the context node, and the
195                    # axis of the location step doesn't include the current
196                    # element, skip the test
197                    if ctxtnode and (axis is CHILD or axis is DESCENDANT):
198                        break
199
200                    # Is this the last step of the location path?
201                    last_step = cursor + 1 == size
202
203                    # Perform the actual node test
204                    matched = nodetest(kind, data, pos, namespaces, variables)
205
206                    # The node test matched
207                    if matched:
208
209                        # Check all the predicates for this step
210                        if predicates:
211                            for predicate in predicates:
212                                pretval = predicate(kind, data, pos, namespaces,
213                                                    variables)
214                                if type(pretval) is float:
215                                    counter[cursor] += 1
216                                    if counter[cursor] != int(pretval):
217                                        pretval = False
218                                if not pretval:
219                                    matched = None
220                                    break
221
222                        # Both the node test and the predicates matched
223                        if matched:
224                            if last_step:
225                                if not ctxtnode or kind is not START \
226                                        or axis is ATTRIBUTE or axis is SELF:
227                                    retval = matched
228                            elif not ctxtnode or axis is SELF \
229                                              or axis is DESCENDANT_OR_SELF:
230                                cursor += 1
231                                cursors[-1] = cursor
232                            cutoff[:] = []
233
234                    if kind is START:
235                        if last_step and not (axis is DESCENDANT or
236                                              axis is DESCENDANT_OR_SELF):
237                            cutoff[:] = [depth]
238
239                        elif steps[cursor][0] is ATTRIBUTE:
240                            # If the axis of the next location step is the
241                            # attribute axis, we need to move on to processing
242                            # that step without waiting for the next markup
243                            # event
244                            continue
245
246                    # We're done with this step if it's the last step or the
247                    # axis isn't "self"
248                    if not matched or last_step or not (
249                            axis is SELF or axis is DESCENDANT_OR_SELF):
250                        break
251
252                if (retval or not matched) and kind is START and \
253                        not (axis is DESCENDANT or axis is DESCENDANT_OR_SELF):
254                    # If this step is not a closure, it cannot be matched until
255                    # the current element is closed... so we need to move the
256                    # cursor back to the previous closure and retest that
257                    # against the current element
258                    backsteps = [(i, k, d, p) for i, (k, d, p)
259                                 in enumerate(steps[:cursor])
260                                 if k is DESCENDANT or k is DESCENDANT_OR_SELF]
261                    backsteps.reverse()
262                    for cursor, axis, nodetest, predicates in backsteps:
263                        if nodetest(kind, data, pos, namespaces, variables):
264                            cutoff[:] = []
265                            break
266                    cursors[-1] = cursor
267
268            return retval
269
270        return _test
271
272
273class PathSyntaxError(Exception):
274    """Exception raised when an XPath expression is syntactically incorrect."""
275
276    def __init__(self, message, filename=None, lineno=-1, offset=-1):
277        if filename:
278            message = '%s (%s, line %d)' % (message, filename, lineno)
279        Exception.__init__(self, message)
280        self.filename = filename
281        self.lineno = lineno
282        self.offset = offset
283
284
285class PathParser(object):
286    """Tokenizes and parses an XPath expression."""
287
288    _QUOTES = (("'", "'"), ('"', '"'))
289    _TOKENS = ('::', ':', '..', '.', '//', '/', '[', ']', '()', '(', ')', '@',
290               '=', '!=', '!', '|', ',', '>=', '>', '<=', '<', '$')
291    _tokenize = re.compile('("[^"]*")|(\'[^\']*\')|((?:\d+)?\.\d+)|(%s)|([^%s\s]+)|\s+' % (
292                           '|'.join([re.escape(t) for t in _TOKENS]),
293                           ''.join([re.escape(t[0]) for t in _TOKENS]))).findall
294
295    def __init__(self, text, filename=None, lineno=-1):
296        self.filename = filename
297        self.lineno = lineno
298        self.tokens = filter(None, [dqstr or sqstr or number or token or name
299                                    for dqstr, sqstr, number, token, name in
300                                    self._tokenize(text)])
301        self.pos = 0
302
303    # Tokenizer
304
305    at_end = property(lambda self: self.pos == len(self.tokens) - 1)
306    cur_token = property(lambda self: self.tokens[self.pos])
307
308    def next_token(self):
309        self.pos += 1
310        return self.tokens[self.pos]
311
312    def peek_token(self):
313        if not self.at_end:
314            return self.tokens[self.pos + 1]
315        return None
316
317    # Recursive descent parser
318
319    def parse(self):
320        """Parses the XPath expression and returns a list of location path
321        tests.
322       
323        For union expressions (such as `*|text()`), this function returns one
324        test for each operand in the union. For patch expressions that don't
325        use the union operator, the function always returns a list of size 1.
326       
327        Each path test in turn is a sequence of tests that correspond to the
328        location steps, each tuples of the form `(axis, testfunc, predicates)`
329        """
330        paths = [self._location_path()]
331        while self.cur_token == '|':
332            self.next_token()
333            paths.append(self._location_path())
334        if not self.at_end:
335            raise PathSyntaxError('Unexpected token %r after end of expression'
336                                  % self.cur_token, self.filename, self.lineno)
337        return paths
338
339    def _location_path(self):
340        steps = []
341        while True:
342            if self.cur_token.startswith('/'):
343                if self.cur_token == '//':
344                    steps.append((DESCENDANT_OR_SELF, NodeTest(), []))
345                elif not steps:
346                    raise PathSyntaxError('Absolute location paths not '
347                                          'supported', self.filename,
348                                          self.lineno)
349                self.next_token()
350
351            axis, nodetest, predicates = self._location_step()
352            if not axis:
353                axis = CHILD
354            steps.append((axis, nodetest, predicates))
355
356            if self.at_end or not self.cur_token.startswith('/'):
357                break
358
359        return steps
360
361    def _location_step(self):
362        if self.cur_token == '@':
363            axis = ATTRIBUTE
364            self.next_token()
365        elif self.cur_token == '.':
366            axis = SELF
367        elif self.cur_token == '..':
368            raise PathSyntaxError('Unsupported axis "parent"', self.filename,
369                                  self.lineno)
370        elif self.peek_token() == '::':
371            axis = Axis.forname(self.cur_token)
372            if axis is None:
373                raise PathSyntaxError('Unsupport axis "%s"' % axis,
374                                      self.filename, self.lineno)
375            self.next_token()
376            self.next_token()
377        else:
378            axis = None
379        nodetest = self._node_test(axis or CHILD)
380        predicates = []
381        while self.cur_token == '[':
382            predicates.append(self._predicate())
383        return axis, nodetest, predicates
384
385    def _node_test(self, axis=None):
386        test = prefix = None
387        next_token = self.peek_token()
388        if next_token in ('(', '()'): # Node type test
389            test = self._node_type()
390
391        elif next_token == ':': # Namespace prefix
392            prefix = self.cur_token
393            self.next_token()
394            localname = self.next_token()
395            if localname == '*':
396                test = QualifiedPrincipalTypeTest(axis, prefix)
397            else:
398                test = QualifiedNameTest(axis, prefix, localname)
399
400        else: # Name test
401            if self.cur_token == '*':
402                test = PrincipalTypeTest(axis)
403            elif self.cur_token == '.':
404                test = NodeTest()
405            else:
406                test = LocalNameTest(axis, self.cur_token)
407
408        if not self.at_end:
409            self.next_token()
410        return test
411
412    def _node_type(self):
413        name = self.cur_token
414        self.next_token()
415
416        args = []
417        if self.cur_token != '()':
418            # The processing-instruction() function optionally accepts the
419            # name of the PI as argument, which must be a literal string
420            self.next_token() # (
421            if self.cur_token != ')':
422                string = self.cur_token
423                if (string[0], string[-1]) in self._QUOTES:
424                    string = string[1:-1]
425                args.append(string)
426
427        cls = _nodetest_map.get(name)
428        if not cls:
429            raise PathSyntaxError('%s() not allowed here' % name, self.filename,
430                                  self.lineno)
431        return cls(*args)
432
433    def _predicate(self):
434        assert self.cur_token == '['
435        self.next_token()
436        expr = self._or_expr()
437        if self.cur_token != ']':
438            raise PathSyntaxError('Expected "]" to close predicate, '
439                                  'but found "%s"' % self.cur_token,
440                                  self.filename, self.lineno)
441        if not self.at_end:
442            self.next_token()
443        return expr
444
445    def _or_expr(self):
446        expr = self._and_expr()
447        while self.cur_token == 'or':
448            self.next_token()
449            expr = OrOperator(expr, self._and_expr())
450        return expr
451
452    def _and_expr(self):
453        expr = self._equality_expr()
454        while self.cur_token == 'and':
455            self.next_token()
456            expr = AndOperator(expr, self._equality_expr())
457        return expr
458
459    def _equality_expr(self):
460        expr = self._relational_expr()
461        while self.cur_token in ('=', '!='):
462            op = _operator_map[self.cur_token]
463            self.next_token()
464            expr = op(expr, self._relational_expr())
465        return expr
466
467    def _relational_expr(self):
468        expr = self._primary_expr()
469        while self.cur_token in ('>', '>=', '<', '>='):
470            op = _operator_map[self.cur_token]
471            self.next_token()
472            expr = op(expr, self._primary_expr())
473        return expr
474
475    def _primary_expr(self):
476        token = self.cur_token
477        if len(token) > 1 and (token[0], token[-1]) in self._QUOTES:
478            self.next_token()
479            return StringLiteral(token[1:-1])
480        elif token[0].isdigit() or token[0] == '.':
481            self.next_token()
482            return NumberLiteral(float(token))
483        elif token == '$':
484            token = self.next_token()
485            self.next_token()
486            return VariableReference(token)
487        elif not self.at_end and self.peek_token().startswith('('):
488            return self._function_call()
489        else:
490            axis = None
491            if token == '@':
492                axis = ATTRIBUTE
493                self.next_token()
494            return self._node_test(axis)
495
496    def _function_call(self):
497        name = self.cur_token
498        if self.next_token() == '()':
499            args = []
500        else:
501            assert self.cur_token == '('
502            self.next_token()
503            args = [self._or_expr()]
504            while self.cur_token == ',':
505                self.next_token()
506                args.append(self._or_expr())
507            if not self.cur_token == ')':
508                raise PathSyntaxError('Expected ")" to close function argument '
509                                      'list, but found "%s"' % self.cur_token,
510                                      self.filename, self.lineno)
511        self.next_token()
512        cls = _function_map.get(name)
513        if not cls:
514            raise PathSyntaxError('Unsupported function "%s"' % name,
515                                  self.filename, self.lineno)
516        return cls(*args)
517
518
519# Node tests
520
521class PrincipalTypeTest(object):
522    """Node test that matches any event with the given principal type."""
523    __slots__ = ['principal_type']
524    def __init__(self, principal_type):
525        self.principal_type = principal_type
526    def __call__(self, kind, data, pos, namespaces, variables):
527        if kind is START:
528            if self.principal_type is ATTRIBUTE:
529                return data[1] or None
530            else:
531                return True
532    def __repr__(self):
533        return '*'
534
535class QualifiedPrincipalTypeTest(object):
536    """Node test that matches any event with the given principal type in a
537    specific namespace."""
538    __slots__ = ['principal_type', 'prefix']
539    def __init__(self, principal_type, prefix):
540        self.principal_type = principal_type
541        self.prefix = prefix
542    def __call__(self, kind, data, pos, namespaces, variables):
543        namespace = Namespace(namespaces.get(self.prefix))
544        if kind is START:
545            if self.principal_type is ATTRIBUTE and data[1]:
546                return Attrs([(name, value) for name, value in data[1]
547                              if name in namespace]) or None
548            else:
549                return data[0] in namespace
550    def __repr__(self):
551        return '%s:*' % self.prefix
552
553class LocalNameTest(object):
554    """Node test that matches any event with the given principal type and
555    local name.
556    """
557    __slots__ = ['principal_type', 'name']
558    def __init__(self, principal_type, name):
559        self.principal_type = principal_type
560        self.name = name
561    def __call__(self, kind, data, pos, namespaces, variables):
562        if kind is START:
563            if self.principal_type is ATTRIBUTE and self.name in data[1]:
564                return data[1].get(self.name)
565            else:
566                return data[0].localname == self.name
567    def __repr__(self):
568        return self.name
569
570class QualifiedNameTest(object):
571    """Node test that matches any event with the given principal type and
572    qualified name.
573    """
574    __slots__ = ['principal_type', 'prefix', 'name']
575    def __init__(self, principal_type, prefix, name):
576        self.principal_type = principal_type
577        self.prefix = prefix
578        self.name = name
579    def __call__(self, kind, data, pos, namespaces, variables):
580        qname = QName('%s}%s' % (namespaces.get(self.prefix), self.name))
581        if kind is START:
582            if self.principal_type is ATTRIBUTE and qname in data[1]:
583                return data[1].get(qname)
584            else:
585                return data[0] == qname
586    def __repr__(self):
587        return '%s:%s' % (self.prefix, self.name)
588
589class CommentNodeTest(object):
590    """Node test that matches any comment events."""
591    __slots__ = []
592    def __call__(self, kind, data, pos, namespaces, variables):
593        return kind is COMMENT
594    def __repr__(self):
595        return 'comment()'
596
597class NodeTest(object):
598    """Node test that matches any node."""
599    __slots__ = []
600    def __call__(self, kind, data, pos, namespaces, variables):
601        if kind is START:
602            return True
603        return kind, data, pos
604    def __repr__(self):
605        return 'node()'
606
607class ProcessingInstructionNodeTest(object):
608    """Node test that matches any processing instruction event."""
609    __slots__ = ['target']
610    def __init__(self, target=None):
611        self.target = target
612    def __call__(self, kind, data, pos, namespaces, variables):
613        return kind is PI and (not self.target or data[0] == self.target)
614    def __repr__(self):
615        arg = ''
616        if self.target:
617            arg = '"' + self.target + '"'
618        return 'processing-instruction(%s)' % arg
619
620class TextNodeTest(object):
621    """Node test that matches any text event."""
622    __slots__ = []
623    def __call__(self, kind, data, pos, namespaces, variables):
624        return kind is TEXT
625    def __repr__(self):
626        return 'text()'
627
628_nodetest_map = {'comment': CommentNodeTest, 'node': NodeTest,
629                 'processing-instruction': ProcessingInstructionNodeTest,
630                 'text': TextNodeTest}
631
632# Functions
633
634class Function(object):
635    """Base class for function nodes in XPath expressions."""
636
637class BooleanFunction(Function):
638    """The `boolean` function, which converts its argument to a boolean
639    value.
640    """
641    __slots__ = ['expr']
642    def __init__(self, expr):
643        self.expr = expr
644    def __call__(self, kind, data, pos, namespaces, variables):
645        val = self.expr(kind, data, pos, namespaces, variables)
646        return bool(val)
647    def __repr__(self):
648        return 'boolean(%r)' % self.expr
649
650class CeilingFunction(Function):
651    """The `ceiling` function, which returns the nearest lower integer number
652    for the given number.
653    """
654    __slots__ = ['number']
655    def __init__(self, number):
656        self.number = number
657    def __call__(self, kind, data, pos, namespaces, variables):
658        number = self.number(kind, data, pos, namespaces, variables)
659        return ceil(float(number))
660    def __repr__(self):
661        return 'ceiling(%r)' % self.number
662
663class ConcatFunction(Function):
664    """The `concat` function, which concatenates (joins) the variable number of
665    strings it gets as arguments.
666    """
667    __slots__ = ['exprs']
668    def __init__(self, *exprs):
669        self.exprs = exprs
670    def __call__(self, kind, data, pos, namespaces, variables):
671        strings = []
672        for item in [expr(kind, data, pos, namespaces, variables)
673                     for expr in self.exprs]:
674            strings.append(item)
675        return u''.join(strings)
676    def __repr__(self):
677        return 'concat(%s)' % ', '.join([repr(expr) for expr in self.exprs])
678
679class ContainsFunction(Function):
680    """The `contains` function, which returns whether a string contains a given
681    substring.
682    """
683    __slots__ = ['string1', 'string2']
684    def __init__(self, string1, string2):
685        self.string1 = string1
686        self.string2 = string2
687    def __call__(self, kind, data, pos, namespaces, variables):
688        string1 = self.string1(kind, data, pos, namespaces, variables)
689        string2 = self.string2(kind, data, pos, namespaces, variables)
690        return string2 in string1
691    def __repr__(self):
692        return 'contains(%r, %r)' % (self.string1, self.string2)
693
694class FalseFunction(Function):
695    """The `false` function, which always returns the boolean `false` value."""
696    __slots__ = []
697    def __call__(self, kind, data, pos, namespaces, variables):
698        return False
699    def __repr__(self):
700        return 'false()'
701
702class FloorFunction(Function):
703    """The `ceiling` function, which returns the nearest higher integer number
704    for the given number.
705    """
706    __slots__ = ['number']
707    def __init__(self, number):
708        self.number = number
709    def __call__(self, kind, data, pos, namespaces, variables):
710        number = self.number(kind, data, pos, namespaces, variables)
711        return floor(float(number))
712    def __repr__(self):
713        return 'floor(%r)' % self.number
714
715class LocalNameFunction(Function):
716    """The `local-name` function, which returns the local name of the current
717    element.
718    """
719    __slots__ = []
720    def __call__(self, kind, data, pos, namespaces, variables):
721        if kind is START:
722            return data[0].localname
723    def __repr__(self):
724        return 'local-name()'
725
726class NameFunction(Function):
727    """The `name` function, which returns the qualified name of the current
728    element.
729    """
730    __slots__ = []
731    def __call__(self, kind, data, pos, namespaces, variables):
732        if kind is START:
733            return data[0]
734    def __repr__(self):
735        return 'name()'
736
737class NamespaceUriFunction(Function):
738    """The `namespace-uri` function, which returns the namespace URI of the
739    current element.
740    """
741    __slots__ = []
742    def __call__(self, kind, data, pos, namespaces, variables):
743        if kind is START:
744            return data[0].namespace
745    def __repr__(self):
746        return 'namespace-uri()'
747
748class NotFunction(Function):
749    """The `not` function, which returns the negated boolean value of its
750    argument.
751    """
752    __slots__ = ['expr']
753    def __init__(self, expr):
754        self.expr = expr
755    def __call__(self, kind, data, pos, namespaces, variables):
756        return not self.expr(kind, data, pos, namespaces, variables)
757    def __repr__(self):
758        return 'not(%s)' % self.expr
759
760class NormalizeSpaceFunction(Function):
761    """The `normalize-space` function, which removes leading and trailing
762    whitespace in the given string, and replaces multiple adjacent whitespace
763    characters inside the string with a single space.
764    """
765    __slots__ = ['expr']
766    _normalize = re.compile(r'\s{2,}').sub
767    def __init__(self, expr):
768        self.expr = expr
769    def __call__(self, kind, data, pos, namespaces, variables):
770        string = self.expr(kind, data, pos, namespaces, variables)
771        return self._normalize(' ', string.strip())
772    def __repr__(self):
773        return 'normalize-space(%s)' % repr(self.expr)
774
775class NumberFunction(Function):
776    """The `number` function that converts its argument to a number."""
777    __slots__ = ['expr']
778    def __init__(self, expr):
779        self.expr = expr
780    def __call__(self, kind, data, pos, namespaces, variables):
781        val = self.expr(kind, data, pos, namespaces, variables)
782        return float(val)
783    def __repr__(self):
784        return 'number(%r)' % self.expr
785
786class RoundFunction(Function):
787    """The `round` function, which returns the nearest integer number for the
788    given number.
789    """
790    __slots__ = ['number']
791    def __init__(self, number):
792        self.number = number
793    def __call__(self, kind, data, pos, namespaces, variables):
794        number = self.number(kind, data, pos, namespaces, variables)
795        return round(float(number))
796    def __repr__(self):
797        return 'round(%r)' % self.number
798
799class StartsWithFunction(Function):
800    """The `starts-with` function that returns whether one string starts with
801    a given substring.
802    """
803    __slots__ = ['string1', 'string2']
804    def __init__(self, string1, string2):
805        self.string1 = string1
806        self.string2 = string2
807    def __call__(self, kind, data, pos, namespaces, variables):
808        string1 = self.string1(kind, data, pos, namespaces, variables)
809        string2 = self.string2(kind, data, pos, namespaces, variables)
810        return string1.startswith(string2)
811    def __repr__(self):
812        return 'starts-with(%r, %r)' % (self.string1, self.string2)
813
814class StringLengthFunction(Function):
815    """The `string-length` function that returns the length of the given
816    string.
817    """
818    __slots__ = ['expr']
819    def __init__(self, expr):
820        self.expr = expr
821    def __call__(self, kind, data, pos, namespaces, variables):
822        string = self.expr(kind, data, pos, namespaces, variables)
823        return len(string)
824    def __repr__(self):
825        return 'string-length(%r)' % self.expr
826
827class SubstringFunction(Function):
828    """The `substring` function that returns the part of a string that starts
829    at the given offset, and optionally limited to the given length.
830    """
831    __slots__ = ['string', 'start', 'length']
832    def __init__(self, string, start, length=None):
833        self.string = string
834        self.start = start
835        self.length = length
836    def __call__(self, kind, data, pos, namespaces, variables):
837        string = self.string(kind, data, pos, namespaces, variables)
838        start = self.start(kind, data, pos, namespaces, variables)
839        length = 0
840        if self.length is not None:
841            length = self.length(kind, data, pos, namespaces, variables)
842        return string[int(start):len(string) - int(length)]
843    def __repr__(self):
844        if self.length is not None:
845            return 'substring(%r, %r, %r)' % (self.string, self.start,
846                                              self.length)
847        else:
848            return 'substring(%r, %r)' % (self.string, self.start)
849
850class SubstringAfterFunction(Function):
851    """The `substring-after` function that returns the part of a string that
852    is found after the given substring.
853    """
854    __slots__ = ['string1', 'string2']
855    def __init__(self, string1, string2):
856        self.string1 = string1
857        self.string2 = string2
858    def __call__(self, kind, data, pos, namespaces, variables):
859        string1 = self.string1(kind, data, pos, namespaces, variables)
860        string2 = self.string2(kind, data, pos, namespaces, variables)
861        index = string1.find(string2)
862        if index >= 0:
863            return string1[index + len(string2):]
864        return u''
865    def __repr__(self):
866        return 'substring-after(%r, %r)' % (self.string1, self.string2)
867
868class SubstringBeforeFunction(Function):
869    """The `substring-before` function that returns the part of a string that
870    is found before the given substring.
871    """
872    __slots__ = ['string1', 'string2']
873    def __init__(self, string1, string2):
874        self.string1 = string1
875        self.string2 = string2
876    def __call__(self, kind, data, pos, namespaces, variables):
877        string1 = self.string1(kind, data, pos, namespaces, variables)
878        string2 = self.string2(kind, data, pos, namespaces, variables)
879        index = string1.find(string2)
880        if index >= 0:
881            return string1[:index]
882        return u''
883    def __repr__(self):
884        return 'substring-after(%r, %r)' % (self.string1, self.string2)
885
886class TranslateFunction(Function):
887    """The `translate` function that translates a set of characters in a
888    string to target set of characters.
889    """
890    __slots__ = ['string', 'fromchars', 'tochars']
891    def __init__(self, string, fromchars, tochars):
892        self.string = string
893        self.fromchars = fromchars
894        self.tochars = tochars
895    def __call__(self, kind, data, pos, namespaces, variables):
896        string = self.string(kind, data, pos, namespaces, variables)
897        fromchars = self.fromchars(kind, data, pos, namespaces, variables)
898        tochars = self.tochars(kind, data, pos, namespaces, variables)
899        table = dict(zip([ord(c) for c in fromchars],
900                         [ord(c) for c in tochars]))
901        return string.translate(table)
902    def __repr__(self):
903        return 'translate(%r, %r, %r)' % (self.string, self.fromchars,
904                                          self.tochars)
905
906class TrueFunction(Function):
907    """The `true` function, which always returns the boolean `true` value."""
908    __slots__ = []
909    def __call__(self, kind, data, pos, namespaces, variables):
910        return True
911    def __repr__(self):
912        return 'true()'
913
914_function_map = {'boolean': BooleanFunction, 'ceiling': CeilingFunction,
915                 'concat': ConcatFunction, 'contains': ContainsFunction,
916                 'false': FalseFunction, 'floor': FloorFunction,
917                 'local-name': LocalNameFunction, 'name': NameFunction,
918                 'namespace-uri': NamespaceUriFunction,
919                 'normalize-space': NormalizeSpaceFunction, 'not': NotFunction,
920                 'number': NumberFunction, 'round': RoundFunction,
921                 'starts-with': StartsWithFunction,
922                 'string-length': StringLengthFunction,
923                 'substring': SubstringFunction,
924                 'substring-after': SubstringAfterFunction,
925                 'substring-before': SubstringBeforeFunction,
926                 'translate': TranslateFunction, 'true': TrueFunction}
927
928# Literals & Variables
929
930class Literal(object):
931    """Abstract base class for literal nodes."""
932
933class StringLiteral(Literal):
934    """A string literal node."""
935    __slots__ = ['text']
936    def __init__(self, text):
937        self.text = text
938    def __call__(self, kind, data, pos, namespaces, variables):
939        return self.text
940    def __repr__(self):
941        return '"%s"' % self.text
942
943class NumberLiteral(Literal):
944    """A number literal node."""
945    __slots__ = ['number']
946    def __init__(self, number):
947        self.number = number
948    def __call__(self, kind, data, pos, namespaces, variables):
949        return self.number
950    def __repr__(self):
951        return str(self.number)
952
953class VariableReference(Literal):
954    """A variable reference node."""
955    __slots__ = ['name']
956    def __init__(self, name):
957        self.name = name
958    def __call__(self, kind, data, pos, namespaces, variables):
959        return variables.get(self.name)
960    def __repr__(self):
961        return str(self.name)
962
963# Operators
964
965class AndOperator(object):
966    """The boolean operator `and`."""
967    __slots__ = ['lval', 'rval']
968    def __init__(self, lval, rval):
969        self.lval = lval
970        self.rval = rval
971    def __call__(self, kind, data, pos, namespaces, variables):
972        lval = self.lval(kind, data, pos, namespaces, variables)
973        if not lval:
974            return False
975        rval = self.rval(kind, data, pos, namespaces, variables)
976        return bool(rval)
977    def __repr__(self):
978        return '%s and %s' % (self.lval, self.rval)
979
980class EqualsOperator(object):
981    """The equality operator `=`."""
982    __slots__ = ['lval', 'rval']
983    def __init__(self, lval, rval):
984        self.lval = lval
985        self.rval = rval
986    def __call__(self, kind, data, pos, namespaces, variables):
987        lval = self.lval(kind, data, pos, namespaces, variables)
988        rval = self.rval(kind, data, pos, namespaces, variables)
989        return lval == rval
990    def __repr__(self):
991        return '%s=%s' % (self.lval, self.rval)
992
993class NotEqualsOperator(object):
994    """The equality operator `!=`."""
995    __slots__ = ['lval', 'rval']
996    def __init__(self, lval, rval):
997        self.lval = lval
998        self.rval = rval
999    def __call__(self, kind, data, pos, namespaces, variables):
1000        lval = self.lval(kind, data, pos, namespaces, variables)
1001        rval = self.rval(kind, data, pos, namespaces, variables)
1002        return lval != rval
1003    def __repr__(self):
1004        return '%s!=%s' % (self.lval, self.rval)
1005
1006class OrOperator(object):
1007    """The boolean operator `or`."""
1008    __slots__ = ['lval', 'rval']
1009    def __init__(self, lval, rval):
1010        self.lval = lval
1011        self.rval = rval
1012    def __call__(self, kind, data, pos, namespaces, variables):
1013        lval = self.lval(kind, data, pos, namespaces, variables)
1014        if lval:
1015            return True
1016        rval = self.rval(kind, data, pos, namespaces, variables)
1017        return bool(rval)
1018    def __repr__(self):
1019        return '%s or %s' % (self.lval, self.rval)
1020
1021class GreaterThanOperator(object):
1022    """The relational operator `>` (greater than)."""
1023    __slots__ = ['lval', 'rval']
1024    def __init__(self, lval, rval):
1025        self.lval = lval
1026        self.rval = rval
1027    def __call__(self, kind, data, pos, namespaces, variables):
1028        lval = self.lval(kind, data, pos, namespaces, variables)
1029        rval = self.rval(kind, data, pos, namespaces, variables)
1030        return float(lval) > float(rval)
1031    def __repr__(self):
1032        return '%s>%s' % (self.lval, self.rval)
1033
1034class GreaterThanOrEqualOperator(object):
1035    """The relational operator `>=` (greater than or equal)."""
1036    __slots__ = ['lval', 'rval']
1037    def __init__(self, lval, rval):
1038        self.lval = lval
1039        self.rval = rval
1040    def __call__(self, kind, data, pos, namespaces, variables):
1041        lval = self.lval(kind, data, pos, namespaces, variables)
1042        rval = self.rval(kind, data, pos, namespaces, variables)
1043        return float(lval) >= float(rval)
1044    def __repr__(self):
1045        return '%s>=%s' % (self.lval, self.rval)
1046
1047class LessThanOperator(object):
1048    """The relational operator `<` (less than)."""
1049    __slots__ = ['lval', 'rval']
1050    def __init__(self, lval, rval):
1051        self.lval = lval
1052        self.rval = rval
1053    def __call__(self, kind, data, pos, namespaces, variables):
1054        lval = self.lval(kind, data, pos, namespaces, variables)
1055        rval = self.rval(kind, data, pos, namespaces, variables)
1056        return float(lval) < float(rval)
1057    def __repr__(self):
1058        return '%s<%s' % (self.lval, self.rval)
1059
1060class LessThanOrEqualOperator(object):
1061    """The relational operator `<=` (less than or equal)."""
1062    __slots__ = ['lval', 'rval']
1063    def __init__(self, lval, rval):
1064        self.lval = lval
1065        self.rval = rval
1066    def __call__(self, kind, data, pos, namespaces, variables):
1067        lval = self.lval(kind, data, pos, namespaces, variables)
1068        rval = self.rval(kind, data, pos, namespaces, variables)
1069        return float(lval) <= float(rval)
1070    def __repr__(self):
1071        return '%s<=%s' % (self.lval, self.rval)
1072
1073_operator_map = {'=': EqualsOperator, '!=': NotEqualsOperator,
1074                 '>': GreaterThanOperator, '>=': GreaterThanOrEqualOperator,
1075                 '<': LessThanOperator, '>=': LessThanOrEqualOperator}
1076
1077
1078_DOTSLASHSLASH = (DESCENDANT_OR_SELF, PrincipalTypeTest(None), ())
Note: See TracBrowser for help on using the repository browser.