##// END OF EJS Templates
templater: split template functions to new module...
Yuya Nishihara -
r36940:521f6c7e default
parent child Browse files
Show More
@@ -132,8 +132,9 b' i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n'
132 $(PYTHON) i18n/hggettext mercurial/commands.py \
132 $(PYTHON) i18n/hggettext mercurial/commands.py \
133 hgext/*.py hgext/*/__init__.py \
133 hgext/*.py hgext/*/__init__.py \
134 mercurial/fileset.py mercurial/revset.py \
134 mercurial/fileset.py mercurial/revset.py \
135 mercurial/templatefilters.py mercurial/templatekw.py \
135 mercurial/templatefilters.py \
136 mercurial/templater.py \
136 mercurial/templatefuncs.py \
137 mercurial/templatekw.py \
137 mercurial/filemerge.py \
138 mercurial/filemerge.py \
138 mercurial/hgweb/webcommands.py \
139 mercurial/hgweb/webcommands.py \
139 mercurial/util.py \
140 mercurial/util.py \
@@ -290,8 +290,8 b' def loadall(ui, whitelist=None):'
290 fileset,
290 fileset,
291 revset,
291 revset,
292 templatefilters,
292 templatefilters,
293 templatefuncs,
293 templatekw,
294 templatekw,
294 templater,
295 )
295 )
296
296
297 # list of (objname, loadermod, loadername) tuple:
297 # list of (objname, loadermod, loadername) tuple:
@@ -307,7 +307,7 b' def loadall(ui, whitelist=None):'
307 ('internalmerge', filemerge, 'loadinternalmerge'),
307 ('internalmerge', filemerge, 'loadinternalmerge'),
308 ('revsetpredicate', revset, 'loadpredicate'),
308 ('revsetpredicate', revset, 'loadpredicate'),
309 ('templatefilter', templatefilters, 'loadfilter'),
309 ('templatefilter', templatefilters, 'loadfilter'),
310 ('templatefunc', templater, 'loadfunction'),
310 ('templatefunc', templatefuncs, 'loadfunction'),
311 ('templatekeyword', templatekw, 'loadkeyword'),
311 ('templatekeyword', templatekw, 'loadkeyword'),
312 ]
312 ]
313 _loadextra(ui, newindex, extraloaders)
313 _loadextra(ui, newindex, extraloaders)
@@ -26,8 +26,8 b' from . import ('
26 pycompat,
26 pycompat,
27 revset,
27 revset,
28 templatefilters,
28 templatefilters,
29 templatefuncs,
29 templatekw,
30 templatekw,
30 templater,
31 util,
31 util,
32 )
32 )
33 from .hgweb import (
33 from .hgweb import (
@@ -309,7 +309,7 b" addtopicsymbols('merge-tools', '.. inter"
309 addtopicsymbols('revisions', '.. predicatesmarker', revset.symbols)
309 addtopicsymbols('revisions', '.. predicatesmarker', revset.symbols)
310 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
310 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
311 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
311 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
312 addtopicsymbols('templates', '.. functionsmarker', templater.funcs)
312 addtopicsymbols('templates', '.. functionsmarker', templatefuncs.funcs)
313 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
313 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
314 dedent=True)
314 dedent=True)
315
315
@@ -368,7 +368,7 b' class templatefunc(_templateregistrarbas'
368 extension, if an instance named as 'templatefunc' is used for
368 extension, if an instance named as 'templatefunc' is used for
369 decorating in extension.
369 decorating in extension.
370
370
371 Otherwise, explicit 'templater.loadfunction()' is needed.
371 Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
372 """
372 """
373 _getname = _funcregistrarbase._parsefuncdecl
373 _getname = _funcregistrarbase._parsefuncdecl
374
374
This diff has been collapsed as it changes many lines, (784 lines changed) Show them Hide them
@@ -1,24 +1,21 b''
1 # templater.py - template expansion for output
1 # templatefuncs.py - common template functions
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, print_function
8 from __future__ import absolute_import
9
9
10 import os
11 import re
10 import re
12
11
13 from .i18n import _
12 from .i18n import _
14 from . import (
13 from . import (
15 color,
14 color,
16 config,
17 encoding,
15 encoding,
18 error,
16 error,
19 minirst,
17 minirst,
20 obsutil,
18 obsutil,
21 parser,
22 pycompat,
19 pycompat,
23 registrar,
20 registrar,
24 revset as revsetmod,
21 revset as revsetmod,
@@ -39,425 +36,8 b' evalstring = templateutil.evalstring'
39 evalstringliteral = templateutil.evalstringliteral
36 evalstringliteral = templateutil.evalstringliteral
40 evalastype = templateutil.evalastype
37 evalastype = templateutil.evalastype
41
38
42 # template parsing
43
44 elements = {
45 # token-type: binding-strength, primary, prefix, infix, suffix
46 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
47 ".": (18, None, None, (".", 18), None),
48 "%": (15, None, None, ("%", 15), None),
49 "|": (15, None, None, ("|", 15), None),
50 "*": (5, None, None, ("*", 5), None),
51 "/": (5, None, None, ("/", 5), None),
52 "+": (4, None, None, ("+", 4), None),
53 "-": (4, None, ("negate", 19), ("-", 4), None),
54 "=": (3, None, None, ("keyvalue", 3), None),
55 ",": (2, None, None, ("list", 2), None),
56 ")": (0, None, None, None, None),
57 "integer": (0, "integer", None, None, None),
58 "symbol": (0, "symbol", None, None, None),
59 "string": (0, "string", None, None, None),
60 "template": (0, "template", None, None, None),
61 "end": (0, None, None, None, None),
62 }
63
64 def tokenize(program, start, end, term=None):
65 """Parse a template expression into a stream of tokens, which must end
66 with term if specified"""
67 pos = start
68 program = pycompat.bytestr(program)
69 while pos < end:
70 c = program[pos]
71 if c.isspace(): # skip inter-token whitespace
72 pass
73 elif c in "(=,).%|+-*/": # handle simple operators
74 yield (c, None, pos)
75 elif c in '"\'': # handle quoted templates
76 s = pos + 1
77 data, pos = _parsetemplate(program, s, end, c)
78 yield ('template', data, s)
79 pos -= 1
80 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
81 # handle quoted strings
82 c = program[pos + 1]
83 s = pos = pos + 2
84 while pos < end: # find closing quote
85 d = program[pos]
86 if d == '\\': # skip over escaped characters
87 pos += 2
88 continue
89 if d == c:
90 yield ('string', program[s:pos], s)
91 break
92 pos += 1
93 else:
94 raise error.ParseError(_("unterminated string"), s)
95 elif c.isdigit():
96 s = pos
97 while pos < end:
98 d = program[pos]
99 if not d.isdigit():
100 break
101 pos += 1
102 yield ('integer', program[s:pos], s)
103 pos -= 1
104 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
105 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
106 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
107 # where some of nested templates were preprocessed as strings and
108 # then compiled. therefore, \"...\" was allowed. (issue4733)
109 #
110 # processing flow of _evalifliteral() at 5ab28a2e9962:
111 # outer template string -> stringify() -> compiletemplate()
112 # ------------------------ ------------ ------------------
113 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
114 # ~~~~~~~~
115 # escaped quoted string
116 if c == 'r':
117 pos += 1
118 token = 'string'
119 else:
120 token = 'template'
121 quote = program[pos:pos + 2]
122 s = pos = pos + 2
123 while pos < end: # find closing escaped quote
124 if program.startswith('\\\\\\', pos, end):
125 pos += 4 # skip over double escaped characters
126 continue
127 if program.startswith(quote, pos, end):
128 # interpret as if it were a part of an outer string
129 data = parser.unescapestr(program[s:pos])
130 if token == 'template':
131 data = _parsetemplate(data, 0, len(data))[0]
132 yield (token, data, s)
133 pos += 1
134 break
135 pos += 1
136 else:
137 raise error.ParseError(_("unterminated string"), s)
138 elif c.isalnum() or c in '_':
139 s = pos
140 pos += 1
141 while pos < end: # find end of symbol
142 d = program[pos]
143 if not (d.isalnum() or d == "_"):
144 break
145 pos += 1
146 sym = program[s:pos]
147 yield ('symbol', sym, s)
148 pos -= 1
149 elif c == term:
150 yield ('end', None, pos)
151 return
152 else:
153 raise error.ParseError(_("syntax error"), pos)
154 pos += 1
155 if term:
156 raise error.ParseError(_("unterminated template expansion"), start)
157 yield ('end', None, pos)
158
159 def _parsetemplate(tmpl, start, stop, quote=''):
160 r"""
161 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
162 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
163 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
164 ([('string', 'foo'), ('symbol', 'bar')], 9)
165 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
166 ([('string', 'foo')], 4)
167 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
168 ([('string', 'foo"'), ('string', 'bar')], 9)
169 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
170 ([('string', 'foo\\')], 6)
171 """
172 parsed = []
173 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
174 if typ == 'string':
175 parsed.append((typ, val))
176 elif typ == 'template':
177 parsed.append(val)
178 elif typ == 'end':
179 return parsed, pos
180 else:
181 raise error.ProgrammingError('unexpected type: %s' % typ)
182 raise error.ProgrammingError('unterminated scanning of template')
183
184 def scantemplate(tmpl, raw=False):
185 r"""Scan (type, start, end) positions of outermost elements in template
186
187 If raw=True, a backslash is not taken as an escape character just like
188 r'' string in Python. Note that this is different from r'' literal in
189 template in that no template fragment can appear in r'', e.g. r'{foo}'
190 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
191 'foo'.
192
193 >>> list(scantemplate(b'foo{bar}"baz'))
194 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
195 >>> list(scantemplate(b'outer{"inner"}outer'))
196 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
197 >>> list(scantemplate(b'foo\\{escaped}'))
198 [('string', 0, 5), ('string', 5, 13)]
199 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
200 [('string', 0, 4), ('template', 4, 13)]
201 """
202 last = None
203 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
204 if last:
205 yield last + (pos,)
206 if typ == 'end':
207 return
208 else:
209 last = (typ, pos)
210 raise error.ProgrammingError('unterminated scanning of template')
211
212 def _scantemplate(tmpl, start, stop, quote='', raw=False):
213 """Parse template string into chunks of strings and template expressions"""
214 sepchars = '{' + quote
215 unescape = [parser.unescapestr, pycompat.identity][raw]
216 pos = start
217 p = parser.parser(elements)
218 try:
219 while pos < stop:
220 n = min((tmpl.find(c, pos, stop) for c in sepchars),
221 key=lambda n: (n < 0, n))
222 if n < 0:
223 yield ('string', unescape(tmpl[pos:stop]), pos)
224 pos = stop
225 break
226 c = tmpl[n:n + 1]
227 bs = 0 # count leading backslashes
228 if not raw:
229 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
230 if bs % 2 == 1:
231 # escaped (e.g. '\{', '\\\{', but not '\\{')
232 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
233 pos = n + 1
234 continue
235 if n > pos:
236 yield ('string', unescape(tmpl[pos:n]), pos)
237 if c == quote:
238 yield ('end', None, n + 1)
239 return
240
241 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
242 if not tmpl.startswith('}', pos):
243 raise error.ParseError(_("invalid token"), pos)
244 yield ('template', parseres, n)
245 pos += 1
246
247 if quote:
248 raise error.ParseError(_("unterminated string"), start)
249 except error.ParseError as inst:
250 if len(inst.args) > 1: # has location
251 loc = inst.args[1]
252 # Offset the caret location by the number of newlines before the
253 # location of the error, since we will replace one-char newlines
254 # with the two-char literal r'\n'.
255 offset = tmpl[:loc].count('\n')
256 tmpl = tmpl.replace('\n', br'\n')
257 # We want the caret to point to the place in the template that
258 # failed to parse, but in a hint we get a open paren at the
259 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
260 # to line up the caret with the location of the error.
261 inst.hint = (tmpl + '\n'
262 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
263 raise
264 yield ('end', None, pos)
265
266 def _unnesttemplatelist(tree):
267 """Expand list of templates to node tuple
268
269 >>> def f(tree):
270 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
271 >>> f((b'template', []))
272 (string '')
273 >>> f((b'template', [(b'string', b'foo')]))
274 (string 'foo')
275 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
276 (template
277 (string 'foo')
278 (symbol 'rev'))
279 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
280 (template
281 (symbol 'rev'))
282 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
283 (string 'foo')
284 """
285 if not isinstance(tree, tuple):
286 return tree
287 op = tree[0]
288 if op != 'template':
289 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
290
291 assert len(tree) == 2
292 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
293 if not xs:
294 return ('string', '') # empty template ""
295 elif len(xs) == 1 and xs[0][0] == 'string':
296 return xs[0] # fast path for string with no template fragment "x"
297 else:
298 return (op,) + xs
299
300 def parse(tmpl):
301 """Parse template string into tree"""
302 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
303 assert pos == len(tmpl), 'unquoted template should be consumed'
304 return _unnesttemplatelist(('template', parsed))
305
306 def _parseexpr(expr):
307 """Parse a template expression into tree
308
309 >>> _parseexpr(b'"foo"')
310 ('string', 'foo')
311 >>> _parseexpr(b'foo(bar)')
312 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
313 >>> _parseexpr(b'foo(')
314 Traceback (most recent call last):
315 ...
316 ParseError: ('not a prefix: end', 4)
317 >>> _parseexpr(b'"foo" "bar"')
318 Traceback (most recent call last):
319 ...
320 ParseError: ('invalid token', 7)
321 """
322 p = parser.parser(elements)
323 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
324 if pos != len(expr):
325 raise error.ParseError(_('invalid token'), pos)
326 return _unnesttemplatelist(tree)
327
328 def prettyformat(tree):
329 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
330
331 def compileexp(exp, context, curmethods):
332 """Compile parsed template tree to (func, data) pair"""
333 if not exp:
334 raise error.ParseError(_("missing argument"))
335 t = exp[0]
336 if t in curmethods:
337 return curmethods[t](exp, context)
338 raise error.ParseError(_("unknown method '%s'") % t)
339
340 # template evaluation
341
342 def getsymbol(exp):
343 if exp[0] == 'symbol':
344 return exp[1]
345 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
346
347 def getlist(x):
348 if not x:
349 return []
350 if x[0] == 'list':
351 return getlist(x[1]) + [x[2]]
352 return [x]
353
354 def gettemplate(exp, context):
355 """Compile given template tree or load named template from map file;
356 returns (func, data) pair"""
357 if exp[0] in ('template', 'string'):
358 return compileexp(exp, context, methods)
359 if exp[0] == 'symbol':
360 # unlike runsymbol(), here 'symbol' is always taken as template name
361 # even if it exists in mapping. this allows us to override mapping
362 # by web templates, e.g. 'changelogtag' is redefined in map file.
363 return context._load(exp[1])
364 raise error.ParseError(_("expected template specifier"))
365
366 def _runrecursivesymbol(context, mapping, key):
367 raise error.Abort(_("recursive reference '%s' in template") % key)
368
369 def buildtemplate(exp, context):
370 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
371 return (templateutil.runtemplate, ctmpl)
372
373 def buildfilter(exp, context):
374 n = getsymbol(exp[2])
375 if n in context._filters:
376 filt = context._filters[n]
377 arg = compileexp(exp[1], context, methods)
378 return (templateutil.runfilter, (arg, filt))
379 if n in context._funcs:
380 f = context._funcs[n]
381 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
382 return (f, args)
383 raise error.ParseError(_("unknown function '%s'") % n)
384
385 def buildmap(exp, context):
386 darg = compileexp(exp[1], context, methods)
387 targ = gettemplate(exp[2], context)
388 return (templateutil.runmap, (darg, targ))
389
390 def buildmember(exp, context):
391 darg = compileexp(exp[1], context, methods)
392 memb = getsymbol(exp[2])
393 return (templateutil.runmember, (darg, memb))
394
395 def buildnegate(exp, context):
396 arg = compileexp(exp[1], context, exprmethods)
397 return (templateutil.runnegate, arg)
398
399 def buildarithmetic(exp, context, func):
400 left = compileexp(exp[1], context, exprmethods)
401 right = compileexp(exp[2], context, exprmethods)
402 return (templateutil.runarithmetic, (func, left, right))
403
404 def buildfunc(exp, context):
405 n = getsymbol(exp[1])
406 if n in context._funcs:
407 f = context._funcs[n]
408 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
409 return (f, args)
410 if n in context._filters:
411 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
412 if len(args) != 1:
413 raise error.ParseError(_("filter %s expects one argument") % n)
414 f = context._filters[n]
415 return (templateutil.runfilter, (args[0], f))
416 raise error.ParseError(_("unknown function '%s'") % n)
417
418 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
419 """Compile parsed tree of function arguments into list or dict of
420 (func, data) pairs
421
422 >>> context = engine(lambda t: (runsymbol, t))
423 >>> def fargs(expr, argspec):
424 ... x = _parseexpr(expr)
425 ... n = getsymbol(x[1])
426 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
427 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
428 ['l', 'k']
429 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
430 >>> list(args.keys()), list(args[b'opts'].keys())
431 (['opts'], ['opts', 'k'])
432 """
433 def compiledict(xs):
434 return util.sortdict((k, compileexp(x, context, curmethods))
435 for k, x in xs.iteritems())
436 def compilelist(xs):
437 return [compileexp(x, context, curmethods) for x in xs]
438
439 if not argspec:
440 # filter or function with no argspec: return list of positional args
441 return compilelist(getlist(exp))
442
443 # function with argspec: return dict of named args
444 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
445 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
446 keyvaluenode='keyvalue', keynode='symbol')
447 compargs = util.sortdict()
448 if varkey:
449 compargs[varkey] = compilelist(treeargs.pop(varkey))
450 if optkey:
451 compargs[optkey] = compiledict(treeargs.pop(optkey))
452 compargs.update(compiledict(treeargs))
453 return compargs
454
455 def buildkeyvaluepair(exp, content):
456 raise error.ParseError(_("can't use a key-value pair in this context"))
457
458 # dict of template built-in functions
39 # dict of template built-in functions
459 funcs = {}
40 funcs = {}
460
461 templatefunc = registrar.templatefunc(funcs)
41 templatefunc = registrar.templatefunc(funcs)
462
42
463 @templatefunc('date(date[, fmt])')
43 @templatefunc('date(date[, fmt])')
@@ -1074,366 +654,6 b' def word(context, mapping, args):'
1074 else:
654 else:
1075 return tokens[num]
655 return tokens[num]
1076
656
1077 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1078 exprmethods = {
1079 "integer": lambda e, c: (templateutil.runinteger, e[1]),
1080 "string": lambda e, c: (templateutil.runstring, e[1]),
1081 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
1082 "template": buildtemplate,
1083 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1084 ".": buildmember,
1085 "|": buildfilter,
1086 "%": buildmap,
1087 "func": buildfunc,
1088 "keyvalue": buildkeyvaluepair,
1089 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1090 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1091 "negate": buildnegate,
1092 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1093 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1094 }
1095
1096 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1097 methods = exprmethods.copy()
1098 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1099
1100 class _aliasrules(parser.basealiasrules):
1101 """Parsing and expansion rule set of template aliases"""
1102 _section = _('template alias')
1103 _parse = staticmethod(_parseexpr)
1104
1105 @staticmethod
1106 def _trygetfunc(tree):
1107 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1108 None"""
1109 if tree[0] == 'func' and tree[1][0] == 'symbol':
1110 return tree[1][1], getlist(tree[2])
1111 if tree[0] == '|' and tree[2][0] == 'symbol':
1112 return tree[2][1], [tree[1]]
1113
1114 def expandaliases(tree, aliases):
1115 """Return new tree of aliases are expanded"""
1116 aliasmap = _aliasrules.buildmap(aliases)
1117 return _aliasrules.expand(aliasmap, tree)
1118
1119 # template engine
1120
1121 def _flatten(thing):
1122 '''yield a single stream from a possibly nested set of iterators'''
1123 thing = templateutil.unwraphybrid(thing)
1124 if isinstance(thing, bytes):
1125 yield thing
1126 elif isinstance(thing, str):
1127 # We can only hit this on Python 3, and it's here to guard
1128 # against infinite recursion.
1129 raise error.ProgrammingError('Mercurial IO including templates is done'
1130 ' with bytes, not strings, got %r' % thing)
1131 elif thing is None:
1132 pass
1133 elif not util.safehasattr(thing, '__iter__'):
1134 yield pycompat.bytestr(thing)
1135 else:
1136 for i in thing:
1137 i = templateutil.unwraphybrid(i)
1138 if isinstance(i, bytes):
1139 yield i
1140 elif i is None:
1141 pass
1142 elif not util.safehasattr(i, '__iter__'):
1143 yield pycompat.bytestr(i)
1144 else:
1145 for j in _flatten(i):
1146 yield j
1147
1148 def unquotestring(s):
1149 '''unwrap quotes if any; otherwise returns unmodified string'''
1150 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1151 return s
1152 return s[1:-1]
1153
1154 class engine(object):
1155 '''template expansion engine.
1156
1157 template expansion works like this. a map file contains key=value
1158 pairs. if value is quoted, it is treated as string. otherwise, it
1159 is treated as name of template file.
1160
1161 templater is asked to expand a key in map. it looks up key, and
1162 looks for strings like this: {foo}. it expands {foo} by looking up
1163 foo in map, and substituting it. expansion is recursive: it stops
1164 when there is no more {foo} to replace.
1165
1166 expansion also allows formatting and filtering.
1167
1168 format uses key to expand each item in list. syntax is
1169 {key%format}.
1170
1171 filter uses function to transform value. syntax is
1172 {key|filter1|filter2|...}.'''
1173
1174 def __init__(self, loader, filters=None, defaults=None, resources=None,
1175 aliases=()):
1176 self._loader = loader
1177 if filters is None:
1178 filters = {}
1179 self._filters = filters
1180 self._funcs = funcs # make this a parameter if needed
1181 if defaults is None:
1182 defaults = {}
1183 if resources is None:
1184 resources = {}
1185 self._defaults = defaults
1186 self._resources = resources
1187 self._aliasmap = _aliasrules.buildmap(aliases)
1188 self._cache = {} # key: (func, data)
1189
1190 def symbol(self, mapping, key):
1191 """Resolve symbol to value or function; None if nothing found"""
1192 v = None
1193 if key not in self._resources:
1194 v = mapping.get(key)
1195 if v is None:
1196 v = self._defaults.get(key)
1197 return v
1198
1199 def resource(self, mapping, key):
1200 """Return internal data (e.g. cache) used for keyword/function
1201 evaluation"""
1202 v = None
1203 if key in self._resources:
1204 v = mapping.get(key)
1205 if v is None:
1206 v = self._resources.get(key)
1207 if v is None:
1208 raise templateutil.ResourceUnavailable(
1209 _('template resource not available: %s') % key)
1210 return v
1211
1212 def _load(self, t):
1213 '''load, parse, and cache a template'''
1214 if t not in self._cache:
1215 # put poison to cut recursion while compiling 't'
1216 self._cache[t] = (_runrecursivesymbol, t)
1217 try:
1218 x = parse(self._loader(t))
1219 if self._aliasmap:
1220 x = _aliasrules.expand(self._aliasmap, x)
1221 self._cache[t] = compileexp(x, self, methods)
1222 except: # re-raises
1223 del self._cache[t]
1224 raise
1225 return self._cache[t]
1226
1227 def process(self, t, mapping):
1228 '''Perform expansion. t is name of map element to expand.
1229 mapping contains added elements for use during expansion. Is a
1230 generator.'''
1231 func, data = self._load(t)
1232 return _flatten(func(self, mapping, data))
1233
1234 engines = {'default': engine}
1235
1236 def stylelist():
1237 paths = templatepaths()
1238 if not paths:
1239 return _('no templates found, try `hg debuginstall` for more info')
1240 dirlist = os.listdir(paths[0])
1241 stylelist = []
1242 for file in dirlist:
1243 split = file.split(".")
1244 if split[-1] in ('orig', 'rej'):
1245 continue
1246 if split[0] == "map-cmdline":
1247 stylelist.append(split[1])
1248 return ", ".join(sorted(stylelist))
1249
1250 def _readmapfile(mapfile):
1251 """Load template elements from the given map file"""
1252 if not os.path.exists(mapfile):
1253 raise error.Abort(_("style '%s' not found") % mapfile,
1254 hint=_("available styles: %s") % stylelist())
1255
1256 base = os.path.dirname(mapfile)
1257 conf = config.config(includepaths=templatepaths())
1258 conf.read(mapfile, remap={'': 'templates'})
1259
1260 cache = {}
1261 tmap = {}
1262 aliases = []
1263
1264 val = conf.get('templates', '__base__')
1265 if val and val[0] not in "'\"":
1266 # treat as a pointer to a base class for this style
1267 path = util.normpath(os.path.join(base, val))
1268
1269 # fallback check in template paths
1270 if not os.path.exists(path):
1271 for p in templatepaths():
1272 p2 = util.normpath(os.path.join(p, val))
1273 if os.path.isfile(p2):
1274 path = p2
1275 break
1276 p3 = util.normpath(os.path.join(p2, "map"))
1277 if os.path.isfile(p3):
1278 path = p3
1279 break
1280
1281 cache, tmap, aliases = _readmapfile(path)
1282
1283 for key, val in conf['templates'].items():
1284 if not val:
1285 raise error.ParseError(_('missing value'),
1286 conf.source('templates', key))
1287 if val[0] in "'\"":
1288 if val[0] != val[-1]:
1289 raise error.ParseError(_('unmatched quotes'),
1290 conf.source('templates', key))
1291 cache[key] = unquotestring(val)
1292 elif key != '__base__':
1293 val = 'default', val
1294 if ':' in val[1]:
1295 val = val[1].split(':', 1)
1296 tmap[key] = val[0], os.path.join(base, val[1])
1297 aliases.extend(conf['templatealias'].items())
1298 return cache, tmap, aliases
1299
1300 class templater(object):
1301
1302 def __init__(self, filters=None, defaults=None, resources=None,
1303 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1304 """Create template engine optionally with preloaded template fragments
1305
1306 - ``filters``: a dict of functions to transform a value into another.
1307 - ``defaults``: a dict of symbol values/functions; may be overridden
1308 by a ``mapping`` dict.
1309 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1310 from user template; may be overridden by a ``mapping`` dict.
1311 - ``cache``: a dict of preloaded template fragments.
1312 - ``aliases``: a list of alias (name, replacement) pairs.
1313
1314 self.cache may be updated later to register additional template
1315 fragments.
1316 """
1317 if filters is None:
1318 filters = {}
1319 if defaults is None:
1320 defaults = {}
1321 if resources is None:
1322 resources = {}
1323 if cache is None:
1324 cache = {}
1325 self.cache = cache.copy()
1326 self.map = {}
1327 self.filters = templatefilters.filters.copy()
1328 self.filters.update(filters)
1329 self.defaults = defaults
1330 self._resources = {'templ': self}
1331 self._resources.update(resources)
1332 self._aliases = aliases
1333 self.minchunk, self.maxchunk = minchunk, maxchunk
1334 self.ecache = {}
1335
1336 @classmethod
1337 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1338 cache=None, minchunk=1024, maxchunk=65536):
1339 """Create templater from the specified map file"""
1340 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1341 cache, tmap, aliases = _readmapfile(mapfile)
1342 t.cache.update(cache)
1343 t.map = tmap
1344 t._aliases = aliases
1345 return t
1346
1347 def __contains__(self, key):
1348 return key in self.cache or key in self.map
1349
1350 def load(self, t):
1351 '''Get the template for the given template name. Use a local cache.'''
1352 if t not in self.cache:
1353 try:
1354 self.cache[t] = util.readfile(self.map[t][1])
1355 except KeyError as inst:
1356 raise templateutil.TemplateNotFound(
1357 _('"%s" not in template map') % inst.args[0])
1358 except IOError as inst:
1359 reason = (_('template file %s: %s')
1360 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1361 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1362 return self.cache[t]
1363
1364 def render(self, mapping):
1365 """Render the default unnamed template and return result as string"""
1366 mapping = pycompat.strkwargs(mapping)
1367 return templateutil.stringify(self('', **mapping))
1368
1369 def __call__(self, t, **mapping):
1370 mapping = pycompat.byteskwargs(mapping)
1371 ttype = t in self.map and self.map[t][0] or 'default'
1372 if ttype not in self.ecache:
1373 try:
1374 ecls = engines[ttype]
1375 except KeyError:
1376 raise error.Abort(_('invalid template engine: %s') % ttype)
1377 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1378 self._resources, self._aliases)
1379 proc = self.ecache[ttype]
1380
1381 stream = proc.process(t, mapping)
1382 if self.minchunk:
1383 stream = util.increasingchunks(stream, min=self.minchunk,
1384 max=self.maxchunk)
1385 return stream
1386
1387 def templatepaths():
1388 '''return locations used for template files.'''
1389 pathsrel = ['templates']
1390 paths = [os.path.normpath(os.path.join(util.datapath, f))
1391 for f in pathsrel]
1392 return [p for p in paths if os.path.isdir(p)]
1393
1394 def templatepath(name):
1395 '''return location of template file. returns None if not found.'''
1396 for p in templatepaths():
1397 f = os.path.join(p, name)
1398 if os.path.exists(f):
1399 return f
1400 return None
1401
1402 def stylemap(styles, paths=None):
1403 """Return path to mapfile for a given style.
1404
1405 Searches mapfile in the following locations:
1406 1. templatepath/style/map
1407 2. templatepath/map-style
1408 3. templatepath/map
1409 """
1410
1411 if paths is None:
1412 paths = templatepaths()
1413 elif isinstance(paths, bytes):
1414 paths = [paths]
1415
1416 if isinstance(styles, bytes):
1417 styles = [styles]
1418
1419 for style in styles:
1420 # only plain name is allowed to honor template paths
1421 if (not style
1422 or style in (pycompat.oscurdir, pycompat.ospardir)
1423 or pycompat.ossep in style
1424 or pycompat.osaltsep and pycompat.osaltsep in style):
1425 continue
1426 locations = [os.path.join(style, 'map'), 'map-' + style]
1427 locations.append('map')
1428
1429 for path in paths:
1430 for location in locations:
1431 mapfile = os.path.join(path, location)
1432 if os.path.isfile(mapfile):
1433 return style, mapfile
1434
1435 raise RuntimeError("No hgweb templates found in %r" % paths)
1436
1437 def loadfunction(ui, extname, registrarobj):
657 def loadfunction(ui, extname, registrarobj):
1438 """Load template function from specified registrarobj
658 """Load template function from specified registrarobj
1439 """
659 """
This diff has been collapsed as it changes many lines, (649 lines changed) Show them Hide them
@@ -8,36 +8,19 b''
8 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import os
10 import os
11 import re
12
11
13 from .i18n import _
12 from .i18n import _
14 from . import (
13 from . import (
15 color,
16 config,
14 config,
17 encoding,
15 encoding,
18 error,
16 error,
19 minirst,
20 obsutil,
21 parser,
17 parser,
22 pycompat,
18 pycompat,
23 registrar,
24 revset as revsetmod,
25 revsetlang,
26 scmutil,
27 templatefilters,
19 templatefilters,
28 templatekw,
20 templatefuncs,
29 templateutil,
21 templateutil,
30 util,
22 util,
31 )
23 )
32 from .utils import dateutil
33
34 evalrawexp = templateutil.evalrawexp
35 evalfuncarg = templateutil.evalfuncarg
36 evalboolean = templateutil.evalboolean
37 evalinteger = templateutil.evalinteger
38 evalstring = templateutil.evalstring
39 evalstringliteral = templateutil.evalstringliteral
40 evalastype = templateutil.evalastype
41
24
42 # template parsing
25 # template parsing
43
26
@@ -455,625 +438,6 b' def _buildfuncargs(exp, context, curmeth'
455 def buildkeyvaluepair(exp, content):
438 def buildkeyvaluepair(exp, content):
456 raise error.ParseError(_("can't use a key-value pair in this context"))
439 raise error.ParseError(_("can't use a key-value pair in this context"))
457
440
458 # dict of template built-in functions
459 funcs = {}
460
461 templatefunc = registrar.templatefunc(funcs)
462
463 @templatefunc('date(date[, fmt])')
464 def date(context, mapping, args):
465 """Format a date. See :hg:`help dates` for formatting
466 strings. The default is a Unix date format, including the timezone:
467 "Mon Sep 04 15:13:13 2006 0700"."""
468 if not (1 <= len(args) <= 2):
469 # i18n: "date" is a keyword
470 raise error.ParseError(_("date expects one or two arguments"))
471
472 date = evalfuncarg(context, mapping, args[0])
473 fmt = None
474 if len(args) == 2:
475 fmt = evalstring(context, mapping, args[1])
476 try:
477 if fmt is None:
478 return dateutil.datestr(date)
479 else:
480 return dateutil.datestr(date, fmt)
481 except (TypeError, ValueError):
482 # i18n: "date" is a keyword
483 raise error.ParseError(_("date expects a date information"))
484
485 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
486 def dict_(context, mapping, args):
487 """Construct a dict from key-value pairs. A key may be omitted if
488 a value expression can provide an unambiguous name."""
489 data = util.sortdict()
490
491 for v in args['args']:
492 k = templateutil.findsymbolicname(v)
493 if not k:
494 raise error.ParseError(_('dict key cannot be inferred'))
495 if k in data or k in args['kwargs']:
496 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
497 data[k] = evalfuncarg(context, mapping, v)
498
499 data.update((k, evalfuncarg(context, mapping, v))
500 for k, v in args['kwargs'].iteritems())
501 return templateutil.hybriddict(data)
502
503 @templatefunc('diff([includepattern [, excludepattern]])')
504 def diff(context, mapping, args):
505 """Show a diff, optionally
506 specifying files to include or exclude."""
507 if len(args) > 2:
508 # i18n: "diff" is a keyword
509 raise error.ParseError(_("diff expects zero, one, or two arguments"))
510
511 def getpatterns(i):
512 if i < len(args):
513 s = evalstring(context, mapping, args[i]).strip()
514 if s:
515 return [s]
516 return []
517
518 ctx = context.resource(mapping, 'ctx')
519 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
520
521 return ''.join(chunks)
522
523 @templatefunc('extdata(source)', argspec='source')
524 def extdata(context, mapping, args):
525 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
526 if 'source' not in args:
527 # i18n: "extdata" is a keyword
528 raise error.ParseError(_('extdata expects one argument'))
529
530 source = evalstring(context, mapping, args['source'])
531 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
532 ctx = context.resource(mapping, 'ctx')
533 if source in cache:
534 data = cache[source]
535 else:
536 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
537 return data.get(ctx.rev(), '')
538
539 @templatefunc('files(pattern)')
540 def files(context, mapping, args):
541 """All files of the current changeset matching the pattern. See
542 :hg:`help patterns`."""
543 if not len(args) == 1:
544 # i18n: "files" is a keyword
545 raise error.ParseError(_("files expects one argument"))
546
547 raw = evalstring(context, mapping, args[0])
548 ctx = context.resource(mapping, 'ctx')
549 m = ctx.match([raw])
550 files = list(ctx.matches(m))
551 return templateutil.compatlist(context, mapping, "file", files)
552
553 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
554 def fill(context, mapping, args):
555 """Fill many
556 paragraphs with optional indentation. See the "fill" filter."""
557 if not (1 <= len(args) <= 4):
558 # i18n: "fill" is a keyword
559 raise error.ParseError(_("fill expects one to four arguments"))
560
561 text = evalstring(context, mapping, args[0])
562 width = 76
563 initindent = ''
564 hangindent = ''
565 if 2 <= len(args) <= 4:
566 width = evalinteger(context, mapping, args[1],
567 # i18n: "fill" is a keyword
568 _("fill expects an integer width"))
569 try:
570 initindent = evalstring(context, mapping, args[2])
571 hangindent = evalstring(context, mapping, args[3])
572 except IndexError:
573 pass
574
575 return templatefilters.fill(text, width, initindent, hangindent)
576
577 @templatefunc('formatnode(node)')
578 def formatnode(context, mapping, args):
579 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
580 if len(args) != 1:
581 # i18n: "formatnode" is a keyword
582 raise error.ParseError(_("formatnode expects one argument"))
583
584 ui = context.resource(mapping, 'ui')
585 node = evalstring(context, mapping, args[0])
586 if ui.debugflag:
587 return node
588 return templatefilters.short(node)
589
590 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
591 argspec='text width fillchar left')
592 def pad(context, mapping, args):
593 """Pad text with a
594 fill character."""
595 if 'text' not in args or 'width' not in args:
596 # i18n: "pad" is a keyword
597 raise error.ParseError(_("pad() expects two to four arguments"))
598
599 width = evalinteger(context, mapping, args['width'],
600 # i18n: "pad" is a keyword
601 _("pad() expects an integer width"))
602
603 text = evalstring(context, mapping, args['text'])
604
605 left = False
606 fillchar = ' '
607 if 'fillchar' in args:
608 fillchar = evalstring(context, mapping, args['fillchar'])
609 if len(color.stripeffects(fillchar)) != 1:
610 # i18n: "pad" is a keyword
611 raise error.ParseError(_("pad() expects a single fill character"))
612 if 'left' in args:
613 left = evalboolean(context, mapping, args['left'])
614
615 fillwidth = width - encoding.colwidth(color.stripeffects(text))
616 if fillwidth <= 0:
617 return text
618 if left:
619 return fillchar * fillwidth + text
620 else:
621 return text + fillchar * fillwidth
622
623 @templatefunc('indent(text, indentchars[, firstline])')
624 def indent(context, mapping, args):
625 """Indents all non-empty lines
626 with the characters given in the indentchars string. An optional
627 third parameter will override the indent for the first line only
628 if present."""
629 if not (2 <= len(args) <= 3):
630 # i18n: "indent" is a keyword
631 raise error.ParseError(_("indent() expects two or three arguments"))
632
633 text = evalstring(context, mapping, args[0])
634 indent = evalstring(context, mapping, args[1])
635
636 if len(args) == 3:
637 firstline = evalstring(context, mapping, args[2])
638 else:
639 firstline = indent
640
641 # the indent function doesn't indent the first line, so we do it here
642 return templatefilters.indent(firstline + text, indent)
643
644 @templatefunc('get(dict, key)')
645 def get(context, mapping, args):
646 """Get an attribute/key from an object. Some keywords
647 are complex types. This function allows you to obtain the value of an
648 attribute on these types."""
649 if len(args) != 2:
650 # i18n: "get" is a keyword
651 raise error.ParseError(_("get() expects two arguments"))
652
653 dictarg = evalfuncarg(context, mapping, args[0])
654 if not util.safehasattr(dictarg, 'get'):
655 # i18n: "get" is a keyword
656 raise error.ParseError(_("get() expects a dict as first argument"))
657
658 key = evalfuncarg(context, mapping, args[1])
659 return templateutil.getdictitem(dictarg, key)
660
661 @templatefunc('if(expr, then[, else])')
662 def if_(context, mapping, args):
663 """Conditionally execute based on the result of
664 an expression."""
665 if not (2 <= len(args) <= 3):
666 # i18n: "if" is a keyword
667 raise error.ParseError(_("if expects two or three arguments"))
668
669 test = evalboolean(context, mapping, args[0])
670 if test:
671 yield evalrawexp(context, mapping, args[1])
672 elif len(args) == 3:
673 yield evalrawexp(context, mapping, args[2])
674
675 @templatefunc('ifcontains(needle, haystack, then[, else])')
676 def ifcontains(context, mapping, args):
677 """Conditionally execute based
678 on whether the item "needle" is in "haystack"."""
679 if not (3 <= len(args) <= 4):
680 # i18n: "ifcontains" is a keyword
681 raise error.ParseError(_("ifcontains expects three or four arguments"))
682
683 haystack = evalfuncarg(context, mapping, args[1])
684 try:
685 needle = evalastype(context, mapping, args[0],
686 getattr(haystack, 'keytype', None) or bytes)
687 found = (needle in haystack)
688 except error.ParseError:
689 found = False
690
691 if found:
692 yield evalrawexp(context, mapping, args[2])
693 elif len(args) == 4:
694 yield evalrawexp(context, mapping, args[3])
695
696 @templatefunc('ifeq(expr1, expr2, then[, else])')
697 def ifeq(context, mapping, args):
698 """Conditionally execute based on
699 whether 2 items are equivalent."""
700 if not (3 <= len(args) <= 4):
701 # i18n: "ifeq" is a keyword
702 raise error.ParseError(_("ifeq expects three or four arguments"))
703
704 test = evalstring(context, mapping, args[0])
705 match = evalstring(context, mapping, args[1])
706 if test == match:
707 yield evalrawexp(context, mapping, args[2])
708 elif len(args) == 4:
709 yield evalrawexp(context, mapping, args[3])
710
711 @templatefunc('join(list, sep)')
712 def join(context, mapping, args):
713 """Join items in a list with a delimiter."""
714 if not (1 <= len(args) <= 2):
715 # i18n: "join" is a keyword
716 raise error.ParseError(_("join expects one or two arguments"))
717
718 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
719 # abuses generator as a keyword that returns a list of dicts.
720 joinset = evalrawexp(context, mapping, args[0])
721 joinset = templateutil.unwrapvalue(joinset)
722 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
723 joiner = " "
724 if len(args) > 1:
725 joiner = evalstring(context, mapping, args[1])
726
727 first = True
728 for x in pycompat.maybebytestr(joinset):
729 if first:
730 first = False
731 else:
732 yield joiner
733 yield joinfmt(x)
734
735 @templatefunc('label(label, expr)')
736 def label(context, mapping, args):
737 """Apply a label to generated content. Content with
738 a label applied can result in additional post-processing, such as
739 automatic colorization."""
740 if len(args) != 2:
741 # i18n: "label" is a keyword
742 raise error.ParseError(_("label expects two arguments"))
743
744 ui = context.resource(mapping, 'ui')
745 thing = evalstring(context, mapping, args[1])
746 # preserve unknown symbol as literal so effects like 'red', 'bold',
747 # etc. don't need to be quoted
748 label = evalstringliteral(context, mapping, args[0])
749
750 return ui.label(thing, label)
751
752 @templatefunc('latesttag([pattern])')
753 def latesttag(context, mapping, args):
754 """The global tags matching the given pattern on the
755 most recent globally tagged ancestor of this changeset.
756 If no such tags exist, the "{tag}" template resolves to
757 the string "null"."""
758 if len(args) > 1:
759 # i18n: "latesttag" is a keyword
760 raise error.ParseError(_("latesttag expects at most one argument"))
761
762 pattern = None
763 if len(args) == 1:
764 pattern = evalstring(context, mapping, args[0])
765 return templatekw.showlatesttags(context, mapping, pattern)
766
767 @templatefunc('localdate(date[, tz])')
768 def localdate(context, mapping, args):
769 """Converts a date to the specified timezone.
770 The default is local date."""
771 if not (1 <= len(args) <= 2):
772 # i18n: "localdate" is a keyword
773 raise error.ParseError(_("localdate expects one or two arguments"))
774
775 date = evalfuncarg(context, mapping, args[0])
776 try:
777 date = dateutil.parsedate(date)
778 except AttributeError: # not str nor date tuple
779 # i18n: "localdate" is a keyword
780 raise error.ParseError(_("localdate expects a date information"))
781 if len(args) >= 2:
782 tzoffset = None
783 tz = evalfuncarg(context, mapping, args[1])
784 if isinstance(tz, bytes):
785 tzoffset, remainder = dateutil.parsetimezone(tz)
786 if remainder:
787 tzoffset = None
788 if tzoffset is None:
789 try:
790 tzoffset = int(tz)
791 except (TypeError, ValueError):
792 # i18n: "localdate" is a keyword
793 raise error.ParseError(_("localdate expects a timezone"))
794 else:
795 tzoffset = dateutil.makedate()[1]
796 return (date[0], tzoffset)
797
798 @templatefunc('max(iterable)')
799 def max_(context, mapping, args, **kwargs):
800 """Return the max of an iterable"""
801 if len(args) != 1:
802 # i18n: "max" is a keyword
803 raise error.ParseError(_("max expects one argument"))
804
805 iterable = evalfuncarg(context, mapping, args[0])
806 try:
807 x = max(pycompat.maybebytestr(iterable))
808 except (TypeError, ValueError):
809 # i18n: "max" is a keyword
810 raise error.ParseError(_("max first argument should be an iterable"))
811 return templateutil.wraphybridvalue(iterable, x, x)
812
813 @templatefunc('min(iterable)')
814 def min_(context, mapping, args, **kwargs):
815 """Return the min of an iterable"""
816 if len(args) != 1:
817 # i18n: "min" is a keyword
818 raise error.ParseError(_("min expects one argument"))
819
820 iterable = evalfuncarg(context, mapping, args[0])
821 try:
822 x = min(pycompat.maybebytestr(iterable))
823 except (TypeError, ValueError):
824 # i18n: "min" is a keyword
825 raise error.ParseError(_("min first argument should be an iterable"))
826 return templateutil.wraphybridvalue(iterable, x, x)
827
828 @templatefunc('mod(a, b)')
829 def mod(context, mapping, args):
830 """Calculate a mod b such that a / b + a mod b == a"""
831 if not len(args) == 2:
832 # i18n: "mod" is a keyword
833 raise error.ParseError(_("mod expects two arguments"))
834
835 func = lambda a, b: a % b
836 return templateutil.runarithmetic(context, mapping,
837 (func, args[0], args[1]))
838
839 @templatefunc('obsfateoperations(markers)')
840 def obsfateoperations(context, mapping, args):
841 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
842 if len(args) != 1:
843 # i18n: "obsfateoperations" is a keyword
844 raise error.ParseError(_("obsfateoperations expects one argument"))
845
846 markers = evalfuncarg(context, mapping, args[0])
847
848 try:
849 data = obsutil.markersoperations(markers)
850 return templateutil.hybridlist(data, name='operation')
851 except (TypeError, KeyError):
852 # i18n: "obsfateoperations" is a keyword
853 errmsg = _("obsfateoperations first argument should be an iterable")
854 raise error.ParseError(errmsg)
855
856 @templatefunc('obsfatedate(markers)')
857 def obsfatedate(context, mapping, args):
858 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
859 if len(args) != 1:
860 # i18n: "obsfatedate" is a keyword
861 raise error.ParseError(_("obsfatedate expects one argument"))
862
863 markers = evalfuncarg(context, mapping, args[0])
864
865 try:
866 data = obsutil.markersdates(markers)
867 return templateutil.hybridlist(data, name='date', fmt='%d %d')
868 except (TypeError, KeyError):
869 # i18n: "obsfatedate" is a keyword
870 errmsg = _("obsfatedate first argument should be an iterable")
871 raise error.ParseError(errmsg)
872
873 @templatefunc('obsfateusers(markers)')
874 def obsfateusers(context, mapping, args):
875 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
876 if len(args) != 1:
877 # i18n: "obsfateusers" is a keyword
878 raise error.ParseError(_("obsfateusers expects one argument"))
879
880 markers = evalfuncarg(context, mapping, args[0])
881
882 try:
883 data = obsutil.markersusers(markers)
884 return templateutil.hybridlist(data, name='user')
885 except (TypeError, KeyError, ValueError):
886 # i18n: "obsfateusers" is a keyword
887 msg = _("obsfateusers first argument should be an iterable of "
888 "obsmakers")
889 raise error.ParseError(msg)
890
891 @templatefunc('obsfateverb(successors, markers)')
892 def obsfateverb(context, mapping, args):
893 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
894 if len(args) != 2:
895 # i18n: "obsfateverb" is a keyword
896 raise error.ParseError(_("obsfateverb expects two arguments"))
897
898 successors = evalfuncarg(context, mapping, args[0])
899 markers = evalfuncarg(context, mapping, args[1])
900
901 try:
902 return obsutil.obsfateverb(successors, markers)
903 except TypeError:
904 # i18n: "obsfateverb" is a keyword
905 errmsg = _("obsfateverb first argument should be countable")
906 raise error.ParseError(errmsg)
907
908 @templatefunc('relpath(path)')
909 def relpath(context, mapping, args):
910 """Convert a repository-absolute path into a filesystem path relative to
911 the current working directory."""
912 if len(args) != 1:
913 # i18n: "relpath" is a keyword
914 raise error.ParseError(_("relpath expects one argument"))
915
916 repo = context.resource(mapping, 'ctx').repo()
917 path = evalstring(context, mapping, args[0])
918 return repo.pathto(path)
919
920 @templatefunc('revset(query[, formatargs...])')
921 def revset(context, mapping, args):
922 """Execute a revision set query. See
923 :hg:`help revset`."""
924 if not len(args) > 0:
925 # i18n: "revset" is a keyword
926 raise error.ParseError(_("revset expects one or more arguments"))
927
928 raw = evalstring(context, mapping, args[0])
929 ctx = context.resource(mapping, 'ctx')
930 repo = ctx.repo()
931
932 def query(expr):
933 m = revsetmod.match(repo.ui, expr, repo=repo)
934 return m(repo)
935
936 if len(args) > 1:
937 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
938 revs = query(revsetlang.formatspec(raw, *formatargs))
939 revs = list(revs)
940 else:
941 cache = context.resource(mapping, 'cache')
942 revsetcache = cache.setdefault("revsetcache", {})
943 if raw in revsetcache:
944 revs = revsetcache[raw]
945 else:
946 revs = query(raw)
947 revs = list(revs)
948 revsetcache[raw] = revs
949 return templatekw.showrevslist(context, mapping, "revision", revs)
950
951 @templatefunc('rstdoc(text, style)')
952 def rstdoc(context, mapping, args):
953 """Format reStructuredText."""
954 if len(args) != 2:
955 # i18n: "rstdoc" is a keyword
956 raise error.ParseError(_("rstdoc expects two arguments"))
957
958 text = evalstring(context, mapping, args[0])
959 style = evalstring(context, mapping, args[1])
960
961 return minirst.format(text, style=style, keep=['verbose'])
962
963 @templatefunc('separate(sep, args)', argspec='sep *args')
964 def separate(context, mapping, args):
965 """Add a separator between non-empty arguments."""
966 if 'sep' not in args:
967 # i18n: "separate" is a keyword
968 raise error.ParseError(_("separate expects at least one argument"))
969
970 sep = evalstring(context, mapping, args['sep'])
971 first = True
972 for arg in args['args']:
973 argstr = evalstring(context, mapping, arg)
974 if not argstr:
975 continue
976 if first:
977 first = False
978 else:
979 yield sep
980 yield argstr
981
982 @templatefunc('shortest(node, minlength=4)')
983 def shortest(context, mapping, args):
984 """Obtain the shortest representation of
985 a node."""
986 if not (1 <= len(args) <= 2):
987 # i18n: "shortest" is a keyword
988 raise error.ParseError(_("shortest() expects one or two arguments"))
989
990 node = evalstring(context, mapping, args[0])
991
992 minlength = 4
993 if len(args) > 1:
994 minlength = evalinteger(context, mapping, args[1],
995 # i18n: "shortest" is a keyword
996 _("shortest() expects an integer minlength"))
997
998 # _partialmatch() of filtered changelog could take O(len(repo)) time,
999 # which would be unacceptably slow. so we look for hash collision in
1000 # unfiltered space, which means some hashes may be slightly longer.
1001 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1002 return cl.shortest(node, minlength)
1003
1004 @templatefunc('strip(text[, chars])')
1005 def strip(context, mapping, args):
1006 """Strip characters from a string. By default,
1007 strips all leading and trailing whitespace."""
1008 if not (1 <= len(args) <= 2):
1009 # i18n: "strip" is a keyword
1010 raise error.ParseError(_("strip expects one or two arguments"))
1011
1012 text = evalstring(context, mapping, args[0])
1013 if len(args) == 2:
1014 chars = evalstring(context, mapping, args[1])
1015 return text.strip(chars)
1016 return text.strip()
1017
1018 @templatefunc('sub(pattern, replacement, expression)')
1019 def sub(context, mapping, args):
1020 """Perform text substitution
1021 using regular expressions."""
1022 if len(args) != 3:
1023 # i18n: "sub" is a keyword
1024 raise error.ParseError(_("sub expects three arguments"))
1025
1026 pat = evalstring(context, mapping, args[0])
1027 rpl = evalstring(context, mapping, args[1])
1028 src = evalstring(context, mapping, args[2])
1029 try:
1030 patre = re.compile(pat)
1031 except re.error:
1032 # i18n: "sub" is a keyword
1033 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1034 try:
1035 yield patre.sub(rpl, src)
1036 except re.error:
1037 # i18n: "sub" is a keyword
1038 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1039
1040 @templatefunc('startswith(pattern, text)')
1041 def startswith(context, mapping, args):
1042 """Returns the value from the "text" argument
1043 if it begins with the content from the "pattern" argument."""
1044 if len(args) != 2:
1045 # i18n: "startswith" is a keyword
1046 raise error.ParseError(_("startswith expects two arguments"))
1047
1048 patn = evalstring(context, mapping, args[0])
1049 text = evalstring(context, mapping, args[1])
1050 if text.startswith(patn):
1051 return text
1052 return ''
1053
1054 @templatefunc('word(number, text[, separator])')
1055 def word(context, mapping, args):
1056 """Return the nth word from a string."""
1057 if not (2 <= len(args) <= 3):
1058 # i18n: "word" is a keyword
1059 raise error.ParseError(_("word expects two or three arguments, got %d")
1060 % len(args))
1061
1062 num = evalinteger(context, mapping, args[0],
1063 # i18n: "word" is a keyword
1064 _("word expects an integer index"))
1065 text = evalstring(context, mapping, args[1])
1066 if len(args) == 3:
1067 splitter = evalstring(context, mapping, args[2])
1068 else:
1069 splitter = None
1070
1071 tokens = text.split(splitter)
1072 if num >= len(tokens) or num < -len(tokens):
1073 return ''
1074 else:
1075 return tokens[num]
1076
1077 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
441 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1078 exprmethods = {
442 exprmethods = {
1079 "integer": lambda e, c: (templateutil.runinteger, e[1]),
443 "integer": lambda e, c: (templateutil.runinteger, e[1]),
@@ -1177,7 +541,7 b' class engine(object):'
1177 if filters is None:
541 if filters is None:
1178 filters = {}
542 filters = {}
1179 self._filters = filters
543 self._filters = filters
1180 self._funcs = funcs # make this a parameter if needed
544 self._funcs = templatefuncs.funcs # make this a parameter if needed
1181 if defaults is None:
545 if defaults is None:
1182 defaults = {}
546 defaults = {}
1183 if resources is None:
547 if resources is None:
@@ -1433,12 +797,3 b' def stylemap(styles, paths=None):'
1433 return style, mapfile
797 return style, mapfile
1434
798
1435 raise RuntimeError("No hgweb templates found in %r" % paths)
799 raise RuntimeError("No hgweb templates found in %r" % paths)
1436
1437 def loadfunction(ui, extname, registrarobj):
1438 """Load template function from specified registrarobj
1439 """
1440 for name, func in registrarobj._table.iteritems():
1441 funcs[name] = func
1442
1443 # tell hggettext to extract docstrings from these functions:
1444 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now