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