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