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