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