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