comparison markup/template.py @ 21:b4d17897d053 trunk

* Include paths are now interpreted relative to the path of the including template. Closes #3. * The filename is now included as first item in the `pos` tuple of stream events. * Simplified the "basic" example so that it actually ''is'' basic. * Added a more complex example using nested relative includes in [source:/trunk/examples/includes/ examples/includes].
author cmlenz
date Tue, 20 Jun 2006 13:05:37 +0000
parents 5420cfe42d36
children 2483fe549959
comparison
equal deleted inserted replaced
20:cc92d74ce9e5 21:b4d17897d053
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 import os 45 import os
46 import posixpath
46 import re 47 import re
47 from StringIO import StringIO 48 from StringIO import StringIO
48 49
49 from markup.core import Attributes, Namespace, Stream, StreamEventKind 50 from markup.core import Attributes, Namespace, Stream, StreamEventKind
50 from markup.eval import Expression 51 from markup.eval import Expression
572 ('attrs', AttrsDirective), 573 ('attrs', AttrsDirective),
573 ('strip', StripDirective)] 574 ('strip', StripDirective)]
574 _dir_by_name = dict(directives) 575 _dir_by_name = dict(directives)
575 _dir_order = [directive[1] for directive in directives] 576 _dir_order = [directive[1] for directive in directives]
576 577
577 def __init__(self, source, filename=None): 578 def __init__(self, source, basedir=None, filename=None):
578 """Initialize a template from either a string or a file-like object.""" 579 """Initialize a template from either a string or a file-like object."""
579 if isinstance(source, basestring): 580 if isinstance(source, basestring):
580 self.source = StringIO(source) 581 self.source = StringIO(source)
581 else: 582 else:
582 self.source = source 583 self.source = source
584 self.basedir = basedir
583 self.filename = filename or '<string>' 585 self.filename = filename or '<string>'
586 if basedir and filename:
587 self.filepath = os.path.join(basedir, filename)
588 else:
589 self.filepath = '<string>'
584 590
585 self.filters = [self._eval, self._match] 591 self.filters = [self._eval, self._match]
586 self.parse() 592 self.parse()
587 593
588 def __repr__(self): 594 def __repr__(self):
589 return '<%s "%s">' % (self.__class__.__name__, 595 return '<%s "%s">' % (self.__class__.__name__, self.filename)
590 os.path.basename(self.filename))
591 596
592 def parse(self): 597 def parse(self):
593 """Parse the template. 598 """Parse the template.
594 599
595 The parsing stage parses the XML template and constructs a list of 600 The parsing stage parses the XML template and constructs a list of
601 stream = [] # list of events of the "compiled" template 606 stream = [] # list of events of the "compiled" template
602 dirmap = {} # temporary mapping of directives to elements 607 dirmap = {} # temporary mapping of directives to elements
603 ns_prefix = {} 608 ns_prefix = {}
604 depth = 0 609 depth = 0
605 610
606 for kind, data, pos in XMLParser(self.source): 611 for kind, data, pos in XMLParser(self.source, filename=self.filename):
607 612
608 if kind is Stream.START_NS: 613 if kind is Stream.START_NS:
609 # Strip out the namespace declaration for template directives 614 # Strip out the namespace declaration for template directives
610 prefix, uri = data 615 prefix, uri = data
611 if uri == self.NAMESPACE: 616 if uri == self.NAMESPACE:
626 new_attrib = [] 631 new_attrib = []
627 for name, value in attrib: 632 for name, value in attrib:
628 if name in self.NAMESPACE: 633 if name in self.NAMESPACE:
629 cls = self._dir_by_name.get(name.localname) 634 cls = self._dir_by_name.get(name.localname)
630 if cls is None: 635 if cls is None:
631 raise BadDirectiveError(name, self.filename, pos[0]) 636 raise BadDirectiveError(name, self.filename, pos[1])
632 else: 637 else:
633 directives.append(cls(self, value, pos)) 638 directives.append(cls(self, value, pos))
634 else: 639 else:
635 value = list(self._interpolate(value, *pos)) 640 value = list(self._interpolate(value, *pos))
636 new_attrib.append((name, value)) 641 new_attrib.append((name, value))
664 self.stream = stream 669 self.stream = stream
665 670
666 _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}') 671 _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}')
667 _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)') 672 _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)')
668 673
669 def _interpolate(cls, text, lineno=-1, offset=-1): 674 def _interpolate(cls, text, filename=None, lineno=-1, offset=-1):
670 """Parse the given string and extract expressions. 675 """Parse the given string and extract expressions.
671 676
672 This method returns a list containing both literal text and `Expression` 677 This method returns a list containing both literal text and `Expression`
673 objects. 678 objects.
674 679
766 yield event 771 yield event
767 continue 772 continue
768 else: 773 else:
769 yield kind, data, pos 774 yield kind, data, pos
770 except SyntaxError, err: 775 except SyntaxError, err:
771 raise TemplateSyntaxError(err, self.filename, pos[0], 776 raise TemplateSyntaxError(err, self.filename, pos[1],
772 pos[1] + (err.offset or 0)) 777 pos[2] + (err.offset or 0))
773 778
774 def _match(self, stream, ctxt=None): 779 def _match(self, stream, ctxt=None):
775 for kind, data, pos in stream: 780 for kind, data, pos in stream:
776 781
777 # We (currently) only care about start and end events for matching 782 # We (currently) only care about start and end events for matching
781 continue 786 continue
782 787
783 for idx, (test, path, template) in enumerate(ctxt._match_templates): 788 for idx, (test, path, template) in enumerate(ctxt._match_templates):
784 if (kind, data, pos) in template[::len(template)]: 789 if (kind, data, pos) in template[::len(template)]:
785 # This is the event this match template produced itself, so 790 # This is the event this match template produced itself, so
786 # matching it again would result in an infinite loop 791 # matching it again would result in an infinite loop
787 continue 792 continue
788 793
789 result = test(kind, data, pos) 794 result = test(kind, data, pos)
790 795
791 if result: 796 if result:
802 content.append(event) 807 content.append(event)
803 808
804 # enable the path to keep track of the stream state 809 # enable the path to keep track of the stream state
805 test(*event) 810 test(*event)
806 811
807 content = list(self._flatten(content, ctxt, apply_filters=False)) 812 content = list(self._flatten(content, ctxt, False))
808 813
809 def _apply(stream, ctxt): 814 def _apply(stream, ctxt):
810 stream = list(stream) 815 stream = list(stream)
811 ctxt.push(select=lambda path: Stream(stream).select(path)) 816 ctxt.push(select=lambda path: Stream(stream).select(path))
812 for event in template: 817 for event in template:
863 self.search_path = [] 868 self.search_path = []
864 self.auto_reload = auto_reload 869 self.auto_reload = auto_reload
865 self._cache = {} 870 self._cache = {}
866 self._mtime = {} 871 self._mtime = {}
867 872
868 def load(self, filename): 873 def load(self, filename, relative_to=None):
869 """Load the template with the given name. 874 """Load the template with the given name.
870 875
871 This method searches the search path trying to locate a template 876 This method searches the search path trying to locate a template
872 matching the given name. If no such template is found, a 877 matching the given name. If no such template is found, a
873 `TemplateNotFound` exception is raised. Otherwise, a `Template` object 878 `TemplateNotFound` exception is raised. Otherwise, a `Template` object
875 880
876 Template searches are cached to avoid having to parse the same template 881 Template searches are cached to avoid having to parse the same template
877 file more than once. Thus, subsequent calls of this method with the 882 file more than once. Thus, subsequent calls of this method with the
878 same template file name will return the same `Template` object. 883 same template file name will return the same `Template` object.
879 884
885 If the `relative_to` parameter is provided, the `filename` is
886 interpreted as being relative to that path.
887
880 @param filename: the relative path of the template file to load 888 @param filename: the relative path of the template file to load
889 @param relative_to: the filename of the template from which the new
890 template is being loaded, or `None` if the template is being loaded
891 directly
881 """ 892 """
893 if relative_to:
894 filename = posixpath.join(posixpath.dirname(relative_to), filename)
882 filename = os.path.normpath(filename) 895 filename = os.path.normpath(filename)
883 try: 896 try:
884 tmpl = self._cache[filename] 897 tmpl = self._cache[filename]
885 if not self.auto_reload or \ 898 if not self.auto_reload or \
886 os.path.getmtime(tmpl.filename) == self._mtime[filename]: 899 os.path.getmtime(tmpl.filepath) == self._mtime[filename]:
887 return tmpl 900 return tmpl
888 except KeyError: 901 except KeyError:
889 pass 902 pass
890 for dirname in self.search_path: 903 for dirname in self.search_path:
891 filepath = os.path.join(dirname, filename) 904 filepath = os.path.join(dirname, filename)
892 try: 905 try:
893 fileobj = file(filepath, 'rt') 906 fileobj = file(filepath, 'rt')
894 try: 907 try:
895 tmpl = Template(fileobj, filename=filepath) 908 tmpl = Template(fileobj, basedir=dirname, filename=filename)
896 tmpl.filters.append(IncludeFilter(self)) 909 tmpl.filters.append(IncludeFilter(self))
897 finally: 910 finally:
898 fileobj.close() 911 fileobj.close()
899 self._cache[filename] = tmpl 912 self._cache[filename] = tmpl
900 self._mtime[filename] = os.path.getmtime(filepath) 913 self._mtime[filename] = os.path.getmtime(filepath)
Copyright (C) 2012-2017 Edgewall Software