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