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