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