##// END OF EJS Templates
templater: selecting a style with no templates does not crash (issue4140)...
Simon Heimberg -
r20312:268a5ab5 stable
parent child Browse files
Show More
@@ -1,587 +1,589 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, re
9 import sys, os, re
10 import util, config, templatefilters, parser, error
10 import util, config, templatefilters, parser, error
11 import types
11 import types
12 import minirst
12 import minirst
13
13
14 # template parsing
14 # template parsing
15
15
16 elements = {
16 elements = {
17 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
17 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
18 ",": (2, None, ("list", 2)),
18 ",": (2, None, ("list", 2)),
19 "|": (5, None, ("|", 5)),
19 "|": (5, None, ("|", 5)),
20 "%": (6, None, ("%", 6)),
20 "%": (6, None, ("%", 6)),
21 ")": (0, None, None),
21 ")": (0, None, None),
22 "symbol": (0, ("symbol",), None),
22 "symbol": (0, ("symbol",), None),
23 "string": (0, ("string",), None),
23 "string": (0, ("string",), None),
24 "end": (0, None, None),
24 "end": (0, None, None),
25 }
25 }
26
26
27 def tokenizer(data):
27 def tokenizer(data):
28 program, start, end = data
28 program, start, end = data
29 pos = start
29 pos = start
30 while pos < end:
30 while pos < end:
31 c = program[pos]
31 c = program[pos]
32 if c.isspace(): # skip inter-token whitespace
32 if c.isspace(): # skip inter-token whitespace
33 pass
33 pass
34 elif c in "(,)%|": # handle simple operators
34 elif c in "(,)%|": # handle simple operators
35 yield (c, None, pos)
35 yield (c, None, pos)
36 elif (c in '"\'' or c == 'r' and
36 elif (c in '"\'' or c == 'r' and
37 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
37 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
38 if c == 'r':
38 if c == 'r':
39 pos += 1
39 pos += 1
40 c = program[pos]
40 c = program[pos]
41 decode = False
41 decode = False
42 else:
42 else:
43 decode = True
43 decode = True
44 pos += 1
44 pos += 1
45 s = pos
45 s = pos
46 while pos < end: # find closing quote
46 while pos < end: # find closing quote
47 d = program[pos]
47 d = program[pos]
48 if decode and d == '\\': # skip over escaped characters
48 if decode and d == '\\': # skip over escaped characters
49 pos += 2
49 pos += 2
50 continue
50 continue
51 if d == c:
51 if d == c:
52 if not decode:
52 if not decode:
53 yield ('string', program[s:pos].replace('\\', r'\\'), s)
53 yield ('string', program[s:pos].replace('\\', r'\\'), s)
54 break
54 break
55 yield ('string', program[s:pos], s)
55 yield ('string', program[s:pos], s)
56 break
56 break
57 pos += 1
57 pos += 1
58 else:
58 else:
59 raise error.ParseError(_("unterminated string"), s)
59 raise error.ParseError(_("unterminated string"), s)
60 elif c.isalnum() or c in '_':
60 elif c.isalnum() or c in '_':
61 s = pos
61 s = pos
62 pos += 1
62 pos += 1
63 while pos < end: # find end of symbol
63 while pos < end: # find end of symbol
64 d = program[pos]
64 d = program[pos]
65 if not (d.isalnum() or d == "_"):
65 if not (d.isalnum() or d == "_"):
66 break
66 break
67 pos += 1
67 pos += 1
68 sym = program[s:pos]
68 sym = program[s:pos]
69 yield ('symbol', sym, s)
69 yield ('symbol', sym, s)
70 pos -= 1
70 pos -= 1
71 elif c == '}':
71 elif c == '}':
72 pos += 1
72 pos += 1
73 break
73 break
74 else:
74 else:
75 raise error.ParseError(_("syntax error"), pos)
75 raise error.ParseError(_("syntax error"), pos)
76 pos += 1
76 pos += 1
77 yield ('end', None, pos)
77 yield ('end', None, pos)
78
78
79 def compiletemplate(tmpl, context):
79 def compiletemplate(tmpl, context):
80 parsed = []
80 parsed = []
81 pos, stop = 0, len(tmpl)
81 pos, stop = 0, len(tmpl)
82 p = parser.parser(tokenizer, elements)
82 p = parser.parser(tokenizer, elements)
83 while pos < stop:
83 while pos < stop:
84 n = tmpl.find('{', pos)
84 n = tmpl.find('{', pos)
85 if n < 0:
85 if n < 0:
86 parsed.append(("string", tmpl[pos:].decode("string-escape")))
86 parsed.append(("string", tmpl[pos:].decode("string-escape")))
87 break
87 break
88 if n > 0 and tmpl[n - 1] == '\\':
88 if n > 0 and tmpl[n - 1] == '\\':
89 # escaped
89 # escaped
90 parsed.append(("string",
90 parsed.append(("string",
91 (tmpl[pos:n - 1] + "{").decode("string-escape")))
91 (tmpl[pos:n - 1] + "{").decode("string-escape")))
92 pos = n + 1
92 pos = n + 1
93 continue
93 continue
94 if n > pos:
94 if n > pos:
95 parsed.append(("string", tmpl[pos:n].decode("string-escape")))
95 parsed.append(("string", tmpl[pos:n].decode("string-escape")))
96
96
97 pd = [tmpl, n + 1, stop]
97 pd = [tmpl, n + 1, stop]
98 parseres, pos = p.parse(pd)
98 parseres, pos = p.parse(pd)
99 parsed.append(parseres)
99 parsed.append(parseres)
100
100
101 return [compileexp(e, context) for e in parsed]
101 return [compileexp(e, context) for e in parsed]
102
102
103 def compileexp(exp, context):
103 def compileexp(exp, context):
104 t = exp[0]
104 t = exp[0]
105 if t in methods:
105 if t in methods:
106 return methods[t](exp, context)
106 return methods[t](exp, context)
107 raise error.ParseError(_("unknown method '%s'") % t)
107 raise error.ParseError(_("unknown method '%s'") % t)
108
108
109 # template evaluation
109 # template evaluation
110
110
111 def getsymbol(exp):
111 def getsymbol(exp):
112 if exp[0] == 'symbol':
112 if exp[0] == 'symbol':
113 return exp[1]
113 return exp[1]
114 raise error.ParseError(_("expected a symbol"))
114 raise error.ParseError(_("expected a symbol"))
115
115
116 def getlist(x):
116 def getlist(x):
117 if not x:
117 if not x:
118 return []
118 return []
119 if x[0] == 'list':
119 if x[0] == 'list':
120 return getlist(x[1]) + [x[2]]
120 return getlist(x[1]) + [x[2]]
121 return [x]
121 return [x]
122
122
123 def getfilter(exp, context):
123 def getfilter(exp, context):
124 f = getsymbol(exp)
124 f = getsymbol(exp)
125 if f not in context._filters:
125 if f not in context._filters:
126 raise error.ParseError(_("unknown function '%s'") % f)
126 raise error.ParseError(_("unknown function '%s'") % f)
127 return context._filters[f]
127 return context._filters[f]
128
128
129 def gettemplate(exp, context):
129 def gettemplate(exp, context):
130 if exp[0] == 'string':
130 if exp[0] == 'string':
131 return compiletemplate(exp[1], context)
131 return compiletemplate(exp[1], context)
132 if exp[0] == 'symbol':
132 if exp[0] == 'symbol':
133 return context._load(exp[1])
133 return context._load(exp[1])
134 raise error.ParseError(_("expected template specifier"))
134 raise error.ParseError(_("expected template specifier"))
135
135
136 def runstring(context, mapping, data):
136 def runstring(context, mapping, data):
137 return data
137 return data
138
138
139 def runsymbol(context, mapping, key):
139 def runsymbol(context, mapping, key):
140 v = mapping.get(key)
140 v = mapping.get(key)
141 if v is None:
141 if v is None:
142 v = context._defaults.get(key)
142 v = context._defaults.get(key)
143 if v is None:
143 if v is None:
144 try:
144 try:
145 v = context.process(key, mapping)
145 v = context.process(key, mapping)
146 except TemplateNotFound:
146 except TemplateNotFound:
147 v = ''
147 v = ''
148 if util.safehasattr(v, '__call__'):
148 if util.safehasattr(v, '__call__'):
149 return v(**mapping)
149 return v(**mapping)
150 if isinstance(v, types.GeneratorType):
150 if isinstance(v, types.GeneratorType):
151 v = list(v)
151 v = list(v)
152 mapping[key] = v
152 mapping[key] = v
153 return v
153 return v
154 return v
154 return v
155
155
156 def buildfilter(exp, context):
156 def buildfilter(exp, context):
157 func, data = compileexp(exp[1], context)
157 func, data = compileexp(exp[1], context)
158 filt = getfilter(exp[2], context)
158 filt = getfilter(exp[2], context)
159 return (runfilter, (func, data, filt))
159 return (runfilter, (func, data, filt))
160
160
161 def runfilter(context, mapping, data):
161 def runfilter(context, mapping, data):
162 func, data, filt = data
162 func, data, filt = data
163 try:
163 try:
164 return filt(func(context, mapping, data))
164 return filt(func(context, mapping, data))
165 except (ValueError, AttributeError, TypeError):
165 except (ValueError, AttributeError, TypeError):
166 if isinstance(data, tuple):
166 if isinstance(data, tuple):
167 dt = data[1]
167 dt = data[1]
168 else:
168 else:
169 dt = data
169 dt = data
170 raise util.Abort(_("template filter '%s' is not compatible with "
170 raise util.Abort(_("template filter '%s' is not compatible with "
171 "keyword '%s'") % (filt.func_name, dt))
171 "keyword '%s'") % (filt.func_name, dt))
172
172
173 def buildmap(exp, context):
173 def buildmap(exp, context):
174 func, data = compileexp(exp[1], context)
174 func, data = compileexp(exp[1], context)
175 ctmpl = gettemplate(exp[2], context)
175 ctmpl = gettemplate(exp[2], context)
176 return (runmap, (func, data, ctmpl))
176 return (runmap, (func, data, ctmpl))
177
177
178 def runtemplate(context, mapping, template):
178 def runtemplate(context, mapping, template):
179 for func, data in template:
179 for func, data in template:
180 yield func(context, mapping, data)
180 yield func(context, mapping, data)
181
181
182 def runmap(context, mapping, data):
182 def runmap(context, mapping, data):
183 func, data, ctmpl = data
183 func, data, ctmpl = data
184 d = func(context, mapping, data)
184 d = func(context, mapping, data)
185 if util.safehasattr(d, '__call__'):
185 if util.safehasattr(d, '__call__'):
186 d = d()
186 d = d()
187
187
188 lm = mapping.copy()
188 lm = mapping.copy()
189
189
190 for i in d:
190 for i in d:
191 if isinstance(i, dict):
191 if isinstance(i, dict):
192 lm.update(i)
192 lm.update(i)
193 lm['originalnode'] = mapping.get('node')
193 lm['originalnode'] = mapping.get('node')
194 yield runtemplate(context, lm, ctmpl)
194 yield runtemplate(context, lm, ctmpl)
195 else:
195 else:
196 # v is not an iterable of dicts, this happen when 'key'
196 # v is not an iterable of dicts, this happen when 'key'
197 # has been fully expanded already and format is useless.
197 # has been fully expanded already and format is useless.
198 # If so, return the expanded value.
198 # If so, return the expanded value.
199 yield i
199 yield i
200
200
201 def buildfunc(exp, context):
201 def buildfunc(exp, context):
202 n = getsymbol(exp[1])
202 n = getsymbol(exp[1])
203 args = [compileexp(x, context) for x in getlist(exp[2])]
203 args = [compileexp(x, context) for x in getlist(exp[2])]
204 if n in funcs:
204 if n in funcs:
205 f = funcs[n]
205 f = funcs[n]
206 return (f, args)
206 return (f, args)
207 if n in context._filters:
207 if n in context._filters:
208 if len(args) != 1:
208 if len(args) != 1:
209 raise error.ParseError(_("filter %s expects one argument") % n)
209 raise error.ParseError(_("filter %s expects one argument") % n)
210 f = context._filters[n]
210 f = context._filters[n]
211 return (runfilter, (args[0][0], args[0][1], f))
211 return (runfilter, (args[0][0], args[0][1], f))
212
212
213 def date(context, mapping, args):
213 def date(context, mapping, args):
214 if not (1 <= len(args) <= 2):
214 if not (1 <= len(args) <= 2):
215 raise error.ParseError(_("date expects one or two arguments"))
215 raise error.ParseError(_("date expects one or two arguments"))
216
216
217 date = args[0][0](context, mapping, args[0][1])
217 date = args[0][0](context, mapping, args[0][1])
218 if len(args) == 2:
218 if len(args) == 2:
219 fmt = stringify(args[1][0](context, mapping, args[1][1]))
219 fmt = stringify(args[1][0](context, mapping, args[1][1]))
220 return util.datestr(date, fmt)
220 return util.datestr(date, fmt)
221 return util.datestr(date)
221 return util.datestr(date)
222
222
223 def fill(context, mapping, args):
223 def fill(context, mapping, args):
224 if not (1 <= len(args) <= 4):
224 if not (1 <= len(args) <= 4):
225 raise error.ParseError(_("fill expects one to four arguments"))
225 raise error.ParseError(_("fill expects one to four arguments"))
226
226
227 text = stringify(args[0][0](context, mapping, args[0][1]))
227 text = stringify(args[0][0](context, mapping, args[0][1]))
228 width = 76
228 width = 76
229 initindent = ''
229 initindent = ''
230 hangindent = ''
230 hangindent = ''
231 if 2 <= len(args) <= 4:
231 if 2 <= len(args) <= 4:
232 try:
232 try:
233 width = int(stringify(args[1][0](context, mapping, args[1][1])))
233 width = int(stringify(args[1][0](context, mapping, args[1][1])))
234 except ValueError:
234 except ValueError:
235 raise error.ParseError(_("fill expects an integer width"))
235 raise error.ParseError(_("fill expects an integer width"))
236 try:
236 try:
237 initindent = stringify(args[2][0](context, mapping, args[2][1]))
237 initindent = stringify(args[2][0](context, mapping, args[2][1]))
238 initindent = stringify(runtemplate(context, mapping,
238 initindent = stringify(runtemplate(context, mapping,
239 compiletemplate(initindent, context)))
239 compiletemplate(initindent, context)))
240 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
240 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
241 hangindent = stringify(runtemplate(context, mapping,
241 hangindent = stringify(runtemplate(context, mapping,
242 compiletemplate(hangindent, context)))
242 compiletemplate(hangindent, context)))
243 except IndexError:
243 except IndexError:
244 pass
244 pass
245
245
246 return templatefilters.fill(text, width, initindent, hangindent)
246 return templatefilters.fill(text, width, initindent, hangindent)
247
247
248 def get(context, mapping, args):
248 def get(context, mapping, args):
249 if len(args) != 2:
249 if len(args) != 2:
250 # i18n: "get" is a keyword
250 # i18n: "get" is a keyword
251 raise error.ParseError(_("get() expects two arguments"))
251 raise error.ParseError(_("get() expects two arguments"))
252
252
253 dictarg = args[0][0](context, mapping, args[0][1])
253 dictarg = args[0][0](context, mapping, args[0][1])
254 if not util.safehasattr(dictarg, 'get'):
254 if not util.safehasattr(dictarg, 'get'):
255 # i18n: "get" is a keyword
255 # i18n: "get" is a keyword
256 raise error.ParseError(_("get() expects a dict as first argument"))
256 raise error.ParseError(_("get() expects a dict as first argument"))
257
257
258 key = args[1][0](context, mapping, args[1][1])
258 key = args[1][0](context, mapping, args[1][1])
259 yield dictarg.get(key)
259 yield dictarg.get(key)
260
260
261 def _evalifliteral(arg, context, mapping):
261 def _evalifliteral(arg, context, mapping):
262 t = stringify(arg[0](context, mapping, arg[1]))
262 t = stringify(arg[0](context, mapping, arg[1]))
263 if arg[0] == runstring:
263 if arg[0] == runstring:
264 yield runtemplate(context, mapping, compiletemplate(t, context))
264 yield runtemplate(context, mapping, compiletemplate(t, context))
265 else:
265 else:
266 yield t
266 yield t
267
267
268 def if_(context, mapping, args):
268 def if_(context, mapping, args):
269 if not (2 <= len(args) <= 3):
269 if not (2 <= len(args) <= 3):
270 # i18n: "if" is a keyword
270 # i18n: "if" is a keyword
271 raise error.ParseError(_("if expects two or three arguments"))
271 raise error.ParseError(_("if expects two or three arguments"))
272
272
273 test = stringify(args[0][0](context, mapping, args[0][1]))
273 test = stringify(args[0][0](context, mapping, args[0][1]))
274 if test:
274 if test:
275 yield _evalifliteral(args[1], context, mapping)
275 yield _evalifliteral(args[1], context, mapping)
276 elif len(args) == 3:
276 elif len(args) == 3:
277 yield _evalifliteral(args[2], context, mapping)
277 yield _evalifliteral(args[2], context, mapping)
278
278
279 def ifeq(context, mapping, args):
279 def ifeq(context, mapping, args):
280 if not (3 <= len(args) <= 4):
280 if not (3 <= len(args) <= 4):
281 # i18n: "ifeq" is a keyword
281 # i18n: "ifeq" is a keyword
282 raise error.ParseError(_("ifeq expects three or four arguments"))
282 raise error.ParseError(_("ifeq expects three or four arguments"))
283
283
284 test = stringify(args[0][0](context, mapping, args[0][1]))
284 test = stringify(args[0][0](context, mapping, args[0][1]))
285 match = stringify(args[1][0](context, mapping, args[1][1]))
285 match = stringify(args[1][0](context, mapping, args[1][1]))
286 if test == match:
286 if test == match:
287 yield _evalifliteral(args[2], context, mapping)
287 yield _evalifliteral(args[2], context, mapping)
288 elif len(args) == 4:
288 elif len(args) == 4:
289 yield _evalifliteral(args[3], context, mapping)
289 yield _evalifliteral(args[3], context, mapping)
290
290
291 def join(context, mapping, args):
291 def join(context, mapping, args):
292 if not (1 <= len(args) <= 2):
292 if not (1 <= len(args) <= 2):
293 # i18n: "join" is a keyword
293 # i18n: "join" is a keyword
294 raise error.ParseError(_("join expects one or two arguments"))
294 raise error.ParseError(_("join expects one or two arguments"))
295
295
296 joinset = args[0][0](context, mapping, args[0][1])
296 joinset = args[0][0](context, mapping, args[0][1])
297 if util.safehasattr(joinset, '__call__'):
297 if util.safehasattr(joinset, '__call__'):
298 jf = joinset.joinfmt
298 jf = joinset.joinfmt
299 joinset = [jf(x) for x in joinset()]
299 joinset = [jf(x) for x in joinset()]
300
300
301 joiner = " "
301 joiner = " "
302 if len(args) > 1:
302 if len(args) > 1:
303 joiner = args[1][0](context, mapping, args[1][1])
303 joiner = args[1][0](context, mapping, args[1][1])
304
304
305 first = True
305 first = True
306 for x in joinset:
306 for x in joinset:
307 if first:
307 if first:
308 first = False
308 first = False
309 else:
309 else:
310 yield joiner
310 yield joiner
311 yield x
311 yield x
312
312
313 def label(context, mapping, args):
313 def label(context, mapping, args):
314 if len(args) != 2:
314 if len(args) != 2:
315 # i18n: "label" is a keyword
315 # i18n: "label" is a keyword
316 raise error.ParseError(_("label expects two arguments"))
316 raise error.ParseError(_("label expects two arguments"))
317
317
318 # ignore args[0] (the label string) since this is supposed to be a a no-op
318 # ignore args[0] (the label string) since this is supposed to be a a no-op
319 yield _evalifliteral(args[1], context, mapping)
319 yield _evalifliteral(args[1], context, mapping)
320
320
321 def rstdoc(context, mapping, args):
321 def rstdoc(context, mapping, args):
322 if len(args) != 2:
322 if len(args) != 2:
323 # i18n: "rstdoc" is a keyword
323 # i18n: "rstdoc" is a keyword
324 raise error.ParseError(_("rstdoc expects two arguments"))
324 raise error.ParseError(_("rstdoc expects two arguments"))
325
325
326 text = stringify(args[0][0](context, mapping, args[0][1]))
326 text = stringify(args[0][0](context, mapping, args[0][1]))
327 style = stringify(args[1][0](context, mapping, args[1][1]))
327 style = stringify(args[1][0](context, mapping, args[1][1]))
328
328
329 return minirst.format(text, style=style, keep=['verbose'])
329 return minirst.format(text, style=style, keep=['verbose'])
330
330
331 def strip(context, mapping, args):
331 def strip(context, mapping, args):
332 if not (1 <= len(args) <= 2):
332 if not (1 <= len(args) <= 2):
333 raise error.ParseError(_("strip expects one or two arguments"))
333 raise error.ParseError(_("strip expects one or two arguments"))
334
334
335 text = args[0][0](context, mapping, args[0][1])
335 text = args[0][0](context, mapping, args[0][1])
336 if len(args) == 2:
336 if len(args) == 2:
337 chars = args[1][0](context, mapping, args[1][1])
337 chars = args[1][0](context, mapping, args[1][1])
338 return text.strip(chars)
338 return text.strip(chars)
339 return text.strip()
339 return text.strip()
340
340
341 def sub(context, mapping, args):
341 def sub(context, mapping, args):
342 if len(args) != 3:
342 if len(args) != 3:
343 # i18n: "sub" is a keyword
343 # i18n: "sub" is a keyword
344 raise error.ParseError(_("sub expects three arguments"))
344 raise error.ParseError(_("sub expects three arguments"))
345
345
346 pat = stringify(args[0][0](context, mapping, args[0][1]))
346 pat = stringify(args[0][0](context, mapping, args[0][1]))
347 rpl = stringify(args[1][0](context, mapping, args[1][1]))
347 rpl = stringify(args[1][0](context, mapping, args[1][1]))
348 src = stringify(args[2][0](context, mapping, args[2][1]))
348 src = stringify(args[2][0](context, mapping, args[2][1]))
349 src = stringify(runtemplate(context, mapping,
349 src = stringify(runtemplate(context, mapping,
350 compiletemplate(src, context)))
350 compiletemplate(src, context)))
351 yield re.sub(pat, rpl, src)
351 yield re.sub(pat, rpl, src)
352
352
353 methods = {
353 methods = {
354 "string": lambda e, c: (runstring, e[1]),
354 "string": lambda e, c: (runstring, e[1]),
355 "symbol": lambda e, c: (runsymbol, e[1]),
355 "symbol": lambda e, c: (runsymbol, e[1]),
356 "group": lambda e, c: compileexp(e[1], c),
356 "group": lambda e, c: compileexp(e[1], c),
357 # ".": buildmember,
357 # ".": buildmember,
358 "|": buildfilter,
358 "|": buildfilter,
359 "%": buildmap,
359 "%": buildmap,
360 "func": buildfunc,
360 "func": buildfunc,
361 }
361 }
362
362
363 funcs = {
363 funcs = {
364 "date": date,
364 "date": date,
365 "fill": fill,
365 "fill": fill,
366 "get": get,
366 "get": get,
367 "if": if_,
367 "if": if_,
368 "ifeq": ifeq,
368 "ifeq": ifeq,
369 "join": join,
369 "join": join,
370 "label": label,
370 "label": label,
371 "rstdoc": rstdoc,
371 "rstdoc": rstdoc,
372 "strip": strip,
372 "strip": strip,
373 "sub": sub,
373 "sub": sub,
374 }
374 }
375
375
376 # template engine
376 # template engine
377
377
378 path = ['templates', '../templates']
378 path = ['templates', '../templates']
379 stringify = templatefilters.stringify
379 stringify = templatefilters.stringify
380
380
381 def _flatten(thing):
381 def _flatten(thing):
382 '''yield a single stream from a possibly nested set of iterators'''
382 '''yield a single stream from a possibly nested set of iterators'''
383 if isinstance(thing, str):
383 if isinstance(thing, str):
384 yield thing
384 yield thing
385 elif not util.safehasattr(thing, '__iter__'):
385 elif not util.safehasattr(thing, '__iter__'):
386 if thing is not None:
386 if thing is not None:
387 yield str(thing)
387 yield str(thing)
388 else:
388 else:
389 for i in thing:
389 for i in thing:
390 if isinstance(i, str):
390 if isinstance(i, str):
391 yield i
391 yield i
392 elif not util.safehasattr(i, '__iter__'):
392 elif not util.safehasattr(i, '__iter__'):
393 if i is not None:
393 if i is not None:
394 yield str(i)
394 yield str(i)
395 elif i is not None:
395 elif i is not None:
396 for j in _flatten(i):
396 for j in _flatten(i):
397 yield j
397 yield j
398
398
399 def parsestring(s, quoted=True):
399 def parsestring(s, quoted=True):
400 '''parse a string using simple c-like syntax.
400 '''parse a string using simple c-like syntax.
401 string must be in quotes if quoted is True.'''
401 string must be in quotes if quoted is True.'''
402 if quoted:
402 if quoted:
403 if len(s) < 2 or s[0] != s[-1]:
403 if len(s) < 2 or s[0] != s[-1]:
404 raise SyntaxError(_('unmatched quotes'))
404 raise SyntaxError(_('unmatched quotes'))
405 return s[1:-1].decode('string_escape')
405 return s[1:-1].decode('string_escape')
406
406
407 return s.decode('string_escape')
407 return s.decode('string_escape')
408
408
409 class engine(object):
409 class engine(object):
410 '''template expansion engine.
410 '''template expansion engine.
411
411
412 template expansion works like this. a map file contains key=value
412 template expansion works like this. a map file contains key=value
413 pairs. if value is quoted, it is treated as string. otherwise, it
413 pairs. if value is quoted, it is treated as string. otherwise, it
414 is treated as name of template file.
414 is treated as name of template file.
415
415
416 templater is asked to expand a key in map. it looks up key, and
416 templater is asked to expand a key in map. it looks up key, and
417 looks for strings like this: {foo}. it expands {foo} by looking up
417 looks for strings like this: {foo}. it expands {foo} by looking up
418 foo in map, and substituting it. expansion is recursive: it stops
418 foo in map, and substituting it. expansion is recursive: it stops
419 when there is no more {foo} to replace.
419 when there is no more {foo} to replace.
420
420
421 expansion also allows formatting and filtering.
421 expansion also allows formatting and filtering.
422
422
423 format uses key to expand each item in list. syntax is
423 format uses key to expand each item in list. syntax is
424 {key%format}.
424 {key%format}.
425
425
426 filter uses function to transform value. syntax is
426 filter uses function to transform value. syntax is
427 {key|filter1|filter2|...}.'''
427 {key|filter1|filter2|...}.'''
428
428
429 def __init__(self, loader, filters={}, defaults={}):
429 def __init__(self, loader, filters={}, defaults={}):
430 self._loader = loader
430 self._loader = loader
431 self._filters = filters
431 self._filters = filters
432 self._defaults = defaults
432 self._defaults = defaults
433 self._cache = {}
433 self._cache = {}
434
434
435 def _load(self, t):
435 def _load(self, t):
436 '''load, parse, and cache a template'''
436 '''load, parse, and cache a template'''
437 if t not in self._cache:
437 if t not in self._cache:
438 self._cache[t] = compiletemplate(self._loader(t), self)
438 self._cache[t] = compiletemplate(self._loader(t), self)
439 return self._cache[t]
439 return self._cache[t]
440
440
441 def process(self, t, mapping):
441 def process(self, t, mapping):
442 '''Perform expansion. t is name of map element to expand.
442 '''Perform expansion. t is name of map element to expand.
443 mapping contains added elements for use during expansion. Is a
443 mapping contains added elements for use during expansion. Is a
444 generator.'''
444 generator.'''
445 return _flatten(runtemplate(self, mapping, self._load(t)))
445 return _flatten(runtemplate(self, mapping, self._load(t)))
446
446
447 engines = {'default': engine}
447 engines = {'default': engine}
448
448
449 def stylelist():
449 def stylelist():
450 path = templatepath()[0]
450 paths = templatepath()
451 dirlist = os.listdir(path)
451 if not paths:
452 return _('no templates found, try `hg debuginstall` for more info')
453 dirlist = os.listdir(paths[0])
452 stylelist = []
454 stylelist = []
453 for file in dirlist:
455 for file in dirlist:
454 split = file.split(".")
456 split = file.split(".")
455 if split[0] == "map-cmdline":
457 if split[0] == "map-cmdline":
456 stylelist.append(split[1])
458 stylelist.append(split[1])
457 return ", ".join(sorted(stylelist))
459 return ", ".join(sorted(stylelist))
458
460
459 class TemplateNotFound(util.Abort):
461 class TemplateNotFound(util.Abort):
460 pass
462 pass
461
463
462 class templater(object):
464 class templater(object):
463
465
464 def __init__(self, mapfile, filters={}, defaults={}, cache={},
466 def __init__(self, mapfile, filters={}, defaults={}, cache={},
465 minchunk=1024, maxchunk=65536):
467 minchunk=1024, maxchunk=65536):
466 '''set up template engine.
468 '''set up template engine.
467 mapfile is name of file to read map definitions from.
469 mapfile is name of file to read map definitions from.
468 filters is dict of functions. each transforms a value into another.
470 filters is dict of functions. each transforms a value into another.
469 defaults is dict of default map definitions.'''
471 defaults is dict of default map definitions.'''
470 self.mapfile = mapfile or 'template'
472 self.mapfile = mapfile or 'template'
471 self.cache = cache.copy()
473 self.cache = cache.copy()
472 self.map = {}
474 self.map = {}
473 self.base = (mapfile and os.path.dirname(mapfile)) or ''
475 self.base = (mapfile and os.path.dirname(mapfile)) or ''
474 self.filters = templatefilters.filters.copy()
476 self.filters = templatefilters.filters.copy()
475 self.filters.update(filters)
477 self.filters.update(filters)
476 self.defaults = defaults
478 self.defaults = defaults
477 self.minchunk, self.maxchunk = minchunk, maxchunk
479 self.minchunk, self.maxchunk = minchunk, maxchunk
478 self.ecache = {}
480 self.ecache = {}
479
481
480 if not mapfile:
482 if not mapfile:
481 return
483 return
482 if not os.path.exists(mapfile):
484 if not os.path.exists(mapfile):
483 raise util.Abort(_("style '%s' not found") % mapfile,
485 raise util.Abort(_("style '%s' not found") % mapfile,
484 hint=_("available styles: %s") % stylelist())
486 hint=_("available styles: %s") % stylelist())
485
487
486 conf = config.config()
488 conf = config.config()
487 conf.read(mapfile)
489 conf.read(mapfile)
488
490
489 for key, val in conf[''].items():
491 for key, val in conf[''].items():
490 if not val:
492 if not val:
491 raise SyntaxError(_('%s: missing value') % conf.source('', key))
493 raise SyntaxError(_('%s: missing value') % conf.source('', key))
492 if val[0] in "'\"":
494 if val[0] in "'\"":
493 try:
495 try:
494 self.cache[key] = parsestring(val)
496 self.cache[key] = parsestring(val)
495 except SyntaxError, inst:
497 except SyntaxError, inst:
496 raise SyntaxError('%s: %s' %
498 raise SyntaxError('%s: %s' %
497 (conf.source('', key), inst.args[0]))
499 (conf.source('', key), inst.args[0]))
498 else:
500 else:
499 val = 'default', val
501 val = 'default', val
500 if ':' in val[1]:
502 if ':' in val[1]:
501 val = val[1].split(':', 1)
503 val = val[1].split(':', 1)
502 self.map[key] = val[0], os.path.join(self.base, val[1])
504 self.map[key] = val[0], os.path.join(self.base, val[1])
503
505
504 def __contains__(self, key):
506 def __contains__(self, key):
505 return key in self.cache or key in self.map
507 return key in self.cache or key in self.map
506
508
507 def load(self, t):
509 def load(self, t):
508 '''Get the template for the given template name. Use a local cache.'''
510 '''Get the template for the given template name. Use a local cache.'''
509 if t not in self.cache:
511 if t not in self.cache:
510 try:
512 try:
511 self.cache[t] = util.readfile(self.map[t][1])
513 self.cache[t] = util.readfile(self.map[t][1])
512 except KeyError, inst:
514 except KeyError, inst:
513 raise TemplateNotFound(_('"%s" not in template map') %
515 raise TemplateNotFound(_('"%s" not in template map') %
514 inst.args[0])
516 inst.args[0])
515 except IOError, inst:
517 except IOError, inst:
516 raise IOError(inst.args[0], _('template file %s: %s') %
518 raise IOError(inst.args[0], _('template file %s: %s') %
517 (self.map[t][1], inst.args[1]))
519 (self.map[t][1], inst.args[1]))
518 return self.cache[t]
520 return self.cache[t]
519
521
520 def __call__(self, t, **mapping):
522 def __call__(self, t, **mapping):
521 ttype = t in self.map and self.map[t][0] or 'default'
523 ttype = t in self.map and self.map[t][0] or 'default'
522 if ttype not in self.ecache:
524 if ttype not in self.ecache:
523 self.ecache[ttype] = engines[ttype](self.load,
525 self.ecache[ttype] = engines[ttype](self.load,
524 self.filters, self.defaults)
526 self.filters, self.defaults)
525 proc = self.ecache[ttype]
527 proc = self.ecache[ttype]
526
528
527 stream = proc.process(t, mapping)
529 stream = proc.process(t, mapping)
528 if self.minchunk:
530 if self.minchunk:
529 stream = util.increasingchunks(stream, min=self.minchunk,
531 stream = util.increasingchunks(stream, min=self.minchunk,
530 max=self.maxchunk)
532 max=self.maxchunk)
531 return stream
533 return stream
532
534
533 def templatepath(name=None):
535 def templatepath(name=None):
534 '''return location of template file or directory (if no name).
536 '''return location of template file or directory (if no name).
535 returns None if not found.'''
537 returns None if not found.'''
536 normpaths = []
538 normpaths = []
537
539
538 # executable version (py2exe) doesn't support __file__
540 # executable version (py2exe) doesn't support __file__
539 if util.mainfrozen():
541 if util.mainfrozen():
540 module = sys.executable
542 module = sys.executable
541 else:
543 else:
542 module = __file__
544 module = __file__
543 for f in path:
545 for f in path:
544 if f.startswith('/'):
546 if f.startswith('/'):
545 p = f
547 p = f
546 else:
548 else:
547 fl = f.split('/')
549 fl = f.split('/')
548 p = os.path.join(os.path.dirname(module), *fl)
550 p = os.path.join(os.path.dirname(module), *fl)
549 if name:
551 if name:
550 p = os.path.join(p, name)
552 p = os.path.join(p, name)
551 if name and os.path.exists(p):
553 if name and os.path.exists(p):
552 return os.path.normpath(p)
554 return os.path.normpath(p)
553 elif os.path.isdir(p):
555 elif os.path.isdir(p):
554 normpaths.append(os.path.normpath(p))
556 normpaths.append(os.path.normpath(p))
555
557
556 return normpaths
558 return normpaths
557
559
558 def stylemap(styles, paths=None):
560 def stylemap(styles, paths=None):
559 """Return path to mapfile for a given style.
561 """Return path to mapfile for a given style.
560
562
561 Searches mapfile in the following locations:
563 Searches mapfile in the following locations:
562 1. templatepath/style/map
564 1. templatepath/style/map
563 2. templatepath/map-style
565 2. templatepath/map-style
564 3. templatepath/map
566 3. templatepath/map
565 """
567 """
566
568
567 if paths is None:
569 if paths is None:
568 paths = templatepath()
570 paths = templatepath()
569 elif isinstance(paths, str):
571 elif isinstance(paths, str):
570 paths = [paths]
572 paths = [paths]
571
573
572 if isinstance(styles, str):
574 if isinstance(styles, str):
573 styles = [styles]
575 styles = [styles]
574
576
575 for style in styles:
577 for style in styles:
576 if not style:
578 if not style:
577 continue
579 continue
578 locations = [os.path.join(style, 'map'), 'map-' + style]
580 locations = [os.path.join(style, 'map'), 'map-' + style]
579 locations.append('map')
581 locations.append('map')
580
582
581 for path in paths:
583 for path in paths:
582 for location in locations:
584 for location in locations:
583 mapfile = os.path.join(path, location)
585 mapfile = os.path.join(path, location)
584 if os.path.isfile(mapfile):
586 if os.path.isfile(mapfile):
585 return style, mapfile
587 return style, mapfile
586
588
587 raise RuntimeError("No hgweb templates found in %r" % paths)
589 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now