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