##// END OF EJS Templates
templater: replace eval with closure
Matt Mackall -
r8492:d72d1a97 default
parent child Browse files
Show More
@@ -1,242 +1,245 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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, 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 import util, config, templatefilters
10 import util, config, templatefilters
11
11
12 path = ['templates', '../templates']
12 path = ['templates', '../templates']
13 stringify = templatefilters.stringify
13 stringify = templatefilters.stringify
14
14
15 def parsestring(s, quoted=True):
15 def parsestring(s, quoted=True):
16 '''parse a string using simple c-like syntax.
16 '''parse a string using simple c-like syntax.
17 string must be in quotes if quoted is True.'''
17 string must be in quotes if quoted is True.'''
18 if quoted:
18 if quoted:
19 if len(s) < 2 or s[0] != s[-1]:
19 if len(s) < 2 or s[0] != s[-1]:
20 raise SyntaxError(_('unmatched quotes'))
20 raise SyntaxError(_('unmatched quotes'))
21 return s[1:-1].decode('string_escape')
21 return s[1:-1].decode('string_escape')
22
22
23 return s.decode('string_escape')
23 return s.decode('string_escape')
24
24
25 class engine(object):
25 class engine(object):
26 '''template expansion engine.
26 '''template expansion engine.
27
27
28 template expansion works like this. a map file contains key=value
28 template expansion works like this. a map file contains key=value
29 pairs. if value is quoted, it is treated as string. otherwise, it
29 pairs. if value is quoted, it is treated as string. otherwise, it
30 is treated as name of template file.
30 is treated as name of template file.
31
31
32 templater is asked to expand a key in map. it looks up key, and
32 templater is asked to expand a key in map. it looks up key, and
33 looks for strings like this: {foo}. it expands {foo} by looking up
33 looks for strings like this: {foo}. it expands {foo} by looking up
34 foo in map, and substituting it. expansion is recursive: it stops
34 foo in map, and substituting it. expansion is recursive: it stops
35 when there is no more {foo} to replace.
35 when there is no more {foo} to replace.
36
36
37 expansion also allows formatting and filtering.
37 expansion also allows formatting and filtering.
38
38
39 format uses key to expand each item in list. syntax is
39 format uses key to expand each item in list. syntax is
40 {key%format}.
40 {key%format}.
41
41
42 filter uses function to transform value. syntax is
42 filter uses function to transform value. syntax is
43 {key|filter1|filter2|...}.'''
43 {key|filter1|filter2|...}.'''
44
44
45 template_re = re.compile(r'{([\w\|%]+)}|#([\w\|%]+)#')
45 template_re = re.compile(r'{([\w\|%]+)}|#([\w\|%]+)#')
46
46
47 def __init__(self, loader, filters={}, defaults={}):
47 def __init__(self, loader, filters={}, defaults={}):
48 self.loader = loader
48 self.loader = loader
49 self.filters = filters
49 self.filters = filters
50 self.defaults = defaults
50 self.defaults = defaults
51 self.cache = {}
51 self.cache = {}
52
52
53 def process(self, t, map):
53 def process(self, t, map):
54 '''Perform expansion. t is name of map element to expand. map contains
54 '''Perform expansion. t is name of map element to expand. map contains
55 added elements for use during expansion. Is a generator.'''
55 added elements for use during expansion. Is a generator.'''
56 tmpl = self.loader(t)
56 tmpl = self.loader(t)
57 iters = [self._process(tmpl, map)]
57 iters = [self._process(tmpl, map)]
58 while iters:
58 while iters:
59 try:
59 try:
60 item = iters[0].next()
60 item = iters[0].next()
61 except StopIteration:
61 except StopIteration:
62 iters.pop(0)
62 iters.pop(0)
63 continue
63 continue
64 if isinstance(item, str):
64 if isinstance(item, str):
65 yield item
65 yield item
66 elif item is None:
66 elif item is None:
67 yield ''
67 yield ''
68 elif hasattr(item, '__iter__'):
68 elif hasattr(item, '__iter__'):
69 iters.insert(0, iter(item))
69 iters.insert(0, iter(item))
70 else:
70 else:
71 yield str(item)
71 yield str(item)
72
72
73 def _format(self, expr, get, map):
73 def _format(self, expr, get, map):
74 key, format = expr.split('%')
74 key, format = expr.split('%')
75 v = get(key)
75 v = get(key)
76 if not hasattr(v, '__iter__'):
76 if not hasattr(v, '__iter__'):
77 raise SyntaxError(_("Error expanding '%s%%%s'") % (key, format))
77 raise SyntaxError(_("Error expanding '%s%%%s'") % (key, format))
78 lm = map.copy()
78 lm = map.copy()
79 for i in v:
79 for i in v:
80 lm.update(i)
80 lm.update(i)
81 yield self.process(format, lm)
81 yield self.process(format, lm)
82
82
83 def _filter(self, expr, get, map):
83 def _filter(self, expr, get, map):
84 if expr not in self.cache:
84 if expr not in self.cache:
85 parts = expr.split('|')
85 parts = expr.split('|')
86 filters = parts[1:]
86 val = parts[0]
87 for f in filters:
87 try:
88 if f not in self.filters:
88 filters = [self.filters[f] for f in parts[1:]]
89 raise SyntaxError(_("unknown filter '%s'") % expr)
89 except KeyError, i:
90 calls = '('.join(i for i in reversed(filters))
90 raise SyntaxError(_("unknown filter '%s'") % i[0])
91 end = ')' * len(filters)
91 def apply(get):
92 code = "lambda _get: %s(_get('%s')%s" % (calls, parts[0], end)
92 x = get(val)
93 self.cache[expr] = eval(code, self.filters)
93 for f in filters:
94 x = f(x)
95 return x
96 self.cache[expr] = apply
94 return self.cache[expr](get)
97 return self.cache[expr](get)
95
98
96 def _process(self, tmpl, map):
99 def _process(self, tmpl, map):
97 '''Render a template. Returns a generator.'''
100 '''Render a template. Returns a generator.'''
98
101
99 def get(key):
102 def get(key):
100 v = map.get(key)
103 v = map.get(key)
101 if v is None:
104 if v is None:
102 v = self.defaults.get(key, '')
105 v = self.defaults.get(key, '')
103 if hasattr(v, '__call__'):
106 if hasattr(v, '__call__'):
104 v = v(**map)
107 v = v(**map)
105 return v
108 return v
106
109
107 while tmpl:
110 while tmpl:
108 m = self.template_re.search(tmpl)
111 m = self.template_re.search(tmpl)
109 if not m:
112 if not m:
110 yield tmpl
113 yield tmpl
111 break
114 break
112
115
113 start, end = m.span(0)
116 start, end = m.span(0)
114 variants = m.groups()
117 variants = m.groups()
115 expr = variants[0] or variants[1]
118 expr = variants[0] or variants[1]
116
119
117 if start:
120 if start:
118 yield tmpl[:start]
121 yield tmpl[:start]
119 tmpl = tmpl[end:]
122 tmpl = tmpl[end:]
120
123
121 if '%' in expr:
124 if '%' in expr:
122 yield self._format(expr, get, map)
125 yield self._format(expr, get, map)
123 elif '|' in expr:
126 elif '|' in expr:
124 yield self._filter(expr, get, map)
127 yield self._filter(expr, get, map)
125 else:
128 else:
126 yield get(expr)
129 yield get(expr)
127
130
128 engines = {'default': engine}
131 engines = {'default': engine}
129
132
130 class templater(object):
133 class templater(object):
131
134
132 def __init__(self, mapfile, filters={}, defaults={}, cache={},
135 def __init__(self, mapfile, filters={}, defaults={}, cache={},
133 minchunk=1024, maxchunk=65536):
136 minchunk=1024, maxchunk=65536):
134 '''set up template engine.
137 '''set up template engine.
135 mapfile is name of file to read map definitions from.
138 mapfile is name of file to read map definitions from.
136 filters is dict of functions. each transforms a value into another.
139 filters is dict of functions. each transforms a value into another.
137 defaults is dict of default map definitions.'''
140 defaults is dict of default map definitions.'''
138 self.mapfile = mapfile or 'template'
141 self.mapfile = mapfile or 'template'
139 self.cache = cache.copy()
142 self.cache = cache.copy()
140 self.map = {}
143 self.map = {}
141 self.base = (mapfile and os.path.dirname(mapfile)) or ''
144 self.base = (mapfile and os.path.dirname(mapfile)) or ''
142 self.filters = templatefilters.filters.copy()
145 self.filters = templatefilters.filters.copy()
143 self.filters.update(filters)
146 self.filters.update(filters)
144 self.defaults = defaults
147 self.defaults = defaults
145 self.minchunk, self.maxchunk = minchunk, maxchunk
148 self.minchunk, self.maxchunk = minchunk, maxchunk
146 self.engines = {}
149 self.engines = {}
147
150
148 if not mapfile:
151 if not mapfile:
149 return
152 return
150 if not os.path.exists(mapfile):
153 if not os.path.exists(mapfile):
151 raise util.Abort(_('style not found: %s') % mapfile)
154 raise util.Abort(_('style not found: %s') % mapfile)
152
155
153 conf = config.config()
156 conf = config.config()
154 conf.read(mapfile)
157 conf.read(mapfile)
155
158
156 for key, val in conf[''].items():
159 for key, val in conf[''].items():
157 if val[0] in "'\"":
160 if val[0] in "'\"":
158 try:
161 try:
159 self.cache[key] = parsestring(val)
162 self.cache[key] = parsestring(val)
160 except SyntaxError, inst:
163 except SyntaxError, inst:
161 raise SyntaxError('%s: %s' %
164 raise SyntaxError('%s: %s' %
162 (conf.source('', key), inst.args[0]))
165 (conf.source('', key), inst.args[0]))
163 else:
166 else:
164 val = 'default', val
167 val = 'default', val
165 if ':' in val[1]:
168 if ':' in val[1]:
166 val = val[1].split(':', 1)
169 val = val[1].split(':', 1)
167 self.map[key] = val[0], os.path.join(self.base, val[1])
170 self.map[key] = val[0], os.path.join(self.base, val[1])
168
171
169 def __contains__(self, key):
172 def __contains__(self, key):
170 return key in self.cache or key in self.map
173 return key in self.cache or key in self.map
171
174
172 def load(self, t):
175 def load(self, t):
173 '''Get the template for the given template name. Use a local cache.'''
176 '''Get the template for the given template name. Use a local cache.'''
174 if not t in self.cache:
177 if not t in self.cache:
175 try:
178 try:
176 self.cache[t] = open(self.map[t][1]).read()
179 self.cache[t] = open(self.map[t][1]).read()
177 except IOError, inst:
180 except IOError, inst:
178 raise IOError(inst.args[0], _('template file %s: %s') %
181 raise IOError(inst.args[0], _('template file %s: %s') %
179 (self.map[t][1], inst.args[1]))
182 (self.map[t][1], inst.args[1]))
180 return self.cache[t]
183 return self.cache[t]
181
184
182 def __call__(self, t, **map):
185 def __call__(self, t, **map):
183 ttype = t in self.map and self.map[t][0] or 'default'
186 ttype = t in self.map and self.map[t][0] or 'default'
184 proc = self.engines.get(ttype)
187 proc = self.engines.get(ttype)
185 if proc is None:
188 if proc is None:
186 proc = engines[ttype](self.load, self.filters, self.defaults)
189 proc = engines[ttype](self.load, self.filters, self.defaults)
187 self.engines[ttype] = proc
190 self.engines[ttype] = proc
188
191
189 stream = proc.process(t, map)
192 stream = proc.process(t, map)
190 if self.minchunk:
193 if self.minchunk:
191 stream = util.increasingchunks(stream, min=self.minchunk,
194 stream = util.increasingchunks(stream, min=self.minchunk,
192 max=self.maxchunk)
195 max=self.maxchunk)
193 return stream
196 return stream
194
197
195 def templatepath(name=None):
198 def templatepath(name=None):
196 '''return location of template file or directory (if no name).
199 '''return location of template file or directory (if no name).
197 returns None if not found.'''
200 returns None if not found.'''
198 normpaths = []
201 normpaths = []
199
202
200 # executable version (py2exe) doesn't support __file__
203 # executable version (py2exe) doesn't support __file__
201 if hasattr(sys, 'frozen'):
204 if hasattr(sys, 'frozen'):
202 module = sys.executable
205 module = sys.executable
203 else:
206 else:
204 module = __file__
207 module = __file__
205 for f in path:
208 for f in path:
206 if f.startswith('/'):
209 if f.startswith('/'):
207 p = f
210 p = f
208 else:
211 else:
209 fl = f.split('/')
212 fl = f.split('/')
210 p = os.path.join(os.path.dirname(module), *fl)
213 p = os.path.join(os.path.dirname(module), *fl)
211 if name:
214 if name:
212 p = os.path.join(p, name)
215 p = os.path.join(p, name)
213 if name and os.path.exists(p):
216 if name and os.path.exists(p):
214 return os.path.normpath(p)
217 return os.path.normpath(p)
215 elif os.path.isdir(p):
218 elif os.path.isdir(p):
216 normpaths.append(os.path.normpath(p))
219 normpaths.append(os.path.normpath(p))
217
220
218 return normpaths
221 return normpaths
219
222
220 def stylemap(style, paths=None):
223 def stylemap(style, paths=None):
221 """Return path to mapfile for a given style.
224 """Return path to mapfile for a given style.
222
225
223 Searches mapfile in the following locations:
226 Searches mapfile in the following locations:
224 1. templatepath/style/map
227 1. templatepath/style/map
225 2. templatepath/map-style
228 2. templatepath/map-style
226 3. templatepath/map
229 3. templatepath/map
227 """
230 """
228
231
229 if paths is None:
232 if paths is None:
230 paths = templatepath()
233 paths = templatepath()
231 elif isinstance(paths, str):
234 elif isinstance(paths, str):
232 paths = [paths]
235 paths = [paths]
233
236
234 locations = style and [os.path.join(style, "map"), "map-" + style] or []
237 locations = style and [os.path.join(style, "map"), "map-" + style] or []
235 locations.append("map")
238 locations.append("map")
236 for path in paths:
239 for path in paths:
237 for location in locations:
240 for location in locations:
238 mapfile = os.path.join(path, location)
241 mapfile = os.path.join(path, location)
239 if os.path.isfile(mapfile):
242 if os.path.isfile(mapfile):
240 return mapfile
243 return mapfile
241
244
242 raise RuntimeError("No hgweb templates found in %r" % paths)
245 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now