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