##// END OF EJS Templates
templater: clarify engine caching
Matt Mackall -
r13187:e3b87fb3 default
parent child Browse files
Show More
@@ -1,390 +1,390 b''
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import sys, os
10 10 import util, config, templatefilters, parser, error
11 11
12 12 # template parsing
13 13
14 14 elements = {
15 15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 16 ",": (2, None, ("list", 2)),
17 17 "|": (5, None, ("|", 5)),
18 18 "%": (6, None, ("%", 6)),
19 19 ")": (0, None, None),
20 20 "symbol": (0, ("symbol",), None),
21 21 "string": (0, ("string",), None),
22 22 "end": (0, None, None),
23 23 }
24 24
25 25 def tokenizer(data):
26 26 program, start, end = data
27 27 pos = start
28 28 while pos < end:
29 29 c = program[pos]
30 30 if c.isspace(): # skip inter-token whitespace
31 31 pass
32 32 elif c in "(,)%|": # handle simple operators
33 33 yield (c, None, pos)
34 34 elif (c in '"\'' or c == 'r' and
35 35 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
36 36 if c == 'r':
37 37 pos += 1
38 38 c = program[pos]
39 39 decode = lambda x: x
40 40 else:
41 41 decode = lambda x: x.decode('string-escape')
42 42 pos += 1
43 43 s = pos
44 44 while pos < end: # find closing quote
45 45 d = program[pos]
46 46 if d == '\\': # skip over escaped characters
47 47 pos += 2
48 48 continue
49 49 if d == c:
50 50 yield ('string', decode(program[s:pos]), s)
51 51 break
52 52 pos += 1
53 53 else:
54 54 raise error.ParseError(_("unterminated string"), s)
55 55 elif c.isalnum() or c in '_':
56 56 s = pos
57 57 pos += 1
58 58 while pos < end: # find end of symbol
59 59 d = program[pos]
60 60 if not (d.isalnum() or d == "_"):
61 61 break
62 62 pos += 1
63 63 sym = program[s:pos]
64 64 yield ('symbol', sym, s)
65 65 pos -= 1
66 66 elif c == '}':
67 67 pos += 1
68 68 break
69 69 else:
70 70 raise error.ParseError(_("syntax error"), pos)
71 71 pos += 1
72 72 data[2] = pos
73 73 yield ('end', None, pos)
74 74
75 75 def compiletemplate(tmpl, context):
76 76 parsed = []
77 77 pos, stop = 0, len(tmpl)
78 78 p = parser.parser(tokenizer, elements)
79 79
80 80 while pos < stop:
81 81 n = tmpl.find('{', pos)
82 82 if n < 0:
83 83 parsed.append(("string", tmpl[pos:]))
84 84 break
85 85 if n > 0 and tmpl[n - 1] == '\\':
86 86 # escaped
87 87 parsed.append(("string", tmpl[pos:n - 1] + "{"))
88 88 pos = n + 1
89 89 continue
90 90 if n > pos:
91 91 parsed.append(("string", tmpl[pos:n]))
92 92
93 93 pd = [tmpl, n + 1, stop]
94 94 parsed.append(p.parse(pd))
95 95 pos = pd[2]
96 96
97 97 return [compileexp(e, context) for e in parsed]
98 98
99 99 def compileexp(exp, context):
100 100 t = exp[0]
101 101 if t in methods:
102 102 return methods[t](exp, context)
103 103 raise error.ParseError(_("unknown method '%s'") % t)
104 104
105 105 # template evaluation
106 106
107 107 def getsymbol(exp):
108 108 if exp[0] == 'symbol':
109 109 return exp[1]
110 110 raise error.ParseError(_("expected a symbol"))
111 111
112 112 def getlist(x):
113 113 if not x:
114 114 return []
115 115 if x[0] == 'list':
116 116 return getlist(x[1]) + [x[2]]
117 117 return [x]
118 118
119 119 def getfilter(exp, context):
120 120 f = getsymbol(exp)
121 121 if f not in context._filters:
122 122 raise error.ParseError(_("unknown function '%s'") % f)
123 123 return context._filters[f]
124 124
125 125 def gettemplate(exp, context):
126 126 if exp[0] == 'string':
127 127 return compiletemplate(exp[1], context)
128 128 if exp[0] == 'symbol':
129 129 return context._load(exp[1])
130 130 raise error.ParseError(_("expected template specifier"))
131 131
132 132 def runstring(context, mapping, data):
133 133 return data
134 134
135 135 def runsymbol(context, mapping, key):
136 136 v = mapping.get(key)
137 137 if v is None:
138 138 v = context._defaults.get(key, '')
139 139 if hasattr(v, '__call__'):
140 140 return v(**mapping)
141 141 return v
142 142
143 143 def buildfilter(exp, context):
144 144 func, data = compileexp(exp[1], context)
145 145 filt = getfilter(exp[2], context)
146 146 return (runfilter, (func, data, filt))
147 147
148 148 def runfilter(context, mapping, data):
149 149 func, data, filt = data
150 150 return filt(func(context, mapping, data))
151 151
152 152 def buildmap(exp, context):
153 153 func, data = compileexp(exp[1], context)
154 154 ctmpl = gettemplate(exp[2], context)
155 155 return (runmap, (func, data, ctmpl))
156 156
157 157 def runmap(context, mapping, data):
158 158 func, data, ctmpl = data
159 159 d = func(context, mapping, data)
160 160 lm = mapping.copy()
161 161
162 162 for i in d:
163 163 if isinstance(i, dict):
164 164 lm.update(i)
165 165 for f, d in ctmpl:
166 166 yield f(context, lm, d)
167 167 else:
168 168 # v is not an iterable of dicts, this happen when 'key'
169 169 # has been fully expanded already and format is useless.
170 170 # If so, return the expanded value.
171 171 yield i
172 172
173 173 def buildfunc(exp, context):
174 174 n = getsymbol(exp[1])
175 175 args = [compileexp(x, context) for x in getlist(exp[2])]
176 176 if n in context._filters:
177 177 if len(args) != 1:
178 178 raise error.ParseError(_("filter %s expects one argument") % n)
179 179 f = context._filters[n]
180 180 return (runfilter, (args[0][0], args[0][1], f))
181 181 elif n in context._funcs:
182 182 f = context._funcs[n]
183 183 return (f, args)
184 184
185 185 methods = {
186 186 "string": lambda e, c: (runstring, e[1]),
187 187 "symbol": lambda e, c: (runsymbol, e[1]),
188 188 "group": lambda e, c: compileexp(e[1], c),
189 189 # ".": buildmember,
190 190 "|": buildfilter,
191 191 "%": buildmap,
192 192 "func": buildfunc,
193 193 }
194 194
195 195 # template engine
196 196
197 197 path = ['templates', '../templates']
198 198 stringify = templatefilters.stringify
199 199
200 200 def _flatten(thing):
201 201 '''yield a single stream from a possibly nested set of iterators'''
202 202 if isinstance(thing, str):
203 203 yield thing
204 204 elif not hasattr(thing, '__iter__'):
205 205 if thing is not None:
206 206 yield str(thing)
207 207 else:
208 208 for i in thing:
209 209 if isinstance(i, str):
210 210 yield i
211 211 elif not hasattr(i, '__iter__'):
212 212 if i is not None:
213 213 yield str(i)
214 214 elif i is not None:
215 215 for j in _flatten(i):
216 216 yield j
217 217
218 218 def parsestring(s, quoted=True):
219 219 '''parse a string using simple c-like syntax.
220 220 string must be in quotes if quoted is True.'''
221 221 if quoted:
222 222 if len(s) < 2 or s[0] != s[-1]:
223 223 raise SyntaxError(_('unmatched quotes'))
224 224 return s[1:-1].decode('string_escape')
225 225
226 226 return s.decode('string_escape')
227 227
228 228 class engine(object):
229 229 '''template expansion engine.
230 230
231 231 template expansion works like this. a map file contains key=value
232 232 pairs. if value is quoted, it is treated as string. otherwise, it
233 233 is treated as name of template file.
234 234
235 235 templater is asked to expand a key in map. it looks up key, and
236 236 looks for strings like this: {foo}. it expands {foo} by looking up
237 237 foo in map, and substituting it. expansion is recursive: it stops
238 238 when there is no more {foo} to replace.
239 239
240 240 expansion also allows formatting and filtering.
241 241
242 242 format uses key to expand each item in list. syntax is
243 243 {key%format}.
244 244
245 245 filter uses function to transform value. syntax is
246 246 {key|filter1|filter2|...}.'''
247 247
248 248 def __init__(self, loader, filters={}, defaults={}):
249 249 self._loader = loader
250 250 self._filters = filters
251 251 self._defaults = defaults
252 252 self._cache = {}
253 253
254 254 def _load(self, t):
255 255 '''load, parse, and cache a template'''
256 256 if t not in self._cache:
257 257 self._cache[t] = compiletemplate(self._loader(t), self)
258 258 return self._cache[t]
259 259
260 260 def process(self, t, mapping):
261 261 '''Perform expansion. t is name of map element to expand.
262 262 mapping contains added elements for use during expansion. Is a
263 263 generator.'''
264 264 return _flatten(func(self, mapping, data) for func, data in
265 265 self._load(t))
266 266
267 267 engines = {'default': engine}
268 268
269 269 class templater(object):
270 270
271 271 def __init__(self, mapfile, filters={}, defaults={}, cache={},
272 272 minchunk=1024, maxchunk=65536):
273 273 '''set up template engine.
274 274 mapfile is name of file to read map definitions from.
275 275 filters is dict of functions. each transforms a value into another.
276 276 defaults is dict of default map definitions.'''
277 277 self.mapfile = mapfile or 'template'
278 278 self.cache = cache.copy()
279 279 self.map = {}
280 280 self.base = (mapfile and os.path.dirname(mapfile)) or ''
281 281 self.filters = templatefilters.filters.copy()
282 282 self.filters.update(filters)
283 283 self.defaults = defaults
284 284 self.minchunk, self.maxchunk = minchunk, maxchunk
285 self.engines = {}
285 self.ecache = {}
286 286
287 287 if not mapfile:
288 288 return
289 289 if not os.path.exists(mapfile):
290 290 raise util.Abort(_('style not found: %s') % mapfile)
291 291
292 292 conf = config.config()
293 293 conf.read(mapfile)
294 294
295 295 for key, val in conf[''].items():
296 296 if val[0] in "'\"":
297 297 try:
298 298 self.cache[key] = parsestring(val)
299 299 except SyntaxError, inst:
300 300 raise SyntaxError('%s: %s' %
301 301 (conf.source('', key), inst.args[0]))
302 302 else:
303 303 val = 'default', val
304 304 if ':' in val[1]:
305 305 val = val[1].split(':', 1)
306 306 self.map[key] = val[0], os.path.join(self.base, val[1])
307 307
308 308 def __contains__(self, key):
309 309 return key in self.cache or key in self.map
310 310
311 311 def load(self, t):
312 312 '''Get the template for the given template name. Use a local cache.'''
313 313 if not t in self.cache:
314 314 try:
315 315 self.cache[t] = open(self.map[t][1]).read()
316 316 except KeyError, inst:
317 317 raise util.Abort(_('"%s" not in template map') % inst.args[0])
318 318 except IOError, inst:
319 319 raise IOError(inst.args[0], _('template file %s: %s') %
320 320 (self.map[t][1], inst.args[1]))
321 321 return self.cache[t]
322 322
323 323 def __call__(self, t, **mapping):
324 324 ttype = t in self.map and self.map[t][0] or 'default'
325 proc = self.engines.get(ttype)
326 if proc is None:
327 proc = engines[ttype](self.load, self.filters, self.defaults)
328 self.engines[ttype] = proc
325 if ttype not in self.ecache:
326 self.ecache[ttype] = engines[ttype](self.load,
327 self.filters, self.defaults)
328 proc = self.ecache[ttype]
329 329
330 330 stream = proc.process(t, mapping)
331 331 if self.minchunk:
332 332 stream = util.increasingchunks(stream, min=self.minchunk,
333 333 max=self.maxchunk)
334 334 return stream
335 335
336 336 def templatepath(name=None):
337 337 '''return location of template file or directory (if no name).
338 338 returns None if not found.'''
339 339 normpaths = []
340 340
341 341 # executable version (py2exe) doesn't support __file__
342 342 if hasattr(sys, 'frozen'):
343 343 module = sys.executable
344 344 else:
345 345 module = __file__
346 346 for f in path:
347 347 if f.startswith('/'):
348 348 p = f
349 349 else:
350 350 fl = f.split('/')
351 351 p = os.path.join(os.path.dirname(module), *fl)
352 352 if name:
353 353 p = os.path.join(p, name)
354 354 if name and os.path.exists(p):
355 355 return os.path.normpath(p)
356 356 elif os.path.isdir(p):
357 357 normpaths.append(os.path.normpath(p))
358 358
359 359 return normpaths
360 360
361 361 def stylemap(styles, paths=None):
362 362 """Return path to mapfile for a given style.
363 363
364 364 Searches mapfile in the following locations:
365 365 1. templatepath/style/map
366 366 2. templatepath/map-style
367 367 3. templatepath/map
368 368 """
369 369
370 370 if paths is None:
371 371 paths = templatepath()
372 372 elif isinstance(paths, str):
373 373 paths = [paths]
374 374
375 375 if isinstance(styles, str):
376 376 styles = [styles]
377 377
378 378 for style in styles:
379 379 if not style:
380 380 continue
381 381 locations = [os.path.join(style, 'map'), 'map-' + style]
382 382 locations.append('map')
383 383
384 384 for path in paths:
385 385 for location in locations:
386 386 mapfile = os.path.join(path, location)
387 387 if os.path.isfile(mapfile):
388 388 return style, mapfile
389 389
390 390 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now