comparison markup/template.py @ 17:74cc70129d04 trunk

Refactoring to address #6: all match templates are now processed by a single filter, which means that match templates added by included templates are properly applied. A side effect of this refactoring is that `Context` objects may not be reused across multiple template processing runs. Also, output filters are now applied in the `Stream.serialize()` method instead of by the `Template.generate()` method, which just makes more sense.
author cmlenz
date Sun, 18 Jun 2006 22:33:33 +0000
parents c7d33e0c9839
children 5420cfe42d36
comparison
equal deleted inserted replaced
16:bcba0181049c 17:74cc70129d04
40 * Is there any need to support py:extends and/or py:layout? 40 * Is there any need to support py:extends and/or py:layout?
41 * Could we generate byte code from expressions? 41 * Could we generate byte code from expressions?
42 """ 42 """
43 43
44 import compiler 44 import compiler
45 from itertools import chain
46 import os 45 import os
47 import re 46 import re
48 from StringIO import StringIO 47 from StringIO import StringIO
49 48
50 from markup.core import Attributes, Stream, StreamEventKind 49 from markup.core import Attributes, Stream, StreamEventKind
51 from markup.eval import Expression 50 from markup.eval import Expression
52 from markup.filters import EvalFilter, IncludeFilter, WhitespaceFilter 51 from markup.filters import IncludeFilter
53 from markup.input import HTML, XMLParser, XML 52 from markup.input import HTML, XMLParser, XML
54 from markup.path import Path 53 from markup.path import Path
55 54
56 __all__ = ['Context', 'BadDirectiveError', 'TemplateError', 55 __all__ = ['Context', 'BadDirectiveError', 'TemplateError',
57 'TemplateSyntaxError', 'TemplateNotFound', 'Template', 56 'TemplateSyntaxError', 'TemplateNotFound', 'Template',
386 385
387 class MatchDirective(Directive): 386 class MatchDirective(Directive):
388 """Implementation of the `py:match` template directive. 387 """Implementation of the `py:match` template directive.
389 388
390 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#"> 389 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
391 ... <span py:match="div/greeting"> 390 ... <span py:match="greeting">
392 ... Hello ${select('@name')} 391 ... Hello ${select('@name')}
393 ... </span> 392 ... </span>
394 ... <greeting name="Dude" /> 393 ... <greeting name="Dude" />
395 ... </div>''') 394 ... </div>''')
396 >>> print tmpl.generate() 395 >>> print tmpl.generate()
449 448
450 def __init__(self, template, value, pos): 449 def __init__(self, template, value, pos):
451 Directive.__init__(self, template, None, pos) 450 Directive.__init__(self, template, None, pos)
452 self.path = Path(value) 451 self.path = Path(value)
453 self.stream = [] 452 self.stream = []
454 template.filters.append(self._filter)
455 453
456 def __call__(self, stream, ctxt): 454 def __call__(self, stream, ctxt):
457 self.stream = list(stream) 455 self.stream = list(stream)
456 ctxt._match_templates.append((self.path.test(), self.path, self.stream))
458 return [] 457 return []
459 458
460 def __repr__(self): 459 def __repr__(self):
461 return '<%s "%s">' % (self.__class__.__name__, self.path.source) 460 return '<%s "%s">' % (self.__class__.__name__, self.path.source)
462
463 def _filter(self, stream, ctxt=None):
464 test = self.path.test()
465 for event in stream:
466 if self.stream and event in self.stream[::len(self.stream)]:
467 # This is the event this filter produced itself, so matching it
468 # again would result in an infinite loop
469 yield event
470 continue
471 result = test(*event)
472 if result is True:
473 content = [event]
474 depth = 1
475 while depth > 0:
476 ev = stream.next()
477 if ev[0] is Stream.START:
478 depth += 1
479 elif ev[0] is Stream.END:
480 depth -= 1
481 content.append(ev)
482 test(*ev)
483
484 yield (Template.SUB,
485 ([lambda stream, ctxt: self._apply(content, ctxt)], []),
486 content[0][-1])
487 else:
488 yield event
489
490 def _apply(self, orig_stream, ctxt):
491 ctxt.push(select=lambda path: Stream(orig_stream).select(path))
492 for event in self.stream:
493 yield event
494 ctxt.pop()
495 461
496 462
497 class ReplaceDirective(Directive): 463 class ReplaceDirective(Directive):
498 """Implementation of the `py:replace` template directive. 464 """Implementation of the `py:replace` template directive.
499 465
592 """Can parse a template and transform it into the corresponding output 558 """Can parse a template and transform it into the corresponding output
593 based on context data. 559 based on context data.
594 """ 560 """
595 NAMESPACE = 'http://purl.org/kid/ns#' 561 NAMESPACE = 'http://purl.org/kid/ns#'
596 562
597 EXPR = StreamEventKind('expr') # an expression 563 EXPR = StreamEventKind('EXPR') # an expression
598 SUB = StreamEventKind('sub') # a "subprogram" 564 SUB = StreamEventKind('SUB') # a "subprogram"
599 565
600 directives = [('def', DefDirective), 566 directives = [('def', DefDirective),
601 ('match', MatchDirective), 567 ('match', MatchDirective),
602 ('for', ForDirective), 568 ('for', ForDirective),
603 ('if', IfDirective), 569 ('if', IfDirective),
614 self.source = StringIO(source) 580 self.source = StringIO(source)
615 else: 581 else:
616 self.source = source 582 self.source = source
617 self.filename = filename or '<string>' 583 self.filename = filename or '<string>'
618 584
619 self.input_filters = [EvalFilter()] 585 self.filters = [self._eval, self._match]
620 self.filters = []
621 self.output_filters = [WhitespaceFilter()]
622 self.parse() 586 self.parse()
623 587
624 def __repr__(self): 588 def __repr__(self):
625 return '<%s "%s">' % (self.__class__.__name__, 589 return '<%s "%s">' % (self.__class__.__name__,
626 os.path.basename(self.filename)) 590 os.path.basename(self.filename))
697 else: 661 else:
698 stream.append((kind, data, pos)) 662 stream.append((kind, data, pos))
699 663
700 self.stream = stream 664 self.stream = stream
701 665
702 def generate(self, ctxt=None):
703 """Transform the template based on the given context data."""
704
705 if ctxt is None:
706 ctxt = Context()
707
708 def _transform(stream):
709 # Apply input filters
710 for filter_ in chain(self.input_filters, self.filters):
711 stream = filter_(iter(stream), ctxt)
712
713 try:
714 for kind, data, pos in stream:
715
716 if kind is Template.SUB:
717 # This event is a list of directives and a list of
718 # nested events to which those directives should be
719 # applied
720 directives, substream = data
721 directives.reverse()
722 for directive in directives:
723 substream = directive(iter(substream), ctxt)
724 for event in _transform(iter(substream)):
725 yield event
726
727 else:
728 yield kind, data, pos
729
730 except SyntaxError, err:
731 raise TemplateSyntaxError(err, self.filename, pos[0],
732 pos[1] + (err.offset or 0))
733
734 stream = _transform(self.stream)
735
736 # Apply output filters
737 for filter_ in self.output_filters:
738 stream = filter_(iter(stream), ctxt)
739
740 return Stream(stream)
741
742 _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}') 666 _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}')
743 _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)') 667 _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)')
744 668
745 def _interpolate(cls, text, lineno=-1, offset=-1): 669 def _interpolate(cls, text, lineno=-1, offset=-1):
746 """Parse the given string and extract expressions. 670 """Parse the given string and extract expressions.
751 @param text: the text to parse 675 @param text: the text to parse
752 @param lineno: the line number at which the text was found (optional) 676 @param lineno: the line number at which the text was found (optional)
753 @param offset: the column number at which the text starts in the source 677 @param offset: the column number at which the text starts in the source
754 (optional) 678 (optional)
755 """ 679 """
756 patterns = [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE] 680 patterns = [Template._FULL_EXPR_RE, Template._SHORT_EXPR_RE]
757 def _interpolate(text): 681 def _interpolate(text):
758 for idx, group in enumerate(patterns.pop(0).split(text)): 682 for idx, group in enumerate(patterns.pop(0).split(text)):
759 if idx % 2: 683 if idx % 2:
760 yield Template.EXPR, Expression(group), (lineno, offset) 684 yield Template.EXPR, Expression(group), (lineno, offset)
761 elif group: 685 elif group:
765 else: 689 else:
766 yield Stream.TEXT, group.replace('$$', '$'), \ 690 yield Stream.TEXT, group.replace('$$', '$'), \
767 (lineno, offset) 691 (lineno, offset)
768 return _interpolate(text) 692 return _interpolate(text)
769 _interpolate = classmethod(_interpolate) 693 _interpolate = classmethod(_interpolate)
694
695 def generate(self, ctxt=None):
696 """Transform the template based on the given context data."""
697 if ctxt is None:
698 ctxt = Context()
699 if not hasattr(ctxt, '_match_templates'):
700 ctxt._match_templates = []
701
702 return Stream(self._flatten(self.stream, ctxt))
703
704 def _eval(self, stream, ctxt=None):
705 for kind, data, pos in stream:
706
707 if kind is Stream.START:
708 # Attributes may still contain expressions in start tags at
709 # this point, so do some evaluation
710 tag, attrib = data
711 new_attrib = []
712 for name, substream in attrib:
713 if isinstance(substream, basestring):
714 value = substream
715 else:
716 values = []
717 for subkind, subdata, subpos in substream:
718 if subkind is Template.EXPR:
719 values.append(subdata.evaluate(ctxt))
720 else:
721 values.append(subdata)
722 value = filter(lambda x: x is not None, values)
723 if not value:
724 continue
725 new_attrib.append((name, ''.join(value)))
726 yield kind, (tag, Attributes(new_attrib)), pos
727
728 elif kind is Template.EXPR:
729 result = data.evaluate(ctxt)
730 if result is None:
731 continue
732
733 # First check for a string, otherwise the iterable test below
734 # succeeds, and the string will be chopped up into individual
735 # characters
736 if isinstance(result, basestring):
737 yield Stream.TEXT, result, pos
738 else:
739 # Test if the expression evaluated to an iterable, in which
740 # case we yield the individual items
741 try:
742 yield (Template.SUB, ([], iter(result)), pos)
743 except TypeError:
744 # Neither a string nor an iterable, so just pass it
745 # through
746 yield Stream.TEXT, unicode(result), pos
747
748 else:
749 yield kind, data, pos
750
751 def _flatten(self, stream, ctxt=None, apply_filters=True):
752 if apply_filters:
753 for filter_ in self.filters:
754 stream = filter_(iter(stream), ctxt)
755 try:
756 for kind, data, pos in stream:
757 if kind is Template.SUB:
758 # This event is a list of directives and a list of
759 # nested events to which those directives should be
760 # applied
761 directives, substream = data
762 directives.reverse()
763 for directive in directives:
764 substream = directive(iter(substream), ctxt)
765 for event in self._flatten(substream, ctxt):
766 yield event
767 continue
768 else:
769 yield kind, data, pos
770 except SyntaxError, err:
771 raise TemplateSyntaxError(err, self.filename, pos[0],
772 pos[1] + (err.offset or 0))
773
774 def _match(self, stream, ctxt=None):
775 for kind, data, pos in stream:
776
777 # We (currently) only care about start and end events for matching
778 # We might care about namespace events in the future, though
779 if kind not in (Stream.START, Stream.END):
780 yield kind, data, pos
781 continue
782
783 for idx, (test, path, template) in enumerate(ctxt._match_templates):
784 if (kind, data, pos) in template[::len(template)]:
785 # This is the event this match template produced itself, so
786 # matching it again would result in an infinite loop
787 continue
788
789 result = test(kind, data, pos)
790
791 if result:
792 # Consume and store all events until an end event
793 # corresponding to this start event is encountered
794 content = [(kind, data, pos)]
795 depth = 1
796 while depth > 0:
797 event = stream.next()
798 if event[0] is Stream.START:
799 depth += 1
800 elif event[0] is Stream.END:
801 depth -= 1
802 content.append(event)
803
804 # enable the path to keep track of the stream state
805 test(*event)
806
807 content = list(self._flatten(content, ctxt, apply_filters=False))
808
809 def _apply(stream, ctxt):
810 stream = list(stream)
811 ctxt.push(select=lambda path: Stream(stream).select(path))
812 for event in template:
813 yield event
814 ctxt.pop()
815
816 yield (Template.SUB,
817 ([lambda stream, ctxt: _apply(content, ctxt)],
818 []), content[0][-1])
819 break
820 else:
821 yield kind, data, pos
770 822
771 823
772 class TemplateLoader(object): 824 class TemplateLoader(object):
773 """Responsible for loading templates from files on the specified search 825 """Responsible for loading templates from files on the specified search
774 path. 826 path.
839 filepath = os.path.join(dirname, filename) 891 filepath = os.path.join(dirname, filename)
840 try: 892 try:
841 fileobj = file(filepath, 'rt') 893 fileobj = file(filepath, 'rt')
842 try: 894 try:
843 tmpl = Template(fileobj, filename=filepath) 895 tmpl = Template(fileobj, filename=filepath)
844 tmpl.input_filters.append(IncludeFilter(self, tmpl)) 896 tmpl.filters.append(IncludeFilter(self))
845 finally: 897 finally:
846 fileobj.close() 898 fileobj.close()
847 self._cache[filename] = tmpl 899 self._cache[filename] = tmpl
848 self._mtime[filename] = os.path.getmtime(filepath) 900 self._mtime[filename] = os.path.getmtime(filepath)
849 return tmpl 901 return tmpl
Copyright (C) 2012-2017 Edgewall Software