Edgewall Software

source: tags/0.3.0/genshi/path.py

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

Remove duplicate XPath operator definition.

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