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