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