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