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