Edgewall Software

source: tags/0.3.1/genshi/path.py

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

Prepare 0.3.1 release.

  • Property svn:eol-style set to native
File size: 39.5 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 self.paths]
158
159        def _test(kind, data, pos, namespaces, variables):
160            retval = None
161            for steps, size, cursors, cutoff, counter in paths:
162
163                # Manage the stack that tells us "where we are" in the stream
164                if kind is END:
165                    if cursors:
166                        cursors.pop()
167                    continue
168                elif kind is START:
169                    cursors.append(cursors and cursors[-1] or 0)
170
171                if retval or not cursors:
172                    continue
173                cursor = cursors[-1]
174                depth = len(cursors)
175
176                if cutoff and depth + int(kind is not START) > cutoff[0]:
177                    continue
178
179                ctxtnode = not ignore_context and kind is START \
180                                              and depth == 2
181                matched = None
182                while 1:
183                    # Fetch the next location step
184                    axis, nodetest, predicates = steps[cursor]
185
186                    # If this is the start event for the context node, and the
187                    # axis of the location step doesn't include the current
188                    # element, skip the test
189                    if ctxtnode and (axis is CHILD or axis is DESCENDANT):
190                        break
191
192                    # Is this the last step of the location path?
193                    last_step = cursor + 1 == size
194
195                    # Perform the actual node test
196                    matched = nodetest(kind, data, pos, namespaces, variables)
197
198                    # The node test matched
199                    if matched:
200
201                        # Check all the predicates for this step
202                        if predicates:
203                            for predicate in predicates:
204                                pretval = predicate(kind, data, pos, namespaces,
205                                                    variables)
206                                if type(pretval) is float:
207                                    counter[cursor] += 1
208                                    if counter[cursor] != int(pretval):
209                                        pretval = False
210                                if not pretval:
211                                    matched = None
212                                    break
213
214                        # Both the node test and the predicates matched
215                        if matched:
216                            if last_step:
217                                if not ctxtnode or kind is not START \
218                                        or axis is ATTRIBUTE or axis is SELF:
219                                    retval = matched
220                            elif not ctxtnode or axis is SELF \
221                                              or axis is DESCENDANT_OR_SELF:
222                                cursor += 1
223                                cursors[-1] = cursor
224                            cutoff[:] = []
225
226                    elif not ignore_context and kind is START:
227                        cutoff[:] = [depth]
228
229                    if last_step and not ignore_context and kind is START:
230                        if (axis is not DESCENDANT and
231                            axis is not DESCENDANT_OR_SELF):
232                            cutoff[:] = [depth]
233
234                    if kind is START and not last_step:
235                        next_axis = steps[cursor][0]
236                        if next_axis is ATTRIBUTE:
237                            # If the axis of the next location step is the
238                            # attribute axis, we need to move on to processing
239                            # that step without waiting for the next markup
240                            # event
241                            continue
242
243                    # We're done with this step if it's the last step or the
244                    # axis isn't "self"
245                    if last_step or (axis is not SELF and
246                                     axis is not DESCENDANT_OR_SELF):
247                        break
248
249                if kind is START and axis is not DESCENDANT \
250                                 and axis is not DESCENDANT_OR_SELF:
251                    # If this step is not a closure, it cannot be matched until
252                    # the current element is closed... so we need to move the
253                    # cursor back to the previous closure and retest that
254                    # against the current element
255                    backsteps = [(k, d, p) for k, d, p in steps[:cursor]
256                                 if k is DESCENDANT or k is DESCENDANT_OR_SELF]
257                    backsteps.reverse()
258                    for axis, nodetest, predicates in backsteps:
259                        matched = nodetest(kind, data, pos, namespaces,
260                                           variables)
261                        if not matched:
262                            cursor -= 1
263                        cutoff[:] = []
264                        break
265                    cursors[-1] = cursor
266
267            return retval
268
269        return _test
270
271
272class PathSyntaxError(Exception):
273    """Exception raised when an XPath expression is syntactically incorrect."""
274
275    def __init__(self, message, filename=None, lineno=-1, offset=-1):
276        if filename:
277            message = '%s (%s, line %d)' % (message, filename, lineno)
278        Exception.__init__(self, message)
279        self.filename = filename
280        self.lineno = lineno
281        self.offset = offset
282
283
284class PathParser(object):
285    """Tokenizes and parses an XPath expression."""
286
287    _QUOTES = (("'", "'"), ('"', '"'))
288    _TOKENS = ('::', ':', '..', '.', '//', '/', '[', ']', '()', '(', ')', '@',
289               '=', '!=', '!', '|', ',', '>=', '>', '<=', '<', '$')
290    _tokenize = re.compile('("[^"]*")|(\'[^\']*\')|((?:\d+)?\.\d+)|(%s)|([^%s\s]+)|\s+' % (
291                           '|'.join([re.escape(t) for t in _TOKENS]),
292                           ''.join([re.escape(t[0]) for t in _TOKENS]))).findall
293
294    def __init__(self, text, filename=None, lineno=-1):
295        self.filename = filename
296        self.lineno = lineno
297        self.tokens = filter(None, [dqstr or sqstr or number or token or name
298                                    for dqstr, sqstr, number, token, name in
299                                    self._tokenize(text)])
300        self.pos = 0
301
302    # Tokenizer
303
304    at_end = property(lambda self: self.pos == len(self.tokens) - 1)
305    cur_token = property(lambda self: self.tokens[self.pos])
306
307    def next_token(self):
308        self.pos += 1
309        return self.tokens[self.pos]
310
311    def peek_token(self):
312        if not self.at_end:
313            return self.tokens[self.pos + 1]
314        return None
315
316    # Recursive descent parser
317
318    def parse(self):
319        """Parses the XPath expression and returns a list of location path
320        tests.
321       
322        For union expressions (such as `*|text()`), this function returns one
323        test for each operand in the union. For patch expressions that don't
324        use the union operator, the function always returns a list of size 1.
325       
326        Each path test in turn is a sequence of tests that correspond to the
327        location steps, each tuples of the form `(axis, testfunc, predicates)`
328        """
329        paths = [self._location_path()]
330        while self.cur_token == '|':
331            self.next_token()
332            paths.append(self._location_path())
333        if not self.at_end:
334            raise PathSyntaxError('Unexpected token %r after end of expression'
335                                  % self.cur_token, self.filename, self.lineno)
336        return paths
337
338    def _location_path(self):
339        steps = []
340        while True:
341            if self.cur_token.startswith('/'):
342                if self.cur_token == '//':
343                    steps.append((DESCENDANT_OR_SELF, NodeTest(), []))
344                elif not steps:
345                    raise PathSyntaxError('Absolute location paths not '
346                                          'supported', self.filename,
347                                          self.lineno)
348                self.next_token()
349
350            axis, nodetest, predicates = self._location_step()
351            if not axis:
352                axis = CHILD
353            steps.append((axis, nodetest, predicates))
354
355            if self.at_end or not self.cur_token.startswith('/'):
356                break
357
358        return steps
359
360    def _location_step(self):
361        if self.cur_token == '@':
362            axis = ATTRIBUTE
363            self.next_token()
364        elif self.cur_token == '.':
365            axis = SELF
366        elif self.cur_token == '..':
367            raise PathSyntaxError('Unsupported axis "parent"', self.filename,
368                                  self.lineno)
369        elif self.peek_token() == '::':
370            axis = Axis.forname(self.cur_token)
371            if axis is None:
372                raise PathSyntaxError('Unsupport axis "%s"' % axis,
373                                      self.filename, self.lineno)
374            self.next_token()
375            self.next_token()
376        else:
377            axis = None
378        nodetest = self._node_test(axis or CHILD)
379        predicates = []
380        while self.cur_token == '[':
381            predicates.append(self._predicate())
382        return axis, nodetest, predicates
383
384    def _node_test(self, axis=None):
385        test = prefix = None
386        next_token = self.peek_token()
387        if next_token in ('(', '()'): # Node type test
388            test = self._node_type()
389
390        elif next_token == ':': # Namespace prefix
391            prefix = self.cur_token
392            self.next_token()
393            localname = self.next_token()
394            if localname == '*':
395                test = QualifiedPrincipalTypeTest(axis, prefix)
396            else:
397                test = QualifiedNameTest(axis, prefix, localname)
398
399        else: # Name test
400            if self.cur_token == '*':
401                test = PrincipalTypeTest(axis)
402            elif self.cur_token == '.':
403                test = NodeTest()
404            else:
405                test = LocalNameTest(axis, self.cur_token)
406
407        if not self.at_end:
408            self.next_token()
409        return test
410
411    def _node_type(self):
412        name = self.cur_token
413        self.next_token()
414
415        args = []
416        if self.cur_token != '()':
417            # The processing-instruction() function optionally accepts the
418            # name of the PI as argument, which must be a literal string
419            self.next_token() # (
420            if self.cur_token != ')':
421                string = self.cur_token
422                if (string[0], string[-1]) in self._QUOTES:
423                    string = string[1:-1]
424                args.append(string)
425
426        cls = _nodetest_map.get(name)
427        if not cls:
428            raise PathSyntaxError('%s() not allowed here' % name, self.filename,
429                                  self.lineno)
430        return cls(*args)
431
432    def _predicate(self):
433        assert self.cur_token == '['
434        self.next_token()
435        expr = self._or_expr()
436        if self.cur_token != ']':
437            raise PathSyntaxError('Expected "]" to close predicate, '
438                                  'but found "%s"' % self.cur_token,
439                                  self.filename, self.lineno)
440        if not self.at_end:
441            self.next_token()
442        return expr
443
444    def _or_expr(self):
445        expr = self._and_expr()
446        while self.cur_token == 'or':
447            self.next_token()
448            expr = OrOperator(expr, self._and_expr())
449        return expr
450
451    def _and_expr(self):
452        expr = self._equality_expr()
453        while self.cur_token == 'and':
454            self.next_token()
455            expr = AndOperator(expr, self._equality_expr())
456        return expr
457
458    def _equality_expr(self):
459        expr = self._relational_expr()
460        while self.cur_token in ('=', '!='):
461            op = _operator_map[self.cur_token]
462            self.next_token()
463            expr = op(expr, self._relational_expr())
464        return expr
465
466    def _relational_expr(self):
467        expr = self._primary_expr()
468        while self.cur_token in ('>', '>=', '<', '>='):
469            op = _operator_map[self.cur_token]
470            self.next_token()
471            expr = op(expr, self._primary_expr())
472        return expr
473
474    def _primary_expr(self):
475        token = self.cur_token
476        if len(token) > 1 and (token[0], token[-1]) in self._QUOTES:
477            self.next_token()
478            return StringLiteral(token[1:-1])
479        elif token[0].isdigit() or token[0] == '.':
480            self.next_token()
481            return NumberLiteral(float(token))
482        elif token == '$':
483            token = self.next_token()
484            self.next_token()
485            return VariableReference(token)
486        elif not self.at_end and self.peek_token().startswith('('):
487            return self._function_call()
488        else:
489            axis = None
490            if token == '@':
491                axis = ATTRIBUTE
492                self.next_token()
493            return self._node_test(axis)
494
495    def _function_call(self):
496        name = self.cur_token
497        if self.next_token() == '()':
498            args = []
499        else:
500            assert self.cur_token == '('
501            self.next_token()
502            args = [self._or_expr()]
503            while self.cur_token == ',':
504                self.next_token()
505                args.append(self._or_expr())
506            if not self.cur_token == ')':
507                raise PathSyntaxError('Expected ")" to close function argument '
508                                      'list, but found "%s"' % self.cur_token,
509                                      self.filename, self.lineno)
510        self.next_token()
511        cls = _function_map.get(name)
512        if not cls:
513            raise PathSyntaxError('Unsupported function "%s"' % name,
514                                  self.filename, self.lineno)
515        return cls(*args)
516
517
518# Node tests
519
520class PrincipalTypeTest(object):
521    """Node test that matches any event with the given principal type."""
522    __slots__ = ['principal_type']
523    def __init__(self, principal_type):
524        self.principal_type = principal_type
525    def __call__(self, kind, data, pos, namespaces, variables):
526        if kind is START:
527            if self.principal_type is ATTRIBUTE:
528                return data[1] or None
529            else:
530                return True
531    def __repr__(self):
532        return '*'
533
534class QualifiedPrincipalTypeTest(object):
535    """Node test that matches any event with the given principal type in a
536    specific namespace."""
537    __slots__ = ['principal_type', 'prefix']
538    def __init__(self, principal_type, prefix):
539        self.principal_type = principal_type
540        self.prefix = prefix
541    def __call__(self, kind, data, pos, namespaces, variables):
542        namespace = Namespace(namespaces.get(self.prefix))
543        if kind is START:
544            if self.principal_type is ATTRIBUTE and data[1]:
545                return Attrs([(name, value) for name, value in data[1]
546                              if name in namespace]) or None
547            else:
548                return data[0] in namespace
549    def __repr__(self):
550        return '%s:*' % self.prefix
551
552class LocalNameTest(object):
553    """Node test that matches any event with the given prinipal type and
554    local name.
555    """
556    __slots__ = ['principal_type', 'name']
557    def __init__(self, principal_type, name):
558        self.principal_type = principal_type
559        self.name = name
560    def __call__(self, kind, data, pos, namespaces, variables):
561        if kind is START:
562            if self.principal_type is ATTRIBUTE and self.name in data[1]:
563                return data[1].get(self.name)
564            else:
565                return data[0].localname == self.name
566    def __repr__(self):
567        return self.name
568
569class QualifiedNameTest(object):
570    """Node test that matches any event with the given prinipal type and
571    qualified name.
572    """
573    __slots__ = ['principal_type', 'prefix', 'name']
574    def __init__(self, principal_type, prefix, name):
575        self.principal_type = principal_type
576        self.prefix = prefix
577        self.name = name
578    def __call__(self, kind, data, pos, namespaces, variables):
579        qname = QName('%s}%s' % (namespaces.get(self.prefix), self.name))
580        if kind is START:
581            if self.principal_type is ATTRIBUTE and qname in data[1]:
582                return data[1].get(qname)
583            else:
584                return data[0] == qname
585    def __repr__(self):
586        return '%s:%s' % (self.prefix, self.name)
587
588class CommentNodeTest(object):
589    """Node test that matches any comment events."""
590    __slots__ = []
591    def __call__(self, kind, data, pos, namespaces, variables):
592        return kind is COMMENT and (kind, data, pos)
593    def __repr__(self):
594        return 'comment()'
595
596class NodeTest(object):
597    """Node test that matches any node."""
598    __slots__ = []
599    def __call__(self, kind, data, pos, namespaces, variables):
600        if kind is START:
601            return True
602        return kind, data, pos
603    def __repr__(self):
604        return 'node()'
605
606class ProcessingInstructionNodeTest(object):
607    """Node test that matches any processing instruction event."""
608    __slots__ = ['target']
609    def __init__(self, target=None):
610        self.target = target
611    def __call__(self, kind, data, pos, namespaces, variables):
612        if kind is PI and (not self.target or data[0] == self.target):
613            return (kind, data, pos)
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 and (kind, data, pos)
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 = string2
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}
Note: See TracBrowser for help on using the repository browser.