##// END OF EJS Templates
templater: make a template a string-only iterator
Dirkjan Ochtman -
r6783:6d824dc8 default
parent child Browse files
Show More
@@ -1,150 +1,171 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 import re, sys, os
9 import re, sys, os
10 from mercurial import util
10 from mercurial import util
11
11
12 def parsestring(s, quoted=True):
12 def parsestring(s, quoted=True):
13 '''parse a string using simple c-like syntax.
13 '''parse a string using simple c-like syntax.
14 string must be in quotes if quoted is True.'''
14 string must be in quotes if quoted is True.'''
15 if quoted:
15 if quoted:
16 if len(s) < 2 or s[0] != s[-1]:
16 if len(s) < 2 or s[0] != s[-1]:
17 raise SyntaxError(_('unmatched quotes'))
17 raise SyntaxError(_('unmatched quotes'))
18 return s[1:-1].decode('string_escape')
18 return s[1:-1].decode('string_escape')
19
19
20 return s.decode('string_escape')
20 return s.decode('string_escape')
21
21
22 class templater(object):
22 class templater(object):
23 '''template expansion engine.
23 '''template expansion engine.
24
24
25 template expansion works like this. a map file contains key=value
25 template expansion works like this. a map file contains key=value
26 pairs. if value is quoted, it is treated as string. otherwise, it
26 pairs. if value is quoted, it is treated as string. otherwise, it
27 is treated as name of template file.
27 is treated as name of template file.
28
28
29 templater is asked to expand a key in map. it looks up key, and
29 templater is asked to expand a key in map. it looks up key, and
30 looks for strings like this: {foo}. it expands {foo} by looking up
30 looks for strings like this: {foo}. it expands {foo} by looking up
31 foo in map, and substituting it. expansion is recursive: it stops
31 foo in map, and substituting it. expansion is recursive: it stops
32 when there is no more {foo} to replace.
32 when there is no more {foo} to replace.
33
33
34 expansion also allows formatting and filtering.
34 expansion also allows formatting and filtering.
35
35
36 format uses key to expand each item in list. syntax is
36 format uses key to expand each item in list. syntax is
37 {key%format}.
37 {key%format}.
38
38
39 filter uses function to transform value. syntax is
39 filter uses function to transform value. syntax is
40 {key|filter1|filter2|...}.'''
40 {key|filter1|filter2|...}.'''
41
41
42 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
42 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
43 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
43 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
44
44
45 def __init__(self, mapfile, filters={}, defaults={}, cache={}):
45 def __init__(self, mapfile, filters={}, defaults={}, cache={}):
46 '''set up template engine.
46 '''set up template engine.
47 mapfile is name of file to read map definitions from.
47 mapfile is name of file to read map definitions from.
48 filters is dict of functions. each transforms a value into another.
48 filters is dict of functions. each transforms a value into another.
49 defaults is dict of default map definitions.'''
49 defaults is dict of default map definitions.'''
50 self.mapfile = mapfile or 'template'
50 self.mapfile = mapfile or 'template'
51 self.cache = cache.copy()
51 self.cache = cache.copy()
52 self.map = {}
52 self.map = {}
53 self.base = (mapfile and os.path.dirname(mapfile)) or ''
53 self.base = (mapfile and os.path.dirname(mapfile)) or ''
54 self.filters = filters
54 self.filters = filters
55 self.defaults = defaults
55 self.defaults = defaults
56
56
57 if not mapfile:
57 if not mapfile:
58 return
58 return
59 if not os.path.exists(mapfile):
59 if not os.path.exists(mapfile):
60 raise util.Abort(_('style not found: %s') % mapfile)
60 raise util.Abort(_('style not found: %s') % mapfile)
61
61
62 i = 0
62 i = 0
63 for l in file(mapfile):
63 for l in file(mapfile):
64 l = l.strip()
64 l = l.strip()
65 i += 1
65 i += 1
66 if not l or l[0] in '#;': continue
66 if not l or l[0] in '#;': continue
67 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
67 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
68 if m:
68 if m:
69 key, val = m.groups()
69 key, val = m.groups()
70 if val[0] in "'\"":
70 if val[0] in "'\"":
71 try:
71 try:
72 self.cache[key] = parsestring(val)
72 self.cache[key] = parsestring(val)
73 except SyntaxError, inst:
73 except SyntaxError, inst:
74 raise SyntaxError('%s:%s: %s' %
74 raise SyntaxError('%s:%s: %s' %
75 (mapfile, i, inst.args[0]))
75 (mapfile, i, inst.args[0]))
76 else:
76 else:
77 self.map[key] = os.path.join(self.base, val)
77 self.map[key] = os.path.join(self.base, val)
78 else:
78 else:
79 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
79 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
80
80
81 def __contains__(self, key):
81 def __contains__(self, key):
82 return key in self.cache or key in self.map
82 return key in self.cache or key in self.map
83
83
84 def __call__(self, t, **map):
84 def _template(self, t):
85 '''perform expansion.
85 '''Get the template for the given template name. Use a local cache.'''
86 t is name of map element to expand.
87 map is added elements to use during expansion.'''
88 if not t in self.cache:
86 if not t in self.cache:
89 try:
87 try:
90 self.cache[t] = file(self.map[t]).read()
88 self.cache[t] = file(self.map[t]).read()
91 except IOError, inst:
89 except IOError, inst:
92 raise IOError(inst.args[0], _('template file %s: %s') %
90 raise IOError(inst.args[0], _('template file %s: %s') %
93 (self.map[t], inst.args[1]))
91 (self.map[t], inst.args[1]))
94 tmpl = self.cache[t]
92 return self.cache[t]
95
93
94 def _process(self, tmpl, map):
95 '''Render a template. Returns a generator.'''
96 while tmpl:
96 while tmpl:
97 m = self.template_re.search(tmpl)
97 m = self.template_re.search(tmpl)
98 if not m:
98 if not m:
99 yield tmpl
99 yield tmpl
100 break
100 break
101
101
102 start, end = m.span(0)
102 start, end = m.span(0)
103 key, format, fl = m.groups()
103 key, format, fl = m.groups()
104
104
105 if start:
105 if start:
106 yield tmpl[:start]
106 yield tmpl[:start]
107 tmpl = tmpl[end:]
107 tmpl = tmpl[end:]
108
108
109 if key in map:
109 if key in map:
110 v = map[key]
110 v = map[key]
111 else:
111 else:
112 v = self.defaults.get(key, "")
112 v = self.defaults.get(key, "")
113 if callable(v):
113 if callable(v):
114 v = v(**map)
114 v = v(**map)
115 if format:
115 if format:
116 if not hasattr(v, '__iter__'):
116 if not hasattr(v, '__iter__'):
117 raise SyntaxError(_("Error expanding '%s%%%s'")
117 raise SyntaxError(_("Error expanding '%s%%%s'")
118 % (key, format))
118 % (key, format))
119 lm = map.copy()
119 lm = map.copy()
120 for i in v:
120 for i in v:
121 lm.update(i)
121 lm.update(i)
122 yield self(format, **lm)
122 t = self._template(format)
123 yield self._process(t, lm)
123 else:
124 else:
124 if fl:
125 if fl:
125 for f in fl.split("|")[1:]:
126 for f in fl.split("|")[1:]:
126 v = self.filters[f](v)
127 v = self.filters[f](v)
127 yield v
128 yield v
128
129
130 def __call__(self, t, **map):
131 '''Perform expansion. t is name of map element to expand. map contains
132 added elements for use during expansion. Is a generator.'''
133 tmpl = self._template(t)
134 iters = [self._process(tmpl, map)]
135 while iters:
136 try:
137 item = iters[0].next()
138 except StopIteration:
139 iters.pop(0)
140 continue
141 if isinstance(item, str):
142 yield item
143 elif item is None:
144 yield ''
145 elif hasattr(item, '__iter__'):
146 iters.insert(0, iter(item))
147 else:
148 yield str(item)
149
129 def templatepath(name=None):
150 def templatepath(name=None):
130 '''return location of template file or directory (if no name).
151 '''return location of template file or directory (if no name).
131 returns None if not found.'''
152 returns None if not found.'''
132
153
133 # executable version (py2exe) doesn't support __file__
154 # executable version (py2exe) doesn't support __file__
134 if hasattr(sys, 'frozen'):
155 if hasattr(sys, 'frozen'):
135 module = sys.executable
156 module = sys.executable
136 else:
157 else:
137 module = __file__
158 module = __file__
138 for f in 'templates', '../templates':
159 for f in 'templates', '../templates':
139 fl = f.split('/')
160 fl = f.split('/')
140 if name: fl.append(name)
161 if name: fl.append(name)
141 p = os.path.join(os.path.dirname(module), *fl)
162 p = os.path.join(os.path.dirname(module), *fl)
142 if (name and os.path.exists(p)) or os.path.isdir(p):
163 if (name and os.path.exists(p)) or os.path.isdir(p):
143 return os.path.normpath(p)
164 return os.path.normpath(p)
144
165
145 def stringify(thing):
166 def stringify(thing):
146 '''turn nested template iterator into string.'''
167 '''turn nested template iterator into string.'''
147 if hasattr(thing, '__iter__'):
168 if hasattr(thing, '__iter__'):
148 return "".join([stringify(t) for t in thing if t is not None])
169 return "".join([stringify(t) for t in thing if t is not None])
149 return str(thing)
170 return str(thing)
150
171
General Comments 0
You need to be logged in to leave comments. Login now