##// END OF EJS Templates
templater: comment that gettemplate() has different name resolution order...
Yuya Nishihara -
r25599:695b93a7 default
parent child Browse files
Show More
@@ -1,861 +1,864
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 os, re
9 import os, re
10 import util, config, templatefilters, templatekw, parser, error
10 import util, config, templatefilters, templatekw, parser, error
11 import revset as revsetmod
11 import revset as revsetmod
12 import types
12 import types
13 import minirst
13 import minirst
14
14
15 # template parsing
15 # template parsing
16
16
17 elements = {
17 elements = {
18 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
18 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
19 ",": (2, None, ("list", 2)),
19 ",": (2, None, ("list", 2)),
20 "|": (5, None, ("|", 5)),
20 "|": (5, None, ("|", 5)),
21 "%": (6, None, ("%", 6)),
21 "%": (6, None, ("%", 6)),
22 ")": (0, None, None),
22 ")": (0, None, None),
23 "integer": (0, ("integer",), None),
23 "integer": (0, ("integer",), None),
24 "symbol": (0, ("symbol",), None),
24 "symbol": (0, ("symbol",), None),
25 "string": (0, ("template",), None),
25 "string": (0, ("template",), None),
26 "rawstring": (0, ("rawstring",), None),
26 "rawstring": (0, ("rawstring",), None),
27 "end": (0, None, None),
27 "end": (0, None, None),
28 }
28 }
29
29
30 def tokenizer(data):
30 def tokenizer(data):
31 program, start, end = data
31 program, start, end = data
32 pos = start
32 pos = start
33 while pos < end:
33 while pos < end:
34 c = program[pos]
34 c = program[pos]
35 if c.isspace(): # skip inter-token whitespace
35 if c.isspace(): # skip inter-token whitespace
36 pass
36 pass
37 elif c in "(,)%|": # handle simple operators
37 elif c in "(,)%|": # handle simple operators
38 yield (c, None, pos)
38 yield (c, None, pos)
39 elif (c in '"\'' or c == 'r' and
39 elif (c in '"\'' or c == 'r' and
40 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
40 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
41 if c == 'r':
41 if c == 'r':
42 pos += 1
42 pos += 1
43 c = program[pos]
43 c = program[pos]
44 decode = False
44 decode = False
45 else:
45 else:
46 decode = True
46 decode = True
47 pos += 1
47 pos += 1
48 s = pos
48 s = pos
49 while pos < end: # find closing quote
49 while pos < end: # find closing quote
50 d = program[pos]
50 d = program[pos]
51 if decode and d == '\\': # skip over escaped characters
51 if decode and d == '\\': # skip over escaped characters
52 pos += 2
52 pos += 2
53 continue
53 continue
54 if d == c:
54 if d == c:
55 if not decode:
55 if not decode:
56 yield ('rawstring', program[s:pos], s)
56 yield ('rawstring', program[s:pos], s)
57 break
57 break
58 yield ('string', program[s:pos], s)
58 yield ('string', program[s:pos], s)
59 break
59 break
60 pos += 1
60 pos += 1
61 else:
61 else:
62 raise error.ParseError(_("unterminated string"), s)
62 raise error.ParseError(_("unterminated string"), s)
63 elif c.isdigit() or c == '-':
63 elif c.isdigit() or c == '-':
64 s = pos
64 s = pos
65 if c == '-': # simply take negate operator as part of integer
65 if c == '-': # simply take negate operator as part of integer
66 pos += 1
66 pos += 1
67 if pos >= end or not program[pos].isdigit():
67 if pos >= end or not program[pos].isdigit():
68 raise error.ParseError(_("integer literal without digits"), s)
68 raise error.ParseError(_("integer literal without digits"), s)
69 pos += 1
69 pos += 1
70 while pos < end:
70 while pos < end:
71 d = program[pos]
71 d = program[pos]
72 if not d.isdigit():
72 if not d.isdigit():
73 break
73 break
74 pos += 1
74 pos += 1
75 yield ('integer', program[s:pos], s)
75 yield ('integer', program[s:pos], s)
76 pos -= 1
76 pos -= 1
77 elif c.isalnum() or c in '_':
77 elif c.isalnum() or c in '_':
78 s = pos
78 s = pos
79 pos += 1
79 pos += 1
80 while pos < end: # find end of symbol
80 while pos < end: # find end of symbol
81 d = program[pos]
81 d = program[pos]
82 if not (d.isalnum() or d == "_"):
82 if not (d.isalnum() or d == "_"):
83 break
83 break
84 pos += 1
84 pos += 1
85 sym = program[s:pos]
85 sym = program[s:pos]
86 yield ('symbol', sym, s)
86 yield ('symbol', sym, s)
87 pos -= 1
87 pos -= 1
88 elif c == '}':
88 elif c == '}':
89 pos += 1
89 pos += 1
90 break
90 break
91 else:
91 else:
92 raise error.ParseError(_("syntax error"), pos)
92 raise error.ParseError(_("syntax error"), pos)
93 pos += 1
93 pos += 1
94 yield ('end', None, pos)
94 yield ('end', None, pos)
95
95
96 def compiletemplate(tmpl, context):
96 def compiletemplate(tmpl, context):
97 parsed = []
97 parsed = []
98 pos, stop = 0, len(tmpl)
98 pos, stop = 0, len(tmpl)
99 p = parser.parser(tokenizer, elements)
99 p = parser.parser(tokenizer, elements)
100 while pos < stop:
100 while pos < stop:
101 n = tmpl.find('{', pos)
101 n = tmpl.find('{', pos)
102 if n < 0:
102 if n < 0:
103 parsed.append(('string', tmpl[pos:]))
103 parsed.append(('string', tmpl[pos:]))
104 break
104 break
105 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
105 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
106 if bs % 2 == 1:
106 if bs % 2 == 1:
107 # escaped (e.g. '\{', '\\\{', but not '\\{')
107 # escaped (e.g. '\{', '\\\{', but not '\\{')
108 parsed.append(('string', (tmpl[pos:n - 1] + "{")))
108 parsed.append(('string', (tmpl[pos:n - 1] + "{")))
109 pos = n + 1
109 pos = n + 1
110 continue
110 continue
111 if n > pos:
111 if n > pos:
112 parsed.append(('string', tmpl[pos:n]))
112 parsed.append(('string', tmpl[pos:n]))
113
113
114 pd = [tmpl, n + 1, stop]
114 pd = [tmpl, n + 1, stop]
115 parseres, pos = p.parse(pd)
115 parseres, pos = p.parse(pd)
116 parsed.append(parseres)
116 parsed.append(parseres)
117
117
118 return [compileexp(e, context, methods) for e in parsed]
118 return [compileexp(e, context, methods) for e in parsed]
119
119
120 def compileexp(exp, context, curmethods):
120 def compileexp(exp, context, curmethods):
121 t = exp[0]
121 t = exp[0]
122 if t in curmethods:
122 if t in curmethods:
123 return curmethods[t](exp, context)
123 return curmethods[t](exp, context)
124 raise error.ParseError(_("unknown method '%s'") % t)
124 raise error.ParseError(_("unknown method '%s'") % t)
125
125
126 # template evaluation
126 # template evaluation
127
127
128 def getsymbol(exp):
128 def getsymbol(exp):
129 if exp[0] == 'symbol':
129 if exp[0] == 'symbol':
130 return exp[1]
130 return exp[1]
131 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
131 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
132
132
133 def getlist(x):
133 def getlist(x):
134 if not x:
134 if not x:
135 return []
135 return []
136 if x[0] == 'list':
136 if x[0] == 'list':
137 return getlist(x[1]) + [x[2]]
137 return getlist(x[1]) + [x[2]]
138 return [x]
138 return [x]
139
139
140 def getfilter(exp, context):
140 def getfilter(exp, context):
141 f = getsymbol(exp)
141 f = getsymbol(exp)
142 if f not in context._filters:
142 if f not in context._filters:
143 raise error.ParseError(_("unknown function '%s'") % f)
143 raise error.ParseError(_("unknown function '%s'") % f)
144 return context._filters[f]
144 return context._filters[f]
145
145
146 def gettemplate(exp, context):
146 def gettemplate(exp, context):
147 if exp[0] == 'template':
147 if exp[0] == 'template':
148 return compiletemplate(exp[1], context)
148 return compiletemplate(exp[1], context)
149 if exp[0] == 'symbol':
149 if exp[0] == 'symbol':
150 # unlike runsymbol(), here 'symbol' is always taken as template name
151 # even if it exists in mapping. this allows us to override mapping
152 # by web templates, e.g. 'changelogtag' is redefined in map file.
150 return context._load(exp[1])
153 return context._load(exp[1])
151 raise error.ParseError(_("expected template specifier"))
154 raise error.ParseError(_("expected template specifier"))
152
155
153 def runinteger(context, mapping, data):
156 def runinteger(context, mapping, data):
154 return int(data)
157 return int(data)
155
158
156 def runstring(context, mapping, data):
159 def runstring(context, mapping, data):
157 return data.decode("string-escape")
160 return data.decode("string-escape")
158
161
159 def runrawstring(context, mapping, data):
162 def runrawstring(context, mapping, data):
160 return data
163 return data
161
164
162 def runsymbol(context, mapping, key):
165 def runsymbol(context, mapping, key):
163 v = mapping.get(key)
166 v = mapping.get(key)
164 if v is None:
167 if v is None:
165 v = context._defaults.get(key)
168 v = context._defaults.get(key)
166 if v is None:
169 if v is None:
167 try:
170 try:
168 v = context.process(key, mapping)
171 v = context.process(key, mapping)
169 except TemplateNotFound:
172 except TemplateNotFound:
170 v = ''
173 v = ''
171 if callable(v):
174 if callable(v):
172 return v(**mapping)
175 return v(**mapping)
173 if isinstance(v, types.GeneratorType):
176 if isinstance(v, types.GeneratorType):
174 v = list(v)
177 v = list(v)
175 return v
178 return v
176
179
177 def buildtemplate(exp, context):
180 def buildtemplate(exp, context):
178 ctmpl = compiletemplate(exp[1], context)
181 ctmpl = compiletemplate(exp[1], context)
179 if len(ctmpl) == 1:
182 if len(ctmpl) == 1:
180 return ctmpl[0] # fast path for string with no template fragment
183 return ctmpl[0] # fast path for string with no template fragment
181 return (runtemplate, ctmpl)
184 return (runtemplate, ctmpl)
182
185
183 def runtemplate(context, mapping, template):
186 def runtemplate(context, mapping, template):
184 for func, data in template:
187 for func, data in template:
185 yield func(context, mapping, data)
188 yield func(context, mapping, data)
186
189
187 def buildfilter(exp, context):
190 def buildfilter(exp, context):
188 func, data = compileexp(exp[1], context, methods)
191 func, data = compileexp(exp[1], context, methods)
189 filt = getfilter(exp[2], context)
192 filt = getfilter(exp[2], context)
190 return (runfilter, (func, data, filt))
193 return (runfilter, (func, data, filt))
191
194
192 def runfilter(context, mapping, data):
195 def runfilter(context, mapping, data):
193 func, data, filt = data
196 func, data, filt = data
194 # func() may return string, generator of strings or arbitrary object such
197 # func() may return string, generator of strings or arbitrary object such
195 # as date tuple, but filter does not want generator.
198 # as date tuple, but filter does not want generator.
196 thing = func(context, mapping, data)
199 thing = func(context, mapping, data)
197 if isinstance(thing, types.GeneratorType):
200 if isinstance(thing, types.GeneratorType):
198 thing = stringify(thing)
201 thing = stringify(thing)
199 try:
202 try:
200 return filt(thing)
203 return filt(thing)
201 except (ValueError, AttributeError, TypeError):
204 except (ValueError, AttributeError, TypeError):
202 if isinstance(data, tuple):
205 if isinstance(data, tuple):
203 dt = data[1]
206 dt = data[1]
204 else:
207 else:
205 dt = data
208 dt = data
206 raise util.Abort(_("template filter '%s' is not compatible with "
209 raise util.Abort(_("template filter '%s' is not compatible with "
207 "keyword '%s'") % (filt.func_name, dt))
210 "keyword '%s'") % (filt.func_name, dt))
208
211
209 def buildmap(exp, context):
212 def buildmap(exp, context):
210 func, data = compileexp(exp[1], context, methods)
213 func, data = compileexp(exp[1], context, methods)
211 ctmpl = gettemplate(exp[2], context)
214 ctmpl = gettemplate(exp[2], context)
212 return (runmap, (func, data, ctmpl))
215 return (runmap, (func, data, ctmpl))
213
216
214 def runmap(context, mapping, data):
217 def runmap(context, mapping, data):
215 func, data, ctmpl = data
218 func, data, ctmpl = data
216 d = func(context, mapping, data)
219 d = func(context, mapping, data)
217 if callable(d):
220 if callable(d):
218 d = d()
221 d = d()
219
222
220 lm = mapping.copy()
223 lm = mapping.copy()
221
224
222 for i in d:
225 for i in d:
223 if isinstance(i, dict):
226 if isinstance(i, dict):
224 lm.update(i)
227 lm.update(i)
225 lm['originalnode'] = mapping.get('node')
228 lm['originalnode'] = mapping.get('node')
226 yield runtemplate(context, lm, ctmpl)
229 yield runtemplate(context, lm, ctmpl)
227 else:
230 else:
228 # v is not an iterable of dicts, this happen when 'key'
231 # v is not an iterable of dicts, this happen when 'key'
229 # has been fully expanded already and format is useless.
232 # has been fully expanded already and format is useless.
230 # If so, return the expanded value.
233 # If so, return the expanded value.
231 yield i
234 yield i
232
235
233 def buildfunc(exp, context):
236 def buildfunc(exp, context):
234 n = getsymbol(exp[1])
237 n = getsymbol(exp[1])
235 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
238 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
236 if n in funcs:
239 if n in funcs:
237 f = funcs[n]
240 f = funcs[n]
238 return (f, args)
241 return (f, args)
239 if n in context._filters:
242 if n in context._filters:
240 if len(args) != 1:
243 if len(args) != 1:
241 raise error.ParseError(_("filter %s expects one argument") % n)
244 raise error.ParseError(_("filter %s expects one argument") % n)
242 f = context._filters[n]
245 f = context._filters[n]
243 return (runfilter, (args[0][0], args[0][1], f))
246 return (runfilter, (args[0][0], args[0][1], f))
244 raise error.ParseError(_("unknown function '%s'") % n)
247 raise error.ParseError(_("unknown function '%s'") % n)
245
248
246 def date(context, mapping, args):
249 def date(context, mapping, args):
247 """:date(date[, fmt]): Format a date. See :hg:`help dates` for formatting
250 """:date(date[, fmt]): Format a date. See :hg:`help dates` for formatting
248 strings."""
251 strings."""
249 if not (1 <= len(args) <= 2):
252 if not (1 <= len(args) <= 2):
250 # i18n: "date" is a keyword
253 # i18n: "date" is a keyword
251 raise error.ParseError(_("date expects one or two arguments"))
254 raise error.ParseError(_("date expects one or two arguments"))
252
255
253 date = args[0][0](context, mapping, args[0][1])
256 date = args[0][0](context, mapping, args[0][1])
254 fmt = None
257 fmt = None
255 if len(args) == 2:
258 if len(args) == 2:
256 fmt = stringify(args[1][0](context, mapping, args[1][1]))
259 fmt = stringify(args[1][0](context, mapping, args[1][1]))
257 try:
260 try:
258 if fmt is None:
261 if fmt is None:
259 return util.datestr(date)
262 return util.datestr(date)
260 else:
263 else:
261 return util.datestr(date, fmt)
264 return util.datestr(date, fmt)
262 except (TypeError, ValueError):
265 except (TypeError, ValueError):
263 # i18n: "date" is a keyword
266 # i18n: "date" is a keyword
264 raise error.ParseError(_("date expects a date information"))
267 raise error.ParseError(_("date expects a date information"))
265
268
266 def diff(context, mapping, args):
269 def diff(context, mapping, args):
267 """:diff([includepattern [, excludepattern]]): Show a diff, optionally
270 """:diff([includepattern [, excludepattern]]): Show a diff, optionally
268 specifying files to include or exclude."""
271 specifying files to include or exclude."""
269 if len(args) > 2:
272 if len(args) > 2:
270 # i18n: "diff" is a keyword
273 # i18n: "diff" is a keyword
271 raise error.ParseError(_("diff expects one, two or no arguments"))
274 raise error.ParseError(_("diff expects one, two or no arguments"))
272
275
273 def getpatterns(i):
276 def getpatterns(i):
274 if i < len(args):
277 if i < len(args):
275 s = stringify(args[i][0](context, mapping, args[i][1])).strip()
278 s = stringify(args[i][0](context, mapping, args[i][1])).strip()
276 if s:
279 if s:
277 return [s]
280 return [s]
278 return []
281 return []
279
282
280 ctx = mapping['ctx']
283 ctx = mapping['ctx']
281 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
284 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
282
285
283 return ''.join(chunks)
286 return ''.join(chunks)
284
287
285 def fill(context, mapping, args):
288 def fill(context, mapping, args):
286 """:fill(text[, width[, initialident[, hangindent]]]): Fill many
289 """:fill(text[, width[, initialident[, hangindent]]]): Fill many
287 paragraphs with optional indentation. See the "fill" filter."""
290 paragraphs with optional indentation. See the "fill" filter."""
288 if not (1 <= len(args) <= 4):
291 if not (1 <= len(args) <= 4):
289 # i18n: "fill" is a keyword
292 # i18n: "fill" is a keyword
290 raise error.ParseError(_("fill expects one to four arguments"))
293 raise error.ParseError(_("fill expects one to four arguments"))
291
294
292 text = stringify(args[0][0](context, mapping, args[0][1]))
295 text = stringify(args[0][0](context, mapping, args[0][1]))
293 width = 76
296 width = 76
294 initindent = ''
297 initindent = ''
295 hangindent = ''
298 hangindent = ''
296 if 2 <= len(args) <= 4:
299 if 2 <= len(args) <= 4:
297 try:
300 try:
298 width = int(stringify(args[1][0](context, mapping, args[1][1])))
301 width = int(stringify(args[1][0](context, mapping, args[1][1])))
299 except ValueError:
302 except ValueError:
300 # i18n: "fill" is a keyword
303 # i18n: "fill" is a keyword
301 raise error.ParseError(_("fill expects an integer width"))
304 raise error.ParseError(_("fill expects an integer width"))
302 try:
305 try:
303 initindent = stringify(args[2][0](context, mapping, args[2][1]))
306 initindent = stringify(args[2][0](context, mapping, args[2][1]))
304 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
307 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
305 except IndexError:
308 except IndexError:
306 pass
309 pass
307
310
308 return templatefilters.fill(text, width, initindent, hangindent)
311 return templatefilters.fill(text, width, initindent, hangindent)
309
312
310 def pad(context, mapping, args):
313 def pad(context, mapping, args):
311 """:pad(text, width[, fillchar=' '[, right=False]]): Pad text with a
314 """:pad(text, width[, fillchar=' '[, right=False]]): Pad text with a
312 fill character."""
315 fill character."""
313 if not (2 <= len(args) <= 4):
316 if not (2 <= len(args) <= 4):
314 # i18n: "pad" is a keyword
317 # i18n: "pad" is a keyword
315 raise error.ParseError(_("pad() expects two to four arguments"))
318 raise error.ParseError(_("pad() expects two to four arguments"))
316
319
317 width = int(args[1][1])
320 width = int(args[1][1])
318
321
319 text = stringify(args[0][0](context, mapping, args[0][1]))
322 text = stringify(args[0][0](context, mapping, args[0][1]))
320
323
321 right = False
324 right = False
322 fillchar = ' '
325 fillchar = ' '
323 if len(args) > 2:
326 if len(args) > 2:
324 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
327 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
325 if len(args) > 3:
328 if len(args) > 3:
326 right = util.parsebool(args[3][1])
329 right = util.parsebool(args[3][1])
327
330
328 if right:
331 if right:
329 return text.rjust(width, fillchar)
332 return text.rjust(width, fillchar)
330 else:
333 else:
331 return text.ljust(width, fillchar)
334 return text.ljust(width, fillchar)
332
335
333 def indent(context, mapping, args):
336 def indent(context, mapping, args):
334 """:indent(text, indentchars[, firstline]): Indents all non-empty lines
337 """:indent(text, indentchars[, firstline]): Indents all non-empty lines
335 with the characters given in the indentchars string. An optional
338 with the characters given in the indentchars string. An optional
336 third parameter will override the indent for the first line only
339 third parameter will override the indent for the first line only
337 if present."""
340 if present."""
338 if not (2 <= len(args) <= 3):
341 if not (2 <= len(args) <= 3):
339 # i18n: "indent" is a keyword
342 # i18n: "indent" is a keyword
340 raise error.ParseError(_("indent() expects two or three arguments"))
343 raise error.ParseError(_("indent() expects two or three arguments"))
341
344
342 text = stringify(args[0][0](context, mapping, args[0][1]))
345 text = stringify(args[0][0](context, mapping, args[0][1]))
343 indent = stringify(args[1][0](context, mapping, args[1][1]))
346 indent = stringify(args[1][0](context, mapping, args[1][1]))
344
347
345 if len(args) == 3:
348 if len(args) == 3:
346 firstline = stringify(args[2][0](context, mapping, args[2][1]))
349 firstline = stringify(args[2][0](context, mapping, args[2][1]))
347 else:
350 else:
348 firstline = indent
351 firstline = indent
349
352
350 # the indent function doesn't indent the first line, so we do it here
353 # the indent function doesn't indent the first line, so we do it here
351 return templatefilters.indent(firstline + text, indent)
354 return templatefilters.indent(firstline + text, indent)
352
355
353 def get(context, mapping, args):
356 def get(context, mapping, args):
354 """:get(dict, key): Get an attribute/key from an object. Some keywords
357 """:get(dict, key): Get an attribute/key from an object. Some keywords
355 are complex types. This function allows you to obtain the value of an
358 are complex types. This function allows you to obtain the value of an
356 attribute on these type."""
359 attribute on these type."""
357 if len(args) != 2:
360 if len(args) != 2:
358 # i18n: "get" is a keyword
361 # i18n: "get" is a keyword
359 raise error.ParseError(_("get() expects two arguments"))
362 raise error.ParseError(_("get() expects two arguments"))
360
363
361 dictarg = args[0][0](context, mapping, args[0][1])
364 dictarg = args[0][0](context, mapping, args[0][1])
362 if not util.safehasattr(dictarg, 'get'):
365 if not util.safehasattr(dictarg, 'get'):
363 # i18n: "get" is a keyword
366 # i18n: "get" is a keyword
364 raise error.ParseError(_("get() expects a dict as first argument"))
367 raise error.ParseError(_("get() expects a dict as first argument"))
365
368
366 key = args[1][0](context, mapping, args[1][1])
369 key = args[1][0](context, mapping, args[1][1])
367 yield dictarg.get(key)
370 yield dictarg.get(key)
368
371
369 def if_(context, mapping, args):
372 def if_(context, mapping, args):
370 """:if(expr, then[, else]): Conditionally execute based on the result of
373 """:if(expr, then[, else]): Conditionally execute based on the result of
371 an expression."""
374 an expression."""
372 if not (2 <= len(args) <= 3):
375 if not (2 <= len(args) <= 3):
373 # i18n: "if" is a keyword
376 # i18n: "if" is a keyword
374 raise error.ParseError(_("if expects two or three arguments"))
377 raise error.ParseError(_("if expects two or three arguments"))
375
378
376 test = stringify(args[0][0](context, mapping, args[0][1]))
379 test = stringify(args[0][0](context, mapping, args[0][1]))
377 if test:
380 if test:
378 yield args[1][0](context, mapping, args[1][1])
381 yield args[1][0](context, mapping, args[1][1])
379 elif len(args) == 3:
382 elif len(args) == 3:
380 yield args[2][0](context, mapping, args[2][1])
383 yield args[2][0](context, mapping, args[2][1])
381
384
382 def ifcontains(context, mapping, args):
385 def ifcontains(context, mapping, args):
383 """:ifcontains(search, thing, then[, else]): Conditionally execute based
386 """:ifcontains(search, thing, then[, else]): Conditionally execute based
384 on whether the item "search" is in "thing"."""
387 on whether the item "search" is in "thing"."""
385 if not (3 <= len(args) <= 4):
388 if not (3 <= len(args) <= 4):
386 # i18n: "ifcontains" is a keyword
389 # i18n: "ifcontains" is a keyword
387 raise error.ParseError(_("ifcontains expects three or four arguments"))
390 raise error.ParseError(_("ifcontains expects three or four arguments"))
388
391
389 item = stringify(args[0][0](context, mapping, args[0][1]))
392 item = stringify(args[0][0](context, mapping, args[0][1]))
390 items = args[1][0](context, mapping, args[1][1])
393 items = args[1][0](context, mapping, args[1][1])
391
394
392 if item in items:
395 if item in items:
393 yield args[2][0](context, mapping, args[2][1])
396 yield args[2][0](context, mapping, args[2][1])
394 elif len(args) == 4:
397 elif len(args) == 4:
395 yield args[3][0](context, mapping, args[3][1])
398 yield args[3][0](context, mapping, args[3][1])
396
399
397 def ifeq(context, mapping, args):
400 def ifeq(context, mapping, args):
398 """:ifeq(expr1, expr2, then[, else]): Conditionally execute based on
401 """:ifeq(expr1, expr2, then[, else]): Conditionally execute based on
399 whether 2 items are equivalent."""
402 whether 2 items are equivalent."""
400 if not (3 <= len(args) <= 4):
403 if not (3 <= len(args) <= 4):
401 # i18n: "ifeq" is a keyword
404 # i18n: "ifeq" is a keyword
402 raise error.ParseError(_("ifeq expects three or four arguments"))
405 raise error.ParseError(_("ifeq expects three or four arguments"))
403
406
404 test = stringify(args[0][0](context, mapping, args[0][1]))
407 test = stringify(args[0][0](context, mapping, args[0][1]))
405 match = stringify(args[1][0](context, mapping, args[1][1]))
408 match = stringify(args[1][0](context, mapping, args[1][1]))
406 if test == match:
409 if test == match:
407 yield args[2][0](context, mapping, args[2][1])
410 yield args[2][0](context, mapping, args[2][1])
408 elif len(args) == 4:
411 elif len(args) == 4:
409 yield args[3][0](context, mapping, args[3][1])
412 yield args[3][0](context, mapping, args[3][1])
410
413
411 def join(context, mapping, args):
414 def join(context, mapping, args):
412 """:join(list, sep): Join items in a list with a delimiter."""
415 """:join(list, sep): Join items in a list with a delimiter."""
413 if not (1 <= len(args) <= 2):
416 if not (1 <= len(args) <= 2):
414 # i18n: "join" is a keyword
417 # i18n: "join" is a keyword
415 raise error.ParseError(_("join expects one or two arguments"))
418 raise error.ParseError(_("join expects one or two arguments"))
416
419
417 joinset = args[0][0](context, mapping, args[0][1])
420 joinset = args[0][0](context, mapping, args[0][1])
418 if callable(joinset):
421 if callable(joinset):
419 jf = joinset.joinfmt
422 jf = joinset.joinfmt
420 joinset = [jf(x) for x in joinset()]
423 joinset = [jf(x) for x in joinset()]
421
424
422 joiner = " "
425 joiner = " "
423 if len(args) > 1:
426 if len(args) > 1:
424 joiner = stringify(args[1][0](context, mapping, args[1][1]))
427 joiner = stringify(args[1][0](context, mapping, args[1][1]))
425
428
426 first = True
429 first = True
427 for x in joinset:
430 for x in joinset:
428 if first:
431 if first:
429 first = False
432 first = False
430 else:
433 else:
431 yield joiner
434 yield joiner
432 yield x
435 yield x
433
436
434 def label(context, mapping, args):
437 def label(context, mapping, args):
435 """:label(label, expr): Apply a label to generated content. Content with
438 """:label(label, expr): Apply a label to generated content. Content with
436 a label applied can result in additional post-processing, such as
439 a label applied can result in additional post-processing, such as
437 automatic colorization."""
440 automatic colorization."""
438 if len(args) != 2:
441 if len(args) != 2:
439 # i18n: "label" is a keyword
442 # i18n: "label" is a keyword
440 raise error.ParseError(_("label expects two arguments"))
443 raise error.ParseError(_("label expects two arguments"))
441
444
442 # ignore args[0] (the label string) since this is supposed to be a a no-op
445 # ignore args[0] (the label string) since this is supposed to be a a no-op
443 yield args[1][0](context, mapping, args[1][1])
446 yield args[1][0](context, mapping, args[1][1])
444
447
445 def revset(context, mapping, args):
448 def revset(context, mapping, args):
446 """:revset(query[, formatargs...]): Execute a revision set query. See
449 """:revset(query[, formatargs...]): Execute a revision set query. See
447 :hg:`help revset`."""
450 :hg:`help revset`."""
448 if not len(args) > 0:
451 if not len(args) > 0:
449 # i18n: "revset" is a keyword
452 # i18n: "revset" is a keyword
450 raise error.ParseError(_("revset expects one or more arguments"))
453 raise error.ParseError(_("revset expects one or more arguments"))
451
454
452 raw = args[0][1]
455 raw = args[0][1]
453 ctx = mapping['ctx']
456 ctx = mapping['ctx']
454 repo = ctx.repo()
457 repo = ctx.repo()
455
458
456 def query(expr):
459 def query(expr):
457 m = revsetmod.match(repo.ui, expr)
460 m = revsetmod.match(repo.ui, expr)
458 return m(repo)
461 return m(repo)
459
462
460 if len(args) > 1:
463 if len(args) > 1:
461 formatargs = list([a[0](context, mapping, a[1]) for a in args[1:]])
464 formatargs = list([a[0](context, mapping, a[1]) for a in args[1:]])
462 revs = query(revsetmod.formatspec(raw, *formatargs))
465 revs = query(revsetmod.formatspec(raw, *formatargs))
463 revs = list([str(r) for r in revs])
466 revs = list([str(r) for r in revs])
464 else:
467 else:
465 revsetcache = mapping['cache'].setdefault("revsetcache", {})
468 revsetcache = mapping['cache'].setdefault("revsetcache", {})
466 if raw in revsetcache:
469 if raw in revsetcache:
467 revs = revsetcache[raw]
470 revs = revsetcache[raw]
468 else:
471 else:
469 revs = query(raw)
472 revs = query(raw)
470 revs = list([str(r) for r in revs])
473 revs = list([str(r) for r in revs])
471 revsetcache[raw] = revs
474 revsetcache[raw] = revs
472
475
473 return templatekw.showlist("revision", revs, **mapping)
476 return templatekw.showlist("revision", revs, **mapping)
474
477
475 def rstdoc(context, mapping, args):
478 def rstdoc(context, mapping, args):
476 """:rstdoc(text, style): Format ReStructuredText."""
479 """:rstdoc(text, style): Format ReStructuredText."""
477 if len(args) != 2:
480 if len(args) != 2:
478 # i18n: "rstdoc" is a keyword
481 # i18n: "rstdoc" is a keyword
479 raise error.ParseError(_("rstdoc expects two arguments"))
482 raise error.ParseError(_("rstdoc expects two arguments"))
480
483
481 text = stringify(args[0][0](context, mapping, args[0][1]))
484 text = stringify(args[0][0](context, mapping, args[0][1]))
482 style = stringify(args[1][0](context, mapping, args[1][1]))
485 style = stringify(args[1][0](context, mapping, args[1][1]))
483
486
484 return minirst.format(text, style=style, keep=['verbose'])
487 return minirst.format(text, style=style, keep=['verbose'])
485
488
486 def shortest(context, mapping, args):
489 def shortest(context, mapping, args):
487 """:shortest(node, minlength=4): Obtain the shortest representation of
490 """:shortest(node, minlength=4): Obtain the shortest representation of
488 a node."""
491 a node."""
489 if not (1 <= len(args) <= 2):
492 if not (1 <= len(args) <= 2):
490 # i18n: "shortest" is a keyword
493 # i18n: "shortest" is a keyword
491 raise error.ParseError(_("shortest() expects one or two arguments"))
494 raise error.ParseError(_("shortest() expects one or two arguments"))
492
495
493 node = stringify(args[0][0](context, mapping, args[0][1]))
496 node = stringify(args[0][0](context, mapping, args[0][1]))
494
497
495 minlength = 4
498 minlength = 4
496 if len(args) > 1:
499 if len(args) > 1:
497 minlength = int(args[1][1])
500 minlength = int(args[1][1])
498
501
499 cl = mapping['ctx']._repo.changelog
502 cl = mapping['ctx']._repo.changelog
500 def isvalid(test):
503 def isvalid(test):
501 try:
504 try:
502 try:
505 try:
503 cl.index.partialmatch(test)
506 cl.index.partialmatch(test)
504 except AttributeError:
507 except AttributeError:
505 # Pure mercurial doesn't support partialmatch on the index.
508 # Pure mercurial doesn't support partialmatch on the index.
506 # Fallback to the slow way.
509 # Fallback to the slow way.
507 if cl._partialmatch(test) is None:
510 if cl._partialmatch(test) is None:
508 return False
511 return False
509
512
510 try:
513 try:
511 i = int(test)
514 i = int(test)
512 # if we are a pure int, then starting with zero will not be
515 # if we are a pure int, then starting with zero will not be
513 # confused as a rev; or, obviously, if the int is larger than
516 # confused as a rev; or, obviously, if the int is larger than
514 # the value of the tip rev
517 # the value of the tip rev
515 if test[0] == '0' or i > len(cl):
518 if test[0] == '0' or i > len(cl):
516 return True
519 return True
517 return False
520 return False
518 except ValueError:
521 except ValueError:
519 return True
522 return True
520 except error.RevlogError:
523 except error.RevlogError:
521 return False
524 return False
522
525
523 shortest = node
526 shortest = node
524 startlength = max(6, minlength)
527 startlength = max(6, minlength)
525 length = startlength
528 length = startlength
526 while True:
529 while True:
527 test = node[:length]
530 test = node[:length]
528 if isvalid(test):
531 if isvalid(test):
529 shortest = test
532 shortest = test
530 if length == minlength or length > startlength:
533 if length == minlength or length > startlength:
531 return shortest
534 return shortest
532 length -= 1
535 length -= 1
533 else:
536 else:
534 length += 1
537 length += 1
535 if len(shortest) <= length:
538 if len(shortest) <= length:
536 return shortest
539 return shortest
537
540
538 def strip(context, mapping, args):
541 def strip(context, mapping, args):
539 """:strip(text[, chars]): Strip characters from a string."""
542 """:strip(text[, chars]): Strip characters from a string."""
540 if not (1 <= len(args) <= 2):
543 if not (1 <= len(args) <= 2):
541 # i18n: "strip" is a keyword
544 # i18n: "strip" is a keyword
542 raise error.ParseError(_("strip expects one or two arguments"))
545 raise error.ParseError(_("strip expects one or two arguments"))
543
546
544 text = stringify(args[0][0](context, mapping, args[0][1]))
547 text = stringify(args[0][0](context, mapping, args[0][1]))
545 if len(args) == 2:
548 if len(args) == 2:
546 chars = stringify(args[1][0](context, mapping, args[1][1]))
549 chars = stringify(args[1][0](context, mapping, args[1][1]))
547 return text.strip(chars)
550 return text.strip(chars)
548 return text.strip()
551 return text.strip()
549
552
550 def sub(context, mapping, args):
553 def sub(context, mapping, args):
551 """:sub(pattern, replacement, expression): Perform text substitution
554 """:sub(pattern, replacement, expression): Perform text substitution
552 using regular expressions."""
555 using regular expressions."""
553 if len(args) != 3:
556 if len(args) != 3:
554 # i18n: "sub" is a keyword
557 # i18n: "sub" is a keyword
555 raise error.ParseError(_("sub expects three arguments"))
558 raise error.ParseError(_("sub expects three arguments"))
556
559
557 pat = stringify(args[0][0](context, mapping, args[0][1]))
560 pat = stringify(args[0][0](context, mapping, args[0][1]))
558 rpl = stringify(args[1][0](context, mapping, args[1][1]))
561 rpl = stringify(args[1][0](context, mapping, args[1][1]))
559 src = stringify(args[2][0](context, mapping, args[2][1]))
562 src = stringify(args[2][0](context, mapping, args[2][1]))
560 yield re.sub(pat, rpl, src)
563 yield re.sub(pat, rpl, src)
561
564
562 def startswith(context, mapping, args):
565 def startswith(context, mapping, args):
563 """:startswith(pattern, text): Returns the value from the "text" argument
566 """:startswith(pattern, text): Returns the value from the "text" argument
564 if it begins with the content from the "pattern" argument."""
567 if it begins with the content from the "pattern" argument."""
565 if len(args) != 2:
568 if len(args) != 2:
566 # i18n: "startswith" is a keyword
569 # i18n: "startswith" is a keyword
567 raise error.ParseError(_("startswith expects two arguments"))
570 raise error.ParseError(_("startswith expects two arguments"))
568
571
569 patn = stringify(args[0][0](context, mapping, args[0][1]))
572 patn = stringify(args[0][0](context, mapping, args[0][1]))
570 text = stringify(args[1][0](context, mapping, args[1][1]))
573 text = stringify(args[1][0](context, mapping, args[1][1]))
571 if text.startswith(patn):
574 if text.startswith(patn):
572 return text
575 return text
573 return ''
576 return ''
574
577
575
578
576 def word(context, mapping, args):
579 def word(context, mapping, args):
577 """:word(number, text[, separator]): Return the nth word from a string."""
580 """:word(number, text[, separator]): Return the nth word from a string."""
578 if not (2 <= len(args) <= 3):
581 if not (2 <= len(args) <= 3):
579 # i18n: "word" is a keyword
582 # i18n: "word" is a keyword
580 raise error.ParseError(_("word expects two or three arguments, got %d")
583 raise error.ParseError(_("word expects two or three arguments, got %d")
581 % len(args))
584 % len(args))
582
585
583 try:
586 try:
584 num = int(stringify(args[0][0](context, mapping, args[0][1])))
587 num = int(stringify(args[0][0](context, mapping, args[0][1])))
585 except ValueError:
588 except ValueError:
586 # i18n: "word" is a keyword
589 # i18n: "word" is a keyword
587 raise error.ParseError(_("word expects an integer index"))
590 raise error.ParseError(_("word expects an integer index"))
588 text = stringify(args[1][0](context, mapping, args[1][1]))
591 text = stringify(args[1][0](context, mapping, args[1][1]))
589 if len(args) == 3:
592 if len(args) == 3:
590 splitter = stringify(args[2][0](context, mapping, args[2][1]))
593 splitter = stringify(args[2][0](context, mapping, args[2][1]))
591 else:
594 else:
592 splitter = None
595 splitter = None
593
596
594 tokens = text.split(splitter)
597 tokens = text.split(splitter)
595 if num >= len(tokens):
598 if num >= len(tokens):
596 return ''
599 return ''
597 else:
600 else:
598 return tokens[num]
601 return tokens[num]
599
602
600 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
603 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
601 exprmethods = {
604 exprmethods = {
602 "integer": lambda e, c: (runinteger, e[1]),
605 "integer": lambda e, c: (runinteger, e[1]),
603 "string": lambda e, c: (runstring, e[1]),
606 "string": lambda e, c: (runstring, e[1]),
604 "rawstring": lambda e, c: (runrawstring, e[1]),
607 "rawstring": lambda e, c: (runrawstring, e[1]),
605 "symbol": lambda e, c: (runsymbol, e[1]),
608 "symbol": lambda e, c: (runsymbol, e[1]),
606 "template": buildtemplate,
609 "template": buildtemplate,
607 "group": lambda e, c: compileexp(e[1], c, exprmethods),
610 "group": lambda e, c: compileexp(e[1], c, exprmethods),
608 # ".": buildmember,
611 # ".": buildmember,
609 "|": buildfilter,
612 "|": buildfilter,
610 "%": buildmap,
613 "%": buildmap,
611 "func": buildfunc,
614 "func": buildfunc,
612 }
615 }
613
616
614 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
617 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
615 methods = exprmethods.copy()
618 methods = exprmethods.copy()
616 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
619 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
617
620
618 funcs = {
621 funcs = {
619 "date": date,
622 "date": date,
620 "diff": diff,
623 "diff": diff,
621 "fill": fill,
624 "fill": fill,
622 "get": get,
625 "get": get,
623 "if": if_,
626 "if": if_,
624 "ifcontains": ifcontains,
627 "ifcontains": ifcontains,
625 "ifeq": ifeq,
628 "ifeq": ifeq,
626 "indent": indent,
629 "indent": indent,
627 "join": join,
630 "join": join,
628 "label": label,
631 "label": label,
629 "pad": pad,
632 "pad": pad,
630 "revset": revset,
633 "revset": revset,
631 "rstdoc": rstdoc,
634 "rstdoc": rstdoc,
632 "shortest": shortest,
635 "shortest": shortest,
633 "startswith": startswith,
636 "startswith": startswith,
634 "strip": strip,
637 "strip": strip,
635 "sub": sub,
638 "sub": sub,
636 "word": word,
639 "word": word,
637 }
640 }
638
641
639 # template engine
642 # template engine
640
643
641 stringify = templatefilters.stringify
644 stringify = templatefilters.stringify
642
645
643 def _flatten(thing):
646 def _flatten(thing):
644 '''yield a single stream from a possibly nested set of iterators'''
647 '''yield a single stream from a possibly nested set of iterators'''
645 if isinstance(thing, str):
648 if isinstance(thing, str):
646 yield thing
649 yield thing
647 elif not util.safehasattr(thing, '__iter__'):
650 elif not util.safehasattr(thing, '__iter__'):
648 if thing is not None:
651 if thing is not None:
649 yield str(thing)
652 yield str(thing)
650 else:
653 else:
651 for i in thing:
654 for i in thing:
652 if isinstance(i, str):
655 if isinstance(i, str):
653 yield i
656 yield i
654 elif not util.safehasattr(i, '__iter__'):
657 elif not util.safehasattr(i, '__iter__'):
655 if i is not None:
658 if i is not None:
656 yield str(i)
659 yield str(i)
657 elif i is not None:
660 elif i is not None:
658 for j in _flatten(i):
661 for j in _flatten(i):
659 yield j
662 yield j
660
663
661 def unquotestring(s):
664 def unquotestring(s):
662 '''unwrap quotes'''
665 '''unwrap quotes'''
663 if len(s) < 2 or s[0] != s[-1]:
666 if len(s) < 2 or s[0] != s[-1]:
664 raise SyntaxError(_('unmatched quotes'))
667 raise SyntaxError(_('unmatched quotes'))
665 # de-backslash-ify only <\">. it is invalid syntax in non-string part of
668 # de-backslash-ify only <\">. it is invalid syntax in non-string part of
666 # template, but we are likely to escape <"> in quoted string and it was
669 # template, but we are likely to escape <"> in quoted string and it was
667 # accepted before, thanks to issue4290. <\\"> is unmodified because it
670 # accepted before, thanks to issue4290. <\\"> is unmodified because it
668 # is ambiguous and it was processed as such before 2.8.1.
671 # is ambiguous and it was processed as such before 2.8.1.
669 #
672 #
670 # template result
673 # template result
671 # --------- ------------------------
674 # --------- ------------------------
672 # {\"\"} parse error
675 # {\"\"} parse error
673 # "{""}" {""} -> <>
676 # "{""}" {""} -> <>
674 # "{\"\"}" {""} -> <>
677 # "{\"\"}" {""} -> <>
675 # {"\""} {"\""} -> <">
678 # {"\""} {"\""} -> <">
676 # '{"\""}' {"\""} -> <">
679 # '{"\""}' {"\""} -> <">
677 # "{"\""}" parse error (don't care)
680 # "{"\""}" parse error (don't care)
678 q = s[0]
681 q = s[0]
679 return s[1:-1].replace('\\\\' + q, '\\\\\\' + q).replace('\\' + q, q)
682 return s[1:-1].replace('\\\\' + q, '\\\\\\' + q).replace('\\' + q, q)
680
683
681 class engine(object):
684 class engine(object):
682 '''template expansion engine.
685 '''template expansion engine.
683
686
684 template expansion works like this. a map file contains key=value
687 template expansion works like this. a map file contains key=value
685 pairs. if value is quoted, it is treated as string. otherwise, it
688 pairs. if value is quoted, it is treated as string. otherwise, it
686 is treated as name of template file.
689 is treated as name of template file.
687
690
688 templater is asked to expand a key in map. it looks up key, and
691 templater is asked to expand a key in map. it looks up key, and
689 looks for strings like this: {foo}. it expands {foo} by looking up
692 looks for strings like this: {foo}. it expands {foo} by looking up
690 foo in map, and substituting it. expansion is recursive: it stops
693 foo in map, and substituting it. expansion is recursive: it stops
691 when there is no more {foo} to replace.
694 when there is no more {foo} to replace.
692
695
693 expansion also allows formatting and filtering.
696 expansion also allows formatting and filtering.
694
697
695 format uses key to expand each item in list. syntax is
698 format uses key to expand each item in list. syntax is
696 {key%format}.
699 {key%format}.
697
700
698 filter uses function to transform value. syntax is
701 filter uses function to transform value. syntax is
699 {key|filter1|filter2|...}.'''
702 {key|filter1|filter2|...}.'''
700
703
701 def __init__(self, loader, filters={}, defaults={}):
704 def __init__(self, loader, filters={}, defaults={}):
702 self._loader = loader
705 self._loader = loader
703 self._filters = filters
706 self._filters = filters
704 self._defaults = defaults
707 self._defaults = defaults
705 self._cache = {}
708 self._cache = {}
706
709
707 def _load(self, t):
710 def _load(self, t):
708 '''load, parse, and cache a template'''
711 '''load, parse, and cache a template'''
709 if t not in self._cache:
712 if t not in self._cache:
710 self._cache[t] = compiletemplate(self._loader(t), self)
713 self._cache[t] = compiletemplate(self._loader(t), self)
711 return self._cache[t]
714 return self._cache[t]
712
715
713 def process(self, t, mapping):
716 def process(self, t, mapping):
714 '''Perform expansion. t is name of map element to expand.
717 '''Perform expansion. t is name of map element to expand.
715 mapping contains added elements for use during expansion. Is a
718 mapping contains added elements for use during expansion. Is a
716 generator.'''
719 generator.'''
717 return _flatten(runtemplate(self, mapping, self._load(t)))
720 return _flatten(runtemplate(self, mapping, self._load(t)))
718
721
719 engines = {'default': engine}
722 engines = {'default': engine}
720
723
721 def stylelist():
724 def stylelist():
722 paths = templatepaths()
725 paths = templatepaths()
723 if not paths:
726 if not paths:
724 return _('no templates found, try `hg debuginstall` for more info')
727 return _('no templates found, try `hg debuginstall` for more info')
725 dirlist = os.listdir(paths[0])
728 dirlist = os.listdir(paths[0])
726 stylelist = []
729 stylelist = []
727 for file in dirlist:
730 for file in dirlist:
728 split = file.split(".")
731 split = file.split(".")
729 if split[0] == "map-cmdline":
732 if split[0] == "map-cmdline":
730 stylelist.append(split[1])
733 stylelist.append(split[1])
731 return ", ".join(sorted(stylelist))
734 return ", ".join(sorted(stylelist))
732
735
733 class TemplateNotFound(util.Abort):
736 class TemplateNotFound(util.Abort):
734 pass
737 pass
735
738
736 class templater(object):
739 class templater(object):
737
740
738 def __init__(self, mapfile, filters={}, defaults={}, cache={},
741 def __init__(self, mapfile, filters={}, defaults={}, cache={},
739 minchunk=1024, maxchunk=65536):
742 minchunk=1024, maxchunk=65536):
740 '''set up template engine.
743 '''set up template engine.
741 mapfile is name of file to read map definitions from.
744 mapfile is name of file to read map definitions from.
742 filters is dict of functions. each transforms a value into another.
745 filters is dict of functions. each transforms a value into another.
743 defaults is dict of default map definitions.'''
746 defaults is dict of default map definitions.'''
744 self.mapfile = mapfile or 'template'
747 self.mapfile = mapfile or 'template'
745 self.cache = cache.copy()
748 self.cache = cache.copy()
746 self.map = {}
749 self.map = {}
747 if mapfile:
750 if mapfile:
748 self.base = os.path.dirname(mapfile)
751 self.base = os.path.dirname(mapfile)
749 else:
752 else:
750 self.base = ''
753 self.base = ''
751 self.filters = templatefilters.filters.copy()
754 self.filters = templatefilters.filters.copy()
752 self.filters.update(filters)
755 self.filters.update(filters)
753 self.defaults = defaults
756 self.defaults = defaults
754 self.minchunk, self.maxchunk = minchunk, maxchunk
757 self.minchunk, self.maxchunk = minchunk, maxchunk
755 self.ecache = {}
758 self.ecache = {}
756
759
757 if not mapfile:
760 if not mapfile:
758 return
761 return
759 if not os.path.exists(mapfile):
762 if not os.path.exists(mapfile):
760 raise util.Abort(_("style '%s' not found") % mapfile,
763 raise util.Abort(_("style '%s' not found") % mapfile,
761 hint=_("available styles: %s") % stylelist())
764 hint=_("available styles: %s") % stylelist())
762
765
763 conf = config.config(includepaths=templatepaths())
766 conf = config.config(includepaths=templatepaths())
764 conf.read(mapfile)
767 conf.read(mapfile)
765
768
766 for key, val in conf[''].items():
769 for key, val in conf[''].items():
767 if not val:
770 if not val:
768 raise SyntaxError(_('%s: missing value') % conf.source('', key))
771 raise SyntaxError(_('%s: missing value') % conf.source('', key))
769 if val[0] in "'\"":
772 if val[0] in "'\"":
770 try:
773 try:
771 self.cache[key] = unquotestring(val)
774 self.cache[key] = unquotestring(val)
772 except SyntaxError, inst:
775 except SyntaxError, inst:
773 raise SyntaxError('%s: %s' %
776 raise SyntaxError('%s: %s' %
774 (conf.source('', key), inst.args[0]))
777 (conf.source('', key), inst.args[0]))
775 else:
778 else:
776 val = 'default', val
779 val = 'default', val
777 if ':' in val[1]:
780 if ':' in val[1]:
778 val = val[1].split(':', 1)
781 val = val[1].split(':', 1)
779 self.map[key] = val[0], os.path.join(self.base, val[1])
782 self.map[key] = val[0], os.path.join(self.base, val[1])
780
783
781 def __contains__(self, key):
784 def __contains__(self, key):
782 return key in self.cache or key in self.map
785 return key in self.cache or key in self.map
783
786
784 def load(self, t):
787 def load(self, t):
785 '''Get the template for the given template name. Use a local cache.'''
788 '''Get the template for the given template name. Use a local cache.'''
786 if t not in self.cache:
789 if t not in self.cache:
787 try:
790 try:
788 self.cache[t] = util.readfile(self.map[t][1])
791 self.cache[t] = util.readfile(self.map[t][1])
789 except KeyError, inst:
792 except KeyError, inst:
790 raise TemplateNotFound(_('"%s" not in template map') %
793 raise TemplateNotFound(_('"%s" not in template map') %
791 inst.args[0])
794 inst.args[0])
792 except IOError, inst:
795 except IOError, inst:
793 raise IOError(inst.args[0], _('template file %s: %s') %
796 raise IOError(inst.args[0], _('template file %s: %s') %
794 (self.map[t][1], inst.args[1]))
797 (self.map[t][1], inst.args[1]))
795 return self.cache[t]
798 return self.cache[t]
796
799
797 def __call__(self, t, **mapping):
800 def __call__(self, t, **mapping):
798 ttype = t in self.map and self.map[t][0] or 'default'
801 ttype = t in self.map and self.map[t][0] or 'default'
799 if ttype not in self.ecache:
802 if ttype not in self.ecache:
800 self.ecache[ttype] = engines[ttype](self.load,
803 self.ecache[ttype] = engines[ttype](self.load,
801 self.filters, self.defaults)
804 self.filters, self.defaults)
802 proc = self.ecache[ttype]
805 proc = self.ecache[ttype]
803
806
804 stream = proc.process(t, mapping)
807 stream = proc.process(t, mapping)
805 if self.minchunk:
808 if self.minchunk:
806 stream = util.increasingchunks(stream, min=self.minchunk,
809 stream = util.increasingchunks(stream, min=self.minchunk,
807 max=self.maxchunk)
810 max=self.maxchunk)
808 return stream
811 return stream
809
812
810 def templatepaths():
813 def templatepaths():
811 '''return locations used for template files.'''
814 '''return locations used for template files.'''
812 pathsrel = ['templates']
815 pathsrel = ['templates']
813 paths = [os.path.normpath(os.path.join(util.datapath, f))
816 paths = [os.path.normpath(os.path.join(util.datapath, f))
814 for f in pathsrel]
817 for f in pathsrel]
815 return [p for p in paths if os.path.isdir(p)]
818 return [p for p in paths if os.path.isdir(p)]
816
819
817 def templatepath(name):
820 def templatepath(name):
818 '''return location of template file. returns None if not found.'''
821 '''return location of template file. returns None if not found.'''
819 for p in templatepaths():
822 for p in templatepaths():
820 f = os.path.join(p, name)
823 f = os.path.join(p, name)
821 if os.path.exists(f):
824 if os.path.exists(f):
822 return f
825 return f
823 return None
826 return None
824
827
825 def stylemap(styles, paths=None):
828 def stylemap(styles, paths=None):
826 """Return path to mapfile for a given style.
829 """Return path to mapfile for a given style.
827
830
828 Searches mapfile in the following locations:
831 Searches mapfile in the following locations:
829 1. templatepath/style/map
832 1. templatepath/style/map
830 2. templatepath/map-style
833 2. templatepath/map-style
831 3. templatepath/map
834 3. templatepath/map
832 """
835 """
833
836
834 if paths is None:
837 if paths is None:
835 paths = templatepaths()
838 paths = templatepaths()
836 elif isinstance(paths, str):
839 elif isinstance(paths, str):
837 paths = [paths]
840 paths = [paths]
838
841
839 if isinstance(styles, str):
842 if isinstance(styles, str):
840 styles = [styles]
843 styles = [styles]
841
844
842 for style in styles:
845 for style in styles:
843 # only plain name is allowed to honor template paths
846 # only plain name is allowed to honor template paths
844 if (not style
847 if (not style
845 or style in (os.curdir, os.pardir)
848 or style in (os.curdir, os.pardir)
846 or os.sep in style
849 or os.sep in style
847 or os.altsep and os.altsep in style):
850 or os.altsep and os.altsep in style):
848 continue
851 continue
849 locations = [os.path.join(style, 'map'), 'map-' + style]
852 locations = [os.path.join(style, 'map'), 'map-' + style]
850 locations.append('map')
853 locations.append('map')
851
854
852 for path in paths:
855 for path in paths:
853 for location in locations:
856 for location in locations:
854 mapfile = os.path.join(path, location)
857 mapfile = os.path.join(path, location)
855 if os.path.isfile(mapfile):
858 if os.path.isfile(mapfile):
856 return style, mapfile
859 return style, mapfile
857
860
858 raise RuntimeError("No hgweb templates found in %r" % paths)
861 raise RuntimeError("No hgweb templates found in %r" % paths)
859
862
860 # tell hggettext to extract docstrings from these functions:
863 # tell hggettext to extract docstrings from these functions:
861 i18nfunctions = funcs.values()
864 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now