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