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