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