Edgewall Software

source: tags/0.3.5/genshi/path.py

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

Ported [444] to 0.3.x.

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