Mercurial > genshi > mirror
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 |