##// END OF EJS Templates
templater: add get() function to access dict element (e.g. extra)
Benoit Boissinot -
r18582:ef78450c default
parent child Browse files
Show More
@@ -1,98 +1,100 b''
1 Mercurial allows you to customize output of commands through
1 Mercurial allows you to customize output of commands through
2 templates. You can either pass in a template from the command
2 templates. You can either pass in a template from the command
3 line, via the --template option, or select an existing
3 line, via the --template option, or select an existing
4 template-style (--style).
4 template-style (--style).
5
5
6 You can customize output for any "log-like" command: log,
6 You can customize output for any "log-like" command: log,
7 outgoing, incoming, tip, parents, heads and glog.
7 outgoing, incoming, tip, parents, heads and glog.
8
8
9 Four styles are packaged with Mercurial: default (the style used
9 Four styles are packaged with Mercurial: default (the style used
10 when no explicit preference is passed), compact, changelog,
10 when no explicit preference is passed), compact, changelog,
11 and xml.
11 and xml.
12 Usage::
12 Usage::
13
13
14 $ hg log -r1 --style changelog
14 $ hg log -r1 --style changelog
15
15
16 A template is a piece of text, with markup to invoke variable
16 A template is a piece of text, with markup to invoke variable
17 expansion::
17 expansion::
18
18
19 $ hg log -r1 --template "{node}\n"
19 $ hg log -r1 --template "{node}\n"
20 b56ce7b07c52de7d5fd79fb89701ea538af65746
20 b56ce7b07c52de7d5fd79fb89701ea538af65746
21
21
22 Strings in curly braces are called keywords. The availability of
22 Strings in curly braces are called keywords. The availability of
23 keywords depends on the exact context of the templater. These
23 keywords depends on the exact context of the templater. These
24 keywords are usually available for templating a log-like command:
24 keywords are usually available for templating a log-like command:
25
25
26 .. keywordsmarker
26 .. keywordsmarker
27
27
28 The "date" keyword does not produce human-readable output. If you
28 The "date" keyword does not produce human-readable output. If you
29 want to use a date in your output, you can use a filter to process
29 want to use a date in your output, you can use a filter to process
30 it. Filters are functions which return a string based on the input
30 it. Filters are functions which return a string based on the input
31 variable. Be sure to use the stringify filter first when you're
31 variable. Be sure to use the stringify filter first when you're
32 applying a string-input filter to a list-like input variable.
32 applying a string-input filter to a list-like input variable.
33 You can also use a chain of filters to get the desired output::
33 You can also use a chain of filters to get the desired output::
34
34
35 $ hg tip --template "{date|isodate}\n"
35 $ hg tip --template "{date|isodate}\n"
36 2008-08-21 18:22 +0000
36 2008-08-21 18:22 +0000
37
37
38 List of filters:
38 List of filters:
39
39
40 .. filtersmarker
40 .. filtersmarker
41
41
42 Note that a filter is nothing more than a function call, i.e.
42 Note that a filter is nothing more than a function call, i.e.
43 ``expr|filter`` is equivalent to ``filter(expr)``.
43 ``expr|filter`` is equivalent to ``filter(expr)``.
44
44
45 In addition to filters, there are some basic built-in functions:
45 In addition to filters, there are some basic built-in functions:
46
46
47 - date(date[, fmt])
48
49 - fill(text[, width])
50
51 - get(dict, key)
52
47 - if(expr, then[, else])
53 - if(expr, then[, else])
48
54
49 - ifeq(expr, expr, then[, else])
55 - ifeq(expr, expr, then[, else])
50
56
51 - sub(pat, repl, expr)
52
53 - join(list, sep)
57 - join(list, sep)
54
58
55 - label(label, expr)
59 - label(label, expr)
56
60
57 - date(date[, fmt])
61 - sub(pat, repl, expr)
58
59 - fill(text[, width])
60
62
61 Also, for any expression that returns a list, there is a list operator:
63 Also, for any expression that returns a list, there is a list operator:
62
64
63 - expr % "{template}"
65 - expr % "{template}"
64
66
65 Some sample command line templates:
67 Some sample command line templates:
66
68
67 - Format lists, e.g. files::
69 - Format lists, e.g. files::
68
70
69 $ hg log -r 0 --template "files:\n{files % ' {file}\n'}"
71 $ hg log -r 0 --template "files:\n{files % ' {file}\n'}"
70
72
71 - Join the list of files with a ", "::
73 - Join the list of files with a ", "::
72
74
73 $ hg log -r 0 --template "files: {join(files, ', ')}\n"
75 $ hg log -r 0 --template "files: {join(files, ', ')}\n"
74
76
75 - Format date::
77 - Format date::
76
78
77 $ hg log -r 0 --template "{date(date, '%Y')}\n"
79 $ hg log -r 0 --template "{date(date, '%Y')}\n"
78
80
79 - Output the description set to a fill-width of 30::
81 - Output the description set to a fill-width of 30::
80
82
81 $ hg log -r 0 --template "{fill(desc, '30')}"
83 $ hg log -r 0 --template "{fill(desc, '30')}"
82
84
83 - Use a conditional to test for the default branch::
85 - Use a conditional to test for the default branch::
84
86
85 $ hg log -r 0 --template "{ifeq(branch, 'default', 'on the main branch',
87 $ hg log -r 0 --template "{ifeq(branch, 'default', 'on the main branch',
86 'on branch {branch}')}\n"
88 'on branch {branch}')}\n"
87
89
88 - Append a newline if not empty::
90 - Append a newline if not empty::
89
91
90 $ hg tip --template "{if(author, '{author}\n')}"
92 $ hg tip --template "{if(author, '{author}\n')}"
91
93
92 - Label the output for use with the color extension::
94 - Label the output for use with the color extension::
93
95
94 $ hg log -r 0 --template "{label('changeset.{phase}', node|short)}\n"
96 $ hg log -r 0 --template "{label('changeset.{phase}', node|short)}\n"
95
97
96 - Invert the firstline filter, i.e. everything but the first line::
98 - Invert the firstline filter, i.e. everything but the first line::
97
99
98 $ hg log -r 0 --template "{sub(r'^.*\n?\n?', '', desc)}\n"
100 $ hg log -r 0 --template "{sub(r'^.*\n?\n?', '', desc)}\n"
@@ -1,491 +1,505 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 sys, os, re
9 import sys, os, re
10 import util, config, templatefilters, parser, error
10 import util, config, templatefilters, parser, error
11 import types
11 import types
12
12
13 # template parsing
13 # template parsing
14
14
15 elements = {
15 elements = {
16 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
17 ",": (2, None, ("list", 2)),
17 ",": (2, None, ("list", 2)),
18 "|": (5, None, ("|", 5)),
18 "|": (5, None, ("|", 5)),
19 "%": (6, None, ("%", 6)),
19 "%": (6, None, ("%", 6)),
20 ")": (0, None, None),
20 ")": (0, None, None),
21 "symbol": (0, ("symbol",), None),
21 "symbol": (0, ("symbol",), None),
22 "string": (0, ("string",), None),
22 "string": (0, ("string",), None),
23 "end": (0, None, None),
23 "end": (0, None, None),
24 }
24 }
25
25
26 def tokenizer(data):
26 def tokenizer(data):
27 program, start, end = data
27 program, start, end = data
28 pos = start
28 pos = start
29 while pos < end:
29 while pos < end:
30 c = program[pos]
30 c = program[pos]
31 if c.isspace(): # skip inter-token whitespace
31 if c.isspace(): # skip inter-token whitespace
32 pass
32 pass
33 elif c in "(,)%|": # handle simple operators
33 elif c in "(,)%|": # handle simple operators
34 yield (c, None, pos)
34 yield (c, None, pos)
35 elif (c in '"\'' or c == 'r' and
35 elif (c in '"\'' or c == 'r' and
36 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
36 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
37 if c == 'r':
37 if c == 'r':
38 pos += 1
38 pos += 1
39 c = program[pos]
39 c = program[pos]
40 decode = False
40 decode = False
41 else:
41 else:
42 decode = True
42 decode = True
43 pos += 1
43 pos += 1
44 s = pos
44 s = pos
45 while pos < end: # find closing quote
45 while pos < end: # find closing quote
46 d = program[pos]
46 d = program[pos]
47 if decode and d == '\\': # skip over escaped characters
47 if decode and d == '\\': # skip over escaped characters
48 pos += 2
48 pos += 2
49 continue
49 continue
50 if d == c:
50 if d == c:
51 if not decode:
51 if not decode:
52 yield ('string', program[s:pos].replace('\\', r'\\'), s)
52 yield ('string', program[s:pos].replace('\\', r'\\'), s)
53 break
53 break
54 yield ('string', program[s:pos].decode('string-escape'), s)
54 yield ('string', program[s:pos].decode('string-escape'), s)
55 break
55 break
56 pos += 1
56 pos += 1
57 else:
57 else:
58 raise error.ParseError(_("unterminated string"), s)
58 raise error.ParseError(_("unterminated string"), s)
59 elif c.isalnum() or c in '_':
59 elif c.isalnum() or c in '_':
60 s = pos
60 s = pos
61 pos += 1
61 pos += 1
62 while pos < end: # find end of symbol
62 while pos < end: # find end of symbol
63 d = program[pos]
63 d = program[pos]
64 if not (d.isalnum() or d == "_"):
64 if not (d.isalnum() or d == "_"):
65 break
65 break
66 pos += 1
66 pos += 1
67 sym = program[s:pos]
67 sym = program[s:pos]
68 yield ('symbol', sym, s)
68 yield ('symbol', sym, s)
69 pos -= 1
69 pos -= 1
70 elif c == '}':
70 elif c == '}':
71 pos += 1
71 pos += 1
72 break
72 break
73 else:
73 else:
74 raise error.ParseError(_("syntax error"), pos)
74 raise error.ParseError(_("syntax error"), pos)
75 pos += 1
75 pos += 1
76 yield ('end', None, pos)
76 yield ('end', None, pos)
77
77
78 def compiletemplate(tmpl, context):
78 def compiletemplate(tmpl, context):
79 parsed = []
79 parsed = []
80 pos, stop = 0, len(tmpl)
80 pos, stop = 0, len(tmpl)
81 p = parser.parser(tokenizer, elements)
81 p = parser.parser(tokenizer, elements)
82
82
83 while pos < stop:
83 while pos < stop:
84 n = tmpl.find('{', pos)
84 n = tmpl.find('{', pos)
85 if n < 0:
85 if n < 0:
86 parsed.append(("string", tmpl[pos:]))
86 parsed.append(("string", tmpl[pos:]))
87 break
87 break
88 if n > 0 and tmpl[n - 1] == '\\':
88 if n > 0 and tmpl[n - 1] == '\\':
89 # escaped
89 # escaped
90 parsed.append(("string", tmpl[pos:n - 1] + "{"))
90 parsed.append(("string", tmpl[pos:n - 1] + "{"))
91 pos = n + 1
91 pos = n + 1
92 continue
92 continue
93 if n > pos:
93 if n > pos:
94 parsed.append(("string", tmpl[pos:n]))
94 parsed.append(("string", tmpl[pos:n]))
95
95
96 pd = [tmpl, n + 1, stop]
96 pd = [tmpl, n + 1, stop]
97 parseres, pos = p.parse(pd)
97 parseres, pos = p.parse(pd)
98 parsed.append(parseres)
98 parsed.append(parseres)
99
99
100 return [compileexp(e, context) for e in parsed]
100 return [compileexp(e, context) for e in parsed]
101
101
102 def compileexp(exp, context):
102 def compileexp(exp, context):
103 t = exp[0]
103 t = exp[0]
104 if t in methods:
104 if t in methods:
105 return methods[t](exp, context)
105 return methods[t](exp, context)
106 raise error.ParseError(_("unknown method '%s'") % t)
106 raise error.ParseError(_("unknown method '%s'") % t)
107
107
108 # template evaluation
108 # template evaluation
109
109
110 def getsymbol(exp):
110 def getsymbol(exp):
111 if exp[0] == 'symbol':
111 if exp[0] == 'symbol':
112 return exp[1]
112 return exp[1]
113 raise error.ParseError(_("expected a symbol"))
113 raise error.ParseError(_("expected a symbol"))
114
114
115 def getlist(x):
115 def getlist(x):
116 if not x:
116 if not x:
117 return []
117 return []
118 if x[0] == 'list':
118 if x[0] == 'list':
119 return getlist(x[1]) + [x[2]]
119 return getlist(x[1]) + [x[2]]
120 return [x]
120 return [x]
121
121
122 def getfilter(exp, context):
122 def getfilter(exp, context):
123 f = getsymbol(exp)
123 f = getsymbol(exp)
124 if f not in context._filters:
124 if f not in context._filters:
125 raise error.ParseError(_("unknown function '%s'") % f)
125 raise error.ParseError(_("unknown function '%s'") % f)
126 return context._filters[f]
126 return context._filters[f]
127
127
128 def gettemplate(exp, context):
128 def gettemplate(exp, context):
129 if exp[0] == 'string':
129 if exp[0] == 'string':
130 return compiletemplate(exp[1], context)
130 return compiletemplate(exp[1], context)
131 if exp[0] == 'symbol':
131 if exp[0] == 'symbol':
132 return context._load(exp[1])
132 return context._load(exp[1])
133 raise error.ParseError(_("expected template specifier"))
133 raise error.ParseError(_("expected template specifier"))
134
134
135 def runstring(context, mapping, data):
135 def runstring(context, mapping, data):
136 return data
136 return data
137
137
138 def runsymbol(context, mapping, key):
138 def runsymbol(context, mapping, key):
139 v = mapping.get(key)
139 v = mapping.get(key)
140 if v is None:
140 if v is None:
141 v = context._defaults.get(key, '')
141 v = context._defaults.get(key, '')
142 if util.safehasattr(v, '__call__'):
142 if util.safehasattr(v, '__call__'):
143 return v(**mapping)
143 return v(**mapping)
144 if isinstance(v, types.GeneratorType):
144 if isinstance(v, types.GeneratorType):
145 v = list(v)
145 v = list(v)
146 mapping[key] = v
146 mapping[key] = v
147 return v
147 return v
148 return v
148 return v
149
149
150 def buildfilter(exp, context):
150 def buildfilter(exp, context):
151 func, data = compileexp(exp[1], context)
151 func, data = compileexp(exp[1], context)
152 filt = getfilter(exp[2], context)
152 filt = getfilter(exp[2], context)
153 return (runfilter, (func, data, filt))
153 return (runfilter, (func, data, filt))
154
154
155 def runfilter(context, mapping, data):
155 def runfilter(context, mapping, data):
156 func, data, filt = data
156 func, data, filt = data
157 try:
157 try:
158 return filt(func(context, mapping, data))
158 return filt(func(context, mapping, data))
159 except (ValueError, AttributeError, TypeError):
159 except (ValueError, AttributeError, TypeError):
160 if isinstance(data, tuple):
160 if isinstance(data, tuple):
161 dt = data[1]
161 dt = data[1]
162 else:
162 else:
163 dt = data
163 dt = data
164 raise util.Abort(_("template filter '%s' is not compatible with "
164 raise util.Abort(_("template filter '%s' is not compatible with "
165 "keyword '%s'") % (filt.func_name, dt))
165 "keyword '%s'") % (filt.func_name, dt))
166
166
167 def buildmap(exp, context):
167 def buildmap(exp, context):
168 func, data = compileexp(exp[1], context)
168 func, data = compileexp(exp[1], context)
169 ctmpl = gettemplate(exp[2], context)
169 ctmpl = gettemplate(exp[2], context)
170 return (runmap, (func, data, ctmpl))
170 return (runmap, (func, data, ctmpl))
171
171
172 def runtemplate(context, mapping, template):
172 def runtemplate(context, mapping, template):
173 for func, data in template:
173 for func, data in template:
174 yield func(context, mapping, data)
174 yield func(context, mapping, data)
175
175
176 def runmap(context, mapping, data):
176 def runmap(context, mapping, data):
177 func, data, ctmpl = data
177 func, data, ctmpl = data
178 d = func(context, mapping, data)
178 d = func(context, mapping, data)
179 if util.safehasattr(d, '__call__'):
179 if util.safehasattr(d, '__call__'):
180 d = d()
180 d = d()
181
181
182 lm = mapping.copy()
182 lm = mapping.copy()
183
183
184 for i in d:
184 for i in d:
185 if isinstance(i, dict):
185 if isinstance(i, dict):
186 lm.update(i)
186 lm.update(i)
187 lm['originalnode'] = mapping.get('node')
187 lm['originalnode'] = mapping.get('node')
188 yield runtemplate(context, lm, ctmpl)
188 yield runtemplate(context, lm, ctmpl)
189 else:
189 else:
190 # v is not an iterable of dicts, this happen when 'key'
190 # v is not an iterable of dicts, this happen when 'key'
191 # has been fully expanded already and format is useless.
191 # has been fully expanded already and format is useless.
192 # If so, return the expanded value.
192 # If so, return the expanded value.
193 yield i
193 yield i
194
194
195 def buildfunc(exp, context):
195 def buildfunc(exp, context):
196 n = getsymbol(exp[1])
196 n = getsymbol(exp[1])
197 args = [compileexp(x, context) for x in getlist(exp[2])]
197 args = [compileexp(x, context) for x in getlist(exp[2])]
198 if n in funcs:
198 if n in funcs:
199 f = funcs[n]
199 f = funcs[n]
200 return (f, args)
200 return (f, args)
201 if n in templatefilters.funcs:
201 if n in templatefilters.funcs:
202 f = templatefilters.funcs[n]
202 f = templatefilters.funcs[n]
203 return (f, args)
203 return (f, args)
204 if n in context._filters:
204 if n in context._filters:
205 if len(args) != 1:
205 if len(args) != 1:
206 raise error.ParseError(_("filter %s expects one argument") % n)
206 raise error.ParseError(_("filter %s expects one argument") % n)
207 f = context._filters[n]
207 f = context._filters[n]
208 return (runfilter, (args[0][0], args[0][1], f))
208 return (runfilter, (args[0][0], args[0][1], f))
209
209
210 def get(context, mapping, args):
211 if len(args) != 2:
212 # i18n: "get" is a keyword
213 raise error.ParseError(_("get() expects two arguments"))
214
215 dictarg = args[0][0](context, mapping, args[0][1])
216 if not util.safehasattr(dictarg, 'get'):
217 # i18n: "get" is a keyword
218 raise error.ParseError(_("get() expects a dict as first argument"))
219
220 key = args[1][0](context, mapping, args[1][1])
221 yield dictarg.get(key)
222
210 def join(context, mapping, args):
223 def join(context, mapping, args):
211 if not (1 <= len(args) <= 2):
224 if not (1 <= len(args) <= 2):
212 # i18n: "join" is a keyword
225 # i18n: "join" is a keyword
213 raise error.ParseError(_("join expects one or two arguments"))
226 raise error.ParseError(_("join expects one or two arguments"))
214
227
215 joinset = args[0][0](context, mapping, args[0][1])
228 joinset = args[0][0](context, mapping, args[0][1])
216 if util.safehasattr(joinset, '__call__'):
229 if util.safehasattr(joinset, '__call__'):
217 joinset = [x.values()[0] for x in joinset()]
230 joinset = [x.values()[0] for x in joinset()]
218
231
219 joiner = " "
232 joiner = " "
220 if len(args) > 1:
233 if len(args) > 1:
221 joiner = args[1][0](context, mapping, args[1][1])
234 joiner = args[1][0](context, mapping, args[1][1])
222
235
223 first = True
236 first = True
224 for x in joinset:
237 for x in joinset:
225 if first:
238 if first:
226 first = False
239 first = False
227 else:
240 else:
228 yield joiner
241 yield joiner
229 yield x
242 yield x
230
243
231 def sub(context, mapping, args):
244 def sub(context, mapping, args):
232 if len(args) != 3:
245 if len(args) != 3:
233 # i18n: "sub" is a keyword
246 # i18n: "sub" is a keyword
234 raise error.ParseError(_("sub expects three arguments"))
247 raise error.ParseError(_("sub expects three arguments"))
235
248
236 pat = stringify(args[0][0](context, mapping, args[0][1]))
249 pat = stringify(args[0][0](context, mapping, args[0][1]))
237 rpl = stringify(args[1][0](context, mapping, args[1][1]))
250 rpl = stringify(args[1][0](context, mapping, args[1][1]))
238 src = stringify(args[2][0](context, mapping, args[2][1]))
251 src = stringify(args[2][0](context, mapping, args[2][1]))
239 yield re.sub(pat, rpl, src)
252 yield re.sub(pat, rpl, src)
240
253
241 def if_(context, mapping, args):
254 def if_(context, mapping, args):
242 if not (2 <= len(args) <= 3):
255 if not (2 <= len(args) <= 3):
243 # i18n: "if" is a keyword
256 # i18n: "if" is a keyword
244 raise error.ParseError(_("if expects two or three arguments"))
257 raise error.ParseError(_("if expects two or three arguments"))
245
258
246 test = stringify(args[0][0](context, mapping, args[0][1]))
259 test = stringify(args[0][0](context, mapping, args[0][1]))
247 if test:
260 if test:
248 t = stringify(args[1][0](context, mapping, args[1][1]))
261 t = stringify(args[1][0](context, mapping, args[1][1]))
249 yield runtemplate(context, mapping, compiletemplate(t, context))
262 yield runtemplate(context, mapping, compiletemplate(t, context))
250 elif len(args) == 3:
263 elif len(args) == 3:
251 t = stringify(args[2][0](context, mapping, args[2][1]))
264 t = stringify(args[2][0](context, mapping, args[2][1]))
252 yield runtemplate(context, mapping, compiletemplate(t, context))
265 yield runtemplate(context, mapping, compiletemplate(t, context))
253
266
254 def ifeq(context, mapping, args):
267 def ifeq(context, mapping, args):
255 if not (3 <= len(args) <= 4):
268 if not (3 <= len(args) <= 4):
256 # i18n: "ifeq" is a keyword
269 # i18n: "ifeq" is a keyword
257 raise error.ParseError(_("ifeq expects three or four arguments"))
270 raise error.ParseError(_("ifeq expects three or four arguments"))
258
271
259 test = stringify(args[0][0](context, mapping, args[0][1]))
272 test = stringify(args[0][0](context, mapping, args[0][1]))
260 match = stringify(args[1][0](context, mapping, args[1][1]))
273 match = stringify(args[1][0](context, mapping, args[1][1]))
261 if test == match:
274 if test == match:
262 t = stringify(args[2][0](context, mapping, args[2][1]))
275 t = stringify(args[2][0](context, mapping, args[2][1]))
263 yield runtemplate(context, mapping, compiletemplate(t, context))
276 yield runtemplate(context, mapping, compiletemplate(t, context))
264 elif len(args) == 4:
277 elif len(args) == 4:
265 t = stringify(args[3][0](context, mapping, args[3][1]))
278 t = stringify(args[3][0](context, mapping, args[3][1]))
266 yield runtemplate(context, mapping, compiletemplate(t, context))
279 yield runtemplate(context, mapping, compiletemplate(t, context))
267
280
268 def label(context, mapping, args):
281 def label(context, mapping, args):
269 if len(args) != 2:
282 if len(args) != 2:
270 # i18n: "label" is a keyword
283 # i18n: "label" is a keyword
271 raise error.ParseError(_("label expects two arguments"))
284 raise error.ParseError(_("label expects two arguments"))
272
285
273 # ignore args[0] (the label string) since this is supposed to be a a no-op
286 # ignore args[0] (the label string) since this is supposed to be a a no-op
274 t = stringify(args[1][0](context, mapping, args[1][1]))
287 t = stringify(args[1][0](context, mapping, args[1][1]))
275 yield runtemplate(context, mapping, compiletemplate(t, context))
288 yield runtemplate(context, mapping, compiletemplate(t, context))
276
289
277 methods = {
290 methods = {
278 "string": lambda e, c: (runstring, e[1]),
291 "string": lambda e, c: (runstring, e[1]),
279 "symbol": lambda e, c: (runsymbol, e[1]),
292 "symbol": lambda e, c: (runsymbol, e[1]),
280 "group": lambda e, c: compileexp(e[1], c),
293 "group": lambda e, c: compileexp(e[1], c),
281 # ".": buildmember,
294 # ".": buildmember,
282 "|": buildfilter,
295 "|": buildfilter,
283 "%": buildmap,
296 "%": buildmap,
284 "func": buildfunc,
297 "func": buildfunc,
285 }
298 }
286
299
287 funcs = {
300 funcs = {
301 "get": get,
288 "if": if_,
302 "if": if_,
289 "ifeq": ifeq,
303 "ifeq": ifeq,
290 "join": join,
304 "join": join,
305 "label": label,
291 "sub": sub,
306 "sub": sub,
292 "label": label,
293 }
307 }
294
308
295 # template engine
309 # template engine
296
310
297 path = ['templates', '../templates']
311 path = ['templates', '../templates']
298 stringify = templatefilters.stringify
312 stringify = templatefilters.stringify
299
313
300 def _flatten(thing):
314 def _flatten(thing):
301 '''yield a single stream from a possibly nested set of iterators'''
315 '''yield a single stream from a possibly nested set of iterators'''
302 if isinstance(thing, str):
316 if isinstance(thing, str):
303 yield thing
317 yield thing
304 elif not util.safehasattr(thing, '__iter__'):
318 elif not util.safehasattr(thing, '__iter__'):
305 if thing is not None:
319 if thing is not None:
306 yield str(thing)
320 yield str(thing)
307 else:
321 else:
308 for i in thing:
322 for i in thing:
309 if isinstance(i, str):
323 if isinstance(i, str):
310 yield i
324 yield i
311 elif not util.safehasattr(i, '__iter__'):
325 elif not util.safehasattr(i, '__iter__'):
312 if i is not None:
326 if i is not None:
313 yield str(i)
327 yield str(i)
314 elif i is not None:
328 elif i is not None:
315 for j in _flatten(i):
329 for j in _flatten(i):
316 yield j
330 yield j
317
331
318 def parsestring(s, quoted=True):
332 def parsestring(s, quoted=True):
319 '''parse a string using simple c-like syntax.
333 '''parse a string using simple c-like syntax.
320 string must be in quotes if quoted is True.'''
334 string must be in quotes if quoted is True.'''
321 if quoted:
335 if quoted:
322 if len(s) < 2 or s[0] != s[-1]:
336 if len(s) < 2 or s[0] != s[-1]:
323 raise SyntaxError(_('unmatched quotes'))
337 raise SyntaxError(_('unmatched quotes'))
324 return s[1:-1].decode('string_escape')
338 return s[1:-1].decode('string_escape')
325
339
326 return s.decode('string_escape')
340 return s.decode('string_escape')
327
341
328 class engine(object):
342 class engine(object):
329 '''template expansion engine.
343 '''template expansion engine.
330
344
331 template expansion works like this. a map file contains key=value
345 template expansion works like this. a map file contains key=value
332 pairs. if value is quoted, it is treated as string. otherwise, it
346 pairs. if value is quoted, it is treated as string. otherwise, it
333 is treated as name of template file.
347 is treated as name of template file.
334
348
335 templater is asked to expand a key in map. it looks up key, and
349 templater is asked to expand a key in map. it looks up key, and
336 looks for strings like this: {foo}. it expands {foo} by looking up
350 looks for strings like this: {foo}. it expands {foo} by looking up
337 foo in map, and substituting it. expansion is recursive: it stops
351 foo in map, and substituting it. expansion is recursive: it stops
338 when there is no more {foo} to replace.
352 when there is no more {foo} to replace.
339
353
340 expansion also allows formatting and filtering.
354 expansion also allows formatting and filtering.
341
355
342 format uses key to expand each item in list. syntax is
356 format uses key to expand each item in list. syntax is
343 {key%format}.
357 {key%format}.
344
358
345 filter uses function to transform value. syntax is
359 filter uses function to transform value. syntax is
346 {key|filter1|filter2|...}.'''
360 {key|filter1|filter2|...}.'''
347
361
348 def __init__(self, loader, filters={}, defaults={}):
362 def __init__(self, loader, filters={}, defaults={}):
349 self._loader = loader
363 self._loader = loader
350 self._filters = filters
364 self._filters = filters
351 self._defaults = defaults
365 self._defaults = defaults
352 self._cache = {}
366 self._cache = {}
353
367
354 def _load(self, t):
368 def _load(self, t):
355 '''load, parse, and cache a template'''
369 '''load, parse, and cache a template'''
356 if t not in self._cache:
370 if t not in self._cache:
357 self._cache[t] = compiletemplate(self._loader(t), self)
371 self._cache[t] = compiletemplate(self._loader(t), self)
358 return self._cache[t]
372 return self._cache[t]
359
373
360 def process(self, t, mapping):
374 def process(self, t, mapping):
361 '''Perform expansion. t is name of map element to expand.
375 '''Perform expansion. t is name of map element to expand.
362 mapping contains added elements for use during expansion. Is a
376 mapping contains added elements for use during expansion. Is a
363 generator.'''
377 generator.'''
364 return _flatten(runtemplate(self, mapping, self._load(t)))
378 return _flatten(runtemplate(self, mapping, self._load(t)))
365
379
366 engines = {'default': engine}
380 engines = {'default': engine}
367
381
368 class templater(object):
382 class templater(object):
369
383
370 def __init__(self, mapfile, filters={}, defaults={}, cache={},
384 def __init__(self, mapfile, filters={}, defaults={}, cache={},
371 minchunk=1024, maxchunk=65536):
385 minchunk=1024, maxchunk=65536):
372 '''set up template engine.
386 '''set up template engine.
373 mapfile is name of file to read map definitions from.
387 mapfile is name of file to read map definitions from.
374 filters is dict of functions. each transforms a value into another.
388 filters is dict of functions. each transforms a value into another.
375 defaults is dict of default map definitions.'''
389 defaults is dict of default map definitions.'''
376 self.mapfile = mapfile or 'template'
390 self.mapfile = mapfile or 'template'
377 self.cache = cache.copy()
391 self.cache = cache.copy()
378 self.map = {}
392 self.map = {}
379 self.base = (mapfile and os.path.dirname(mapfile)) or ''
393 self.base = (mapfile and os.path.dirname(mapfile)) or ''
380 self.filters = templatefilters.filters.copy()
394 self.filters = templatefilters.filters.copy()
381 self.filters.update(filters)
395 self.filters.update(filters)
382 self.defaults = defaults
396 self.defaults = defaults
383 self.minchunk, self.maxchunk = minchunk, maxchunk
397 self.minchunk, self.maxchunk = minchunk, maxchunk
384 self.ecache = {}
398 self.ecache = {}
385
399
386 if not mapfile:
400 if not mapfile:
387 return
401 return
388 if not os.path.exists(mapfile):
402 if not os.path.exists(mapfile):
389 raise util.Abort(_('style not found: %s') % mapfile)
403 raise util.Abort(_('style not found: %s') % mapfile)
390
404
391 conf = config.config()
405 conf = config.config()
392 conf.read(mapfile)
406 conf.read(mapfile)
393
407
394 for key, val in conf[''].items():
408 for key, val in conf[''].items():
395 if not val:
409 if not val:
396 raise SyntaxError(_('%s: missing value') % conf.source('', key))
410 raise SyntaxError(_('%s: missing value') % conf.source('', key))
397 if val[0] in "'\"":
411 if val[0] in "'\"":
398 try:
412 try:
399 self.cache[key] = parsestring(val)
413 self.cache[key] = parsestring(val)
400 except SyntaxError, inst:
414 except SyntaxError, inst:
401 raise SyntaxError('%s: %s' %
415 raise SyntaxError('%s: %s' %
402 (conf.source('', key), inst.args[0]))
416 (conf.source('', key), inst.args[0]))
403 else:
417 else:
404 val = 'default', val
418 val = 'default', val
405 if ':' in val[1]:
419 if ':' in val[1]:
406 val = val[1].split(':', 1)
420 val = val[1].split(':', 1)
407 self.map[key] = val[0], os.path.join(self.base, val[1])
421 self.map[key] = val[0], os.path.join(self.base, val[1])
408
422
409 def __contains__(self, key):
423 def __contains__(self, key):
410 return key in self.cache or key in self.map
424 return key in self.cache or key in self.map
411
425
412 def load(self, t):
426 def load(self, t):
413 '''Get the template for the given template name. Use a local cache.'''
427 '''Get the template for the given template name. Use a local cache.'''
414 if t not in self.cache:
428 if t not in self.cache:
415 try:
429 try:
416 self.cache[t] = util.readfile(self.map[t][1])
430 self.cache[t] = util.readfile(self.map[t][1])
417 except KeyError, inst:
431 except KeyError, inst:
418 raise util.Abort(_('"%s" not in template map') % inst.args[0])
432 raise util.Abort(_('"%s" not in template map') % inst.args[0])
419 except IOError, inst:
433 except IOError, inst:
420 raise IOError(inst.args[0], _('template file %s: %s') %
434 raise IOError(inst.args[0], _('template file %s: %s') %
421 (self.map[t][1], inst.args[1]))
435 (self.map[t][1], inst.args[1]))
422 return self.cache[t]
436 return self.cache[t]
423
437
424 def __call__(self, t, **mapping):
438 def __call__(self, t, **mapping):
425 ttype = t in self.map and self.map[t][0] or 'default'
439 ttype = t in self.map and self.map[t][0] or 'default'
426 if ttype not in self.ecache:
440 if ttype not in self.ecache:
427 self.ecache[ttype] = engines[ttype](self.load,
441 self.ecache[ttype] = engines[ttype](self.load,
428 self.filters, self.defaults)
442 self.filters, self.defaults)
429 proc = self.ecache[ttype]
443 proc = self.ecache[ttype]
430
444
431 stream = proc.process(t, mapping)
445 stream = proc.process(t, mapping)
432 if self.minchunk:
446 if self.minchunk:
433 stream = util.increasingchunks(stream, min=self.minchunk,
447 stream = util.increasingchunks(stream, min=self.minchunk,
434 max=self.maxchunk)
448 max=self.maxchunk)
435 return stream
449 return stream
436
450
437 def templatepath(name=None):
451 def templatepath(name=None):
438 '''return location of template file or directory (if no name).
452 '''return location of template file or directory (if no name).
439 returns None if not found.'''
453 returns None if not found.'''
440 normpaths = []
454 normpaths = []
441
455
442 # executable version (py2exe) doesn't support __file__
456 # executable version (py2exe) doesn't support __file__
443 if util.mainfrozen():
457 if util.mainfrozen():
444 module = sys.executable
458 module = sys.executable
445 else:
459 else:
446 module = __file__
460 module = __file__
447 for f in path:
461 for f in path:
448 if f.startswith('/'):
462 if f.startswith('/'):
449 p = f
463 p = f
450 else:
464 else:
451 fl = f.split('/')
465 fl = f.split('/')
452 p = os.path.join(os.path.dirname(module), *fl)
466 p = os.path.join(os.path.dirname(module), *fl)
453 if name:
467 if name:
454 p = os.path.join(p, name)
468 p = os.path.join(p, name)
455 if name and os.path.exists(p):
469 if name and os.path.exists(p):
456 return os.path.normpath(p)
470 return os.path.normpath(p)
457 elif os.path.isdir(p):
471 elif os.path.isdir(p):
458 normpaths.append(os.path.normpath(p))
472 normpaths.append(os.path.normpath(p))
459
473
460 return normpaths
474 return normpaths
461
475
462 def stylemap(styles, paths=None):
476 def stylemap(styles, paths=None):
463 """Return path to mapfile for a given style.
477 """Return path to mapfile for a given style.
464
478
465 Searches mapfile in the following locations:
479 Searches mapfile in the following locations:
466 1. templatepath/style/map
480 1. templatepath/style/map
467 2. templatepath/map-style
481 2. templatepath/map-style
468 3. templatepath/map
482 3. templatepath/map
469 """
483 """
470
484
471 if paths is None:
485 if paths is None:
472 paths = templatepath()
486 paths = templatepath()
473 elif isinstance(paths, str):
487 elif isinstance(paths, str):
474 paths = [paths]
488 paths = [paths]
475
489
476 if isinstance(styles, str):
490 if isinstance(styles, str):
477 styles = [styles]
491 styles = [styles]
478
492
479 for style in styles:
493 for style in styles:
480 if not style:
494 if not style:
481 continue
495 continue
482 locations = [os.path.join(style, 'map'), 'map-' + style]
496 locations = [os.path.join(style, 'map'), 'map-' + style]
483 locations.append('map')
497 locations.append('map')
484
498
485 for path in paths:
499 for path in paths:
486 for location in locations:
500 for location in locations:
487 mapfile = os.path.join(path, location)
501 mapfile = os.path.join(path, location)
488 if os.path.isfile(mapfile):
502 if os.path.isfile(mapfile):
489 return style, mapfile
503 return style, mapfile
490
504
491 raise RuntimeError("No hgweb templates found in %r" % paths)
505 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now