##// END OF EJS Templates
template: fix shortest(node) function in pure mercurial...
Durham Goode -
r20371:6f3fb6a9 default
parent child Browse files
Show More
@@ -1,655 +1,662 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], s)
55 yield ('string', program[s:pos], 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 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:].decode("string-escape")))
86 parsed.append(("string", tmpl[pos:].decode("string-escape")))
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",
90 parsed.append(("string",
91 (tmpl[pos:n - 1] + "{").decode("string-escape")))
91 (tmpl[pos:n - 1] + "{").decode("string-escape")))
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].decode("string-escape")))
95 parsed.append(("string", tmpl[pos:n].decode("string-escape")))
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 v is None:
143 if v is None:
144 try:
144 try:
145 v = context.process(key, mapping)
145 v = context.process(key, mapping)
146 except TemplateNotFound:
146 except TemplateNotFound:
147 v = ''
147 v = ''
148 if util.safehasattr(v, '__call__'):
148 if util.safehasattr(v, '__call__'):
149 return v(**mapping)
149 return v(**mapping)
150 if isinstance(v, types.GeneratorType):
150 if isinstance(v, types.GeneratorType):
151 v = list(v)
151 v = list(v)
152 mapping[key] = v
152 mapping[key] = v
153 return v
153 return v
154 return v
154 return v
155
155
156 def buildfilter(exp, context):
156 def buildfilter(exp, context):
157 func, data = compileexp(exp[1], context)
157 func, data = compileexp(exp[1], context)
158 filt = getfilter(exp[2], context)
158 filt = getfilter(exp[2], context)
159 return (runfilter, (func, data, filt))
159 return (runfilter, (func, data, filt))
160
160
161 def runfilter(context, mapping, data):
161 def runfilter(context, mapping, data):
162 func, data, filt = data
162 func, data, filt = data
163 try:
163 try:
164 return filt(func(context, mapping, data))
164 return filt(func(context, mapping, data))
165 except (ValueError, AttributeError, TypeError):
165 except (ValueError, AttributeError, TypeError):
166 if isinstance(data, tuple):
166 if isinstance(data, tuple):
167 dt = data[1]
167 dt = data[1]
168 else:
168 else:
169 dt = data
169 dt = data
170 raise util.Abort(_("template filter '%s' is not compatible with "
170 raise util.Abort(_("template filter '%s' is not compatible with "
171 "keyword '%s'") % (filt.func_name, dt))
171 "keyword '%s'") % (filt.func_name, dt))
172
172
173 def buildmap(exp, context):
173 def buildmap(exp, context):
174 func, data = compileexp(exp[1], context)
174 func, data = compileexp(exp[1], context)
175 ctmpl = gettemplate(exp[2], context)
175 ctmpl = gettemplate(exp[2], context)
176 return (runmap, (func, data, ctmpl))
176 return (runmap, (func, data, ctmpl))
177
177
178 def runtemplate(context, mapping, template):
178 def runtemplate(context, mapping, template):
179 for func, data in template:
179 for func, data in template:
180 yield func(context, mapping, data)
180 yield func(context, mapping, data)
181
181
182 def runmap(context, mapping, data):
182 def runmap(context, mapping, data):
183 func, data, ctmpl = data
183 func, data, ctmpl = data
184 d = func(context, mapping, data)
184 d = func(context, mapping, data)
185 if util.safehasattr(d, '__call__'):
185 if util.safehasattr(d, '__call__'):
186 d = d()
186 d = d()
187
187
188 lm = mapping.copy()
188 lm = mapping.copy()
189
189
190 for i in d:
190 for i in d:
191 if isinstance(i, dict):
191 if isinstance(i, dict):
192 lm.update(i)
192 lm.update(i)
193 lm['originalnode'] = mapping.get('node')
193 lm['originalnode'] = mapping.get('node')
194 yield runtemplate(context, lm, ctmpl)
194 yield runtemplate(context, lm, ctmpl)
195 else:
195 else:
196 # v is not an iterable of dicts, this happen when 'key'
196 # v is not an iterable of dicts, this happen when 'key'
197 # has been fully expanded already and format is useless.
197 # has been fully expanded already and format is useless.
198 # If so, return the expanded value.
198 # If so, return the expanded value.
199 yield i
199 yield i
200
200
201 def buildfunc(exp, context):
201 def buildfunc(exp, context):
202 n = getsymbol(exp[1])
202 n = getsymbol(exp[1])
203 args = [compileexp(x, context) for x in getlist(exp[2])]
203 args = [compileexp(x, context) for x in getlist(exp[2])]
204 if n in funcs:
204 if n in funcs:
205 f = funcs[n]
205 f = funcs[n]
206 return (f, args)
206 return (f, args)
207 if n in context._filters:
207 if n in context._filters:
208 if len(args) != 1:
208 if len(args) != 1:
209 raise error.ParseError(_("filter %s expects one argument") % n)
209 raise error.ParseError(_("filter %s expects one argument") % n)
210 f = context._filters[n]
210 f = context._filters[n]
211 return (runfilter, (args[0][0], args[0][1], f))
211 return (runfilter, (args[0][0], args[0][1], f))
212
212
213 def date(context, mapping, args):
213 def date(context, mapping, args):
214 if not (1 <= len(args) <= 2):
214 if not (1 <= len(args) <= 2):
215 raise error.ParseError(_("date expects one or two arguments"))
215 raise error.ParseError(_("date expects one or two arguments"))
216
216
217 date = args[0][0](context, mapping, args[0][1])
217 date = args[0][0](context, mapping, args[0][1])
218 if len(args) == 2:
218 if len(args) == 2:
219 fmt = stringify(args[1][0](context, mapping, args[1][1]))
219 fmt = stringify(args[1][0](context, mapping, args[1][1]))
220 return util.datestr(date, fmt)
220 return util.datestr(date, fmt)
221 return util.datestr(date)
221 return util.datestr(date)
222
222
223 def fill(context, mapping, args):
223 def fill(context, mapping, args):
224 if not (1 <= len(args) <= 4):
224 if not (1 <= len(args) <= 4):
225 raise error.ParseError(_("fill expects one to four arguments"))
225 raise error.ParseError(_("fill expects one to four arguments"))
226
226
227 text = stringify(args[0][0](context, mapping, args[0][1]))
227 text = stringify(args[0][0](context, mapping, args[0][1]))
228 width = 76
228 width = 76
229 initindent = ''
229 initindent = ''
230 hangindent = ''
230 hangindent = ''
231 if 2 <= len(args) <= 4:
231 if 2 <= len(args) <= 4:
232 try:
232 try:
233 width = int(stringify(args[1][0](context, mapping, args[1][1])))
233 width = int(stringify(args[1][0](context, mapping, args[1][1])))
234 except ValueError:
234 except ValueError:
235 raise error.ParseError(_("fill expects an integer width"))
235 raise error.ParseError(_("fill expects an integer width"))
236 try:
236 try:
237 initindent = stringify(args[2][0](context, mapping, args[2][1]))
237 initindent = stringify(args[2][0](context, mapping, args[2][1]))
238 initindent = stringify(runtemplate(context, mapping,
238 initindent = stringify(runtemplate(context, mapping,
239 compiletemplate(initindent, context)))
239 compiletemplate(initindent, context)))
240 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
240 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
241 hangindent = stringify(runtemplate(context, mapping,
241 hangindent = stringify(runtemplate(context, mapping,
242 compiletemplate(hangindent, context)))
242 compiletemplate(hangindent, context)))
243 except IndexError:
243 except IndexError:
244 pass
244 pass
245
245
246 return templatefilters.fill(text, width, initindent, hangindent)
246 return templatefilters.fill(text, width, initindent, hangindent)
247
247
248 def pad(context, mapping, args):
248 def pad(context, mapping, args):
249 """usage: pad(text, width, fillchar=' ', right=False)
249 """usage: pad(text, width, fillchar=' ', right=False)
250 """
250 """
251 if not (2 <= len(args) <= 4):
251 if not (2 <= len(args) <= 4):
252 raise error.ParseError(_("pad() expects two to four arguments"))
252 raise error.ParseError(_("pad() expects two to four arguments"))
253
253
254 width = int(args[1][1])
254 width = int(args[1][1])
255
255
256 text = stringify(args[0][0](context, mapping, args[0][1]))
256 text = stringify(args[0][0](context, mapping, args[0][1]))
257 if args[0][0] == runstring:
257 if args[0][0] == runstring:
258 text = stringify(runtemplate(context, mapping,
258 text = stringify(runtemplate(context, mapping,
259 compiletemplate(text, context)))
259 compiletemplate(text, context)))
260
260
261 right = False
261 right = False
262 fillchar = ' '
262 fillchar = ' '
263 if len(args) > 2:
263 if len(args) > 2:
264 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
264 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
265 if len(args) > 3:
265 if len(args) > 3:
266 right = util.parsebool(args[3][1])
266 right = util.parsebool(args[3][1])
267
267
268 if right:
268 if right:
269 return text.rjust(width, fillchar)
269 return text.rjust(width, fillchar)
270 else:
270 else:
271 return text.ljust(width, fillchar)
271 return text.ljust(width, fillchar)
272
272
273 def get(context, mapping, args):
273 def get(context, mapping, args):
274 if len(args) != 2:
274 if len(args) != 2:
275 # i18n: "get" is a keyword
275 # i18n: "get" is a keyword
276 raise error.ParseError(_("get() expects two arguments"))
276 raise error.ParseError(_("get() expects two arguments"))
277
277
278 dictarg = args[0][0](context, mapping, args[0][1])
278 dictarg = args[0][0](context, mapping, args[0][1])
279 if not util.safehasattr(dictarg, 'get'):
279 if not util.safehasattr(dictarg, 'get'):
280 # i18n: "get" is a keyword
280 # i18n: "get" is a keyword
281 raise error.ParseError(_("get() expects a dict as first argument"))
281 raise error.ParseError(_("get() expects a dict as first argument"))
282
282
283 key = args[1][0](context, mapping, args[1][1])
283 key = args[1][0](context, mapping, args[1][1])
284 yield dictarg.get(key)
284 yield dictarg.get(key)
285
285
286 def _evalifliteral(arg, context, mapping):
286 def _evalifliteral(arg, context, mapping):
287 t = stringify(arg[0](context, mapping, arg[1]))
287 t = stringify(arg[0](context, mapping, arg[1]))
288 if arg[0] == runstring:
288 if arg[0] == runstring:
289 yield runtemplate(context, mapping, compiletemplate(t, context))
289 yield runtemplate(context, mapping, compiletemplate(t, context))
290 else:
290 else:
291 yield t
291 yield t
292
292
293 def if_(context, mapping, args):
293 def if_(context, mapping, args):
294 if not (2 <= len(args) <= 3):
294 if not (2 <= len(args) <= 3):
295 # i18n: "if" is a keyword
295 # i18n: "if" is a keyword
296 raise error.ParseError(_("if expects two or three arguments"))
296 raise error.ParseError(_("if expects two or three arguments"))
297
297
298 test = stringify(args[0][0](context, mapping, args[0][1]))
298 test = stringify(args[0][0](context, mapping, args[0][1]))
299 if test:
299 if test:
300 yield _evalifliteral(args[1], context, mapping)
300 yield _evalifliteral(args[1], context, mapping)
301 elif len(args) == 3:
301 elif len(args) == 3:
302 yield _evalifliteral(args[2], context, mapping)
302 yield _evalifliteral(args[2], context, mapping)
303
303
304 def ifeq(context, mapping, args):
304 def ifeq(context, mapping, args):
305 if not (3 <= len(args) <= 4):
305 if not (3 <= len(args) <= 4):
306 # i18n: "ifeq" is a keyword
306 # i18n: "ifeq" is a keyword
307 raise error.ParseError(_("ifeq expects three or four arguments"))
307 raise error.ParseError(_("ifeq expects three or four arguments"))
308
308
309 test = stringify(args[0][0](context, mapping, args[0][1]))
309 test = stringify(args[0][0](context, mapping, args[0][1]))
310 match = stringify(args[1][0](context, mapping, args[1][1]))
310 match = stringify(args[1][0](context, mapping, args[1][1]))
311 if test == match:
311 if test == match:
312 yield _evalifliteral(args[2], context, mapping)
312 yield _evalifliteral(args[2], context, mapping)
313 elif len(args) == 4:
313 elif len(args) == 4:
314 yield _evalifliteral(args[3], context, mapping)
314 yield _evalifliteral(args[3], context, mapping)
315
315
316 def join(context, mapping, args):
316 def join(context, mapping, args):
317 if not (1 <= len(args) <= 2):
317 if not (1 <= len(args) <= 2):
318 # i18n: "join" is a keyword
318 # i18n: "join" is a keyword
319 raise error.ParseError(_("join expects one or two arguments"))
319 raise error.ParseError(_("join expects one or two arguments"))
320
320
321 joinset = args[0][0](context, mapping, args[0][1])
321 joinset = args[0][0](context, mapping, args[0][1])
322 if util.safehasattr(joinset, '__call__'):
322 if util.safehasattr(joinset, '__call__'):
323 jf = joinset.joinfmt
323 jf = joinset.joinfmt
324 joinset = [jf(x) for x in joinset()]
324 joinset = [jf(x) for x in joinset()]
325
325
326 joiner = " "
326 joiner = " "
327 if len(args) > 1:
327 if len(args) > 1:
328 joiner = args[1][0](context, mapping, args[1][1])
328 joiner = args[1][0](context, mapping, args[1][1])
329
329
330 first = True
330 first = True
331 for x in joinset:
331 for x in joinset:
332 if first:
332 if first:
333 first = False
333 first = False
334 else:
334 else:
335 yield joiner
335 yield joiner
336 yield x
336 yield x
337
337
338 def label(context, mapping, args):
338 def label(context, mapping, args):
339 if len(args) != 2:
339 if len(args) != 2:
340 # i18n: "label" is a keyword
340 # i18n: "label" is a keyword
341 raise error.ParseError(_("label expects two arguments"))
341 raise error.ParseError(_("label expects two arguments"))
342
342
343 # ignore args[0] (the label string) since this is supposed to be a a no-op
343 # ignore args[0] (the label string) since this is supposed to be a a no-op
344 yield _evalifliteral(args[1], context, mapping)
344 yield _evalifliteral(args[1], context, mapping)
345
345
346 def rstdoc(context, mapping, args):
346 def rstdoc(context, mapping, args):
347 if len(args) != 2:
347 if len(args) != 2:
348 # i18n: "rstdoc" is a keyword
348 # i18n: "rstdoc" is a keyword
349 raise error.ParseError(_("rstdoc expects two arguments"))
349 raise error.ParseError(_("rstdoc expects two arguments"))
350
350
351 text = stringify(args[0][0](context, mapping, args[0][1]))
351 text = stringify(args[0][0](context, mapping, args[0][1]))
352 style = stringify(args[1][0](context, mapping, args[1][1]))
352 style = stringify(args[1][0](context, mapping, args[1][1]))
353
353
354 return minirst.format(text, style=style, keep=['verbose'])
354 return minirst.format(text, style=style, keep=['verbose'])
355
355
356 def shortest(context, mapping, args):
356 def shortest(context, mapping, args):
357 """usage: shortest(node, minlength=4)
357 """usage: shortest(node, minlength=4)
358 """
358 """
359 if not (1 <= len(args) <= 2):
359 if not (1 <= len(args) <= 2):
360 raise error.ParseError(_("shortest() expects one or two arguments"))
360 raise error.ParseError(_("shortest() expects one or two arguments"))
361
361
362 node = stringify(args[0][0](context, mapping, args[0][1]))
362 node = stringify(args[0][0](context, mapping, args[0][1]))
363
363
364 minlength = 4
364 minlength = 4
365 if len(args) > 1:
365 if len(args) > 1:
366 minlength = int(args[1][1])
366 minlength = int(args[1][1])
367
367
368 cl = mapping['ctx']._repo.changelog
368 cl = mapping['ctx']._repo.changelog
369 def isvalid(test):
369 def isvalid(test):
370 try:
370 try:
371 cl.index.partialmatch(test)
371 try:
372 cl.index.partialmatch(test)
373 except AttributeError:
374 # Pure mercurial doesn't support partialmatch on the index.
375 # Fallback to the slow way.
376 if cl._partialmatch(test) is None:
377 return False
378
372 try:
379 try:
373 int(test)
380 int(test)
374 return False
381 return False
375 except ValueError:
382 except ValueError:
376 return True
383 return True
377 except error.RevlogError:
384 except error.RevlogError:
378 return False
385 return False
379
386
380 shortest = node
387 shortest = node
381 startlength = max(6, minlength)
388 startlength = max(6, minlength)
382 length = startlength
389 length = startlength
383 while True:
390 while True:
384 test = node[:length]
391 test = node[:length]
385 if isvalid(test):
392 if isvalid(test):
386 shortest = test
393 shortest = test
387 if length == minlength or length > startlength:
394 if length == minlength or length > startlength:
388 return shortest
395 return shortest
389 length -= 1
396 length -= 1
390 else:
397 else:
391 length += 1
398 length += 1
392 if len(shortest) <= length:
399 if len(shortest) <= length:
393 return shortest
400 return shortest
394
401
395 def strip(context, mapping, args):
402 def strip(context, mapping, args):
396 if not (1 <= len(args) <= 2):
403 if not (1 <= len(args) <= 2):
397 raise error.ParseError(_("strip expects one or two arguments"))
404 raise error.ParseError(_("strip expects one or two arguments"))
398
405
399 text = args[0][0](context, mapping, args[0][1])
406 text = args[0][0](context, mapping, args[0][1])
400 if len(args) == 2:
407 if len(args) == 2:
401 chars = args[1][0](context, mapping, args[1][1])
408 chars = args[1][0](context, mapping, args[1][1])
402 return text.strip(chars)
409 return text.strip(chars)
403 return text.strip()
410 return text.strip()
404
411
405 def sub(context, mapping, args):
412 def sub(context, mapping, args):
406 if len(args) != 3:
413 if len(args) != 3:
407 # i18n: "sub" is a keyword
414 # i18n: "sub" is a keyword
408 raise error.ParseError(_("sub expects three arguments"))
415 raise error.ParseError(_("sub expects three arguments"))
409
416
410 pat = stringify(args[0][0](context, mapping, args[0][1]))
417 pat = stringify(args[0][0](context, mapping, args[0][1]))
411 rpl = stringify(args[1][0](context, mapping, args[1][1]))
418 rpl = stringify(args[1][0](context, mapping, args[1][1]))
412 src = stringify(args[2][0](context, mapping, args[2][1]))
419 src = stringify(args[2][0](context, mapping, args[2][1]))
413 src = stringify(runtemplate(context, mapping,
420 src = stringify(runtemplate(context, mapping,
414 compiletemplate(src, context)))
421 compiletemplate(src, context)))
415 yield re.sub(pat, rpl, src)
422 yield re.sub(pat, rpl, src)
416
423
417 methods = {
424 methods = {
418 "string": lambda e, c: (runstring, e[1]),
425 "string": lambda e, c: (runstring, e[1]),
419 "symbol": lambda e, c: (runsymbol, e[1]),
426 "symbol": lambda e, c: (runsymbol, e[1]),
420 "group": lambda e, c: compileexp(e[1], c),
427 "group": lambda e, c: compileexp(e[1], c),
421 # ".": buildmember,
428 # ".": buildmember,
422 "|": buildfilter,
429 "|": buildfilter,
423 "%": buildmap,
430 "%": buildmap,
424 "func": buildfunc,
431 "func": buildfunc,
425 }
432 }
426
433
427 funcs = {
434 funcs = {
428 "date": date,
435 "date": date,
429 "fill": fill,
436 "fill": fill,
430 "get": get,
437 "get": get,
431 "if": if_,
438 "if": if_,
432 "ifeq": ifeq,
439 "ifeq": ifeq,
433 "join": join,
440 "join": join,
434 "label": label,
441 "label": label,
435 "pad": pad,
442 "pad": pad,
436 "rstdoc": rstdoc,
443 "rstdoc": rstdoc,
437 "shortest": shortest,
444 "shortest": shortest,
438 "strip": strip,
445 "strip": strip,
439 "sub": sub,
446 "sub": sub,
440 }
447 }
441
448
442 # template engine
449 # template engine
443
450
444 path = ['templates', '../templates']
451 path = ['templates', '../templates']
445 stringify = templatefilters.stringify
452 stringify = templatefilters.stringify
446
453
447 def _flatten(thing):
454 def _flatten(thing):
448 '''yield a single stream from a possibly nested set of iterators'''
455 '''yield a single stream from a possibly nested set of iterators'''
449 if isinstance(thing, str):
456 if isinstance(thing, str):
450 yield thing
457 yield thing
451 elif not util.safehasattr(thing, '__iter__'):
458 elif not util.safehasattr(thing, '__iter__'):
452 if thing is not None:
459 if thing is not None:
453 yield str(thing)
460 yield str(thing)
454 else:
461 else:
455 for i in thing:
462 for i in thing:
456 if isinstance(i, str):
463 if isinstance(i, str):
457 yield i
464 yield i
458 elif not util.safehasattr(i, '__iter__'):
465 elif not util.safehasattr(i, '__iter__'):
459 if i is not None:
466 if i is not None:
460 yield str(i)
467 yield str(i)
461 elif i is not None:
468 elif i is not None:
462 for j in _flatten(i):
469 for j in _flatten(i):
463 yield j
470 yield j
464
471
465 def parsestring(s, quoted=True):
472 def parsestring(s, quoted=True):
466 '''parse a string using simple c-like syntax.
473 '''parse a string using simple c-like syntax.
467 string must be in quotes if quoted is True.'''
474 string must be in quotes if quoted is True.'''
468 if quoted:
475 if quoted:
469 if len(s) < 2 or s[0] != s[-1]:
476 if len(s) < 2 or s[0] != s[-1]:
470 raise SyntaxError(_('unmatched quotes'))
477 raise SyntaxError(_('unmatched quotes'))
471 return s[1:-1].decode('string_escape')
478 return s[1:-1].decode('string_escape')
472
479
473 return s.decode('string_escape')
480 return s.decode('string_escape')
474
481
475 class engine(object):
482 class engine(object):
476 '''template expansion engine.
483 '''template expansion engine.
477
484
478 template expansion works like this. a map file contains key=value
485 template expansion works like this. a map file contains key=value
479 pairs. if value is quoted, it is treated as string. otherwise, it
486 pairs. if value is quoted, it is treated as string. otherwise, it
480 is treated as name of template file.
487 is treated as name of template file.
481
488
482 templater is asked to expand a key in map. it looks up key, and
489 templater is asked to expand a key in map. it looks up key, and
483 looks for strings like this: {foo}. it expands {foo} by looking up
490 looks for strings like this: {foo}. it expands {foo} by looking up
484 foo in map, and substituting it. expansion is recursive: it stops
491 foo in map, and substituting it. expansion is recursive: it stops
485 when there is no more {foo} to replace.
492 when there is no more {foo} to replace.
486
493
487 expansion also allows formatting and filtering.
494 expansion also allows formatting and filtering.
488
495
489 format uses key to expand each item in list. syntax is
496 format uses key to expand each item in list. syntax is
490 {key%format}.
497 {key%format}.
491
498
492 filter uses function to transform value. syntax is
499 filter uses function to transform value. syntax is
493 {key|filter1|filter2|...}.'''
500 {key|filter1|filter2|...}.'''
494
501
495 def __init__(self, loader, filters={}, defaults={}):
502 def __init__(self, loader, filters={}, defaults={}):
496 self._loader = loader
503 self._loader = loader
497 self._filters = filters
504 self._filters = filters
498 self._defaults = defaults
505 self._defaults = defaults
499 self._cache = {}
506 self._cache = {}
500
507
501 def _load(self, t):
508 def _load(self, t):
502 '''load, parse, and cache a template'''
509 '''load, parse, and cache a template'''
503 if t not in self._cache:
510 if t not in self._cache:
504 self._cache[t] = compiletemplate(self._loader(t), self)
511 self._cache[t] = compiletemplate(self._loader(t), self)
505 return self._cache[t]
512 return self._cache[t]
506
513
507 def process(self, t, mapping):
514 def process(self, t, mapping):
508 '''Perform expansion. t is name of map element to expand.
515 '''Perform expansion. t is name of map element to expand.
509 mapping contains added elements for use during expansion. Is a
516 mapping contains added elements for use during expansion. Is a
510 generator.'''
517 generator.'''
511 return _flatten(runtemplate(self, mapping, self._load(t)))
518 return _flatten(runtemplate(self, mapping, self._load(t)))
512
519
513 engines = {'default': engine}
520 engines = {'default': engine}
514
521
515 def stylelist():
522 def stylelist():
516 paths = templatepath()
523 paths = templatepath()
517 if not paths:
524 if not paths:
518 return _('no templates found, try `hg debuginstall` for more info')
525 return _('no templates found, try `hg debuginstall` for more info')
519 dirlist = os.listdir(paths[0])
526 dirlist = os.listdir(paths[0])
520 stylelist = []
527 stylelist = []
521 for file in dirlist:
528 for file in dirlist:
522 split = file.split(".")
529 split = file.split(".")
523 if split[0] == "map-cmdline":
530 if split[0] == "map-cmdline":
524 stylelist.append(split[1])
531 stylelist.append(split[1])
525 return ", ".join(sorted(stylelist))
532 return ", ".join(sorted(stylelist))
526
533
527 class TemplateNotFound(util.Abort):
534 class TemplateNotFound(util.Abort):
528 pass
535 pass
529
536
530 class templater(object):
537 class templater(object):
531
538
532 def __init__(self, mapfile, filters={}, defaults={}, cache={},
539 def __init__(self, mapfile, filters={}, defaults={}, cache={},
533 minchunk=1024, maxchunk=65536):
540 minchunk=1024, maxchunk=65536):
534 '''set up template engine.
541 '''set up template engine.
535 mapfile is name of file to read map definitions from.
542 mapfile is name of file to read map definitions from.
536 filters is dict of functions. each transforms a value into another.
543 filters is dict of functions. each transforms a value into another.
537 defaults is dict of default map definitions.'''
544 defaults is dict of default map definitions.'''
538 self.mapfile = mapfile or 'template'
545 self.mapfile = mapfile or 'template'
539 self.cache = cache.copy()
546 self.cache = cache.copy()
540 self.map = {}
547 self.map = {}
541 self.base = (mapfile and os.path.dirname(mapfile)) or ''
548 self.base = (mapfile and os.path.dirname(mapfile)) or ''
542 self.filters = templatefilters.filters.copy()
549 self.filters = templatefilters.filters.copy()
543 self.filters.update(filters)
550 self.filters.update(filters)
544 self.defaults = defaults
551 self.defaults = defaults
545 self.minchunk, self.maxchunk = minchunk, maxchunk
552 self.minchunk, self.maxchunk = minchunk, maxchunk
546 self.ecache = {}
553 self.ecache = {}
547
554
548 if not mapfile:
555 if not mapfile:
549 return
556 return
550 if not os.path.exists(mapfile):
557 if not os.path.exists(mapfile):
551 raise util.Abort(_("style '%s' not found") % mapfile,
558 raise util.Abort(_("style '%s' not found") % mapfile,
552 hint=_("available styles: %s") % stylelist())
559 hint=_("available styles: %s") % stylelist())
553
560
554 conf = config.config()
561 conf = config.config()
555 conf.read(mapfile)
562 conf.read(mapfile)
556
563
557 for key, val in conf[''].items():
564 for key, val in conf[''].items():
558 if not val:
565 if not val:
559 raise SyntaxError(_('%s: missing value') % conf.source('', key))
566 raise SyntaxError(_('%s: missing value') % conf.source('', key))
560 if val[0] in "'\"":
567 if val[0] in "'\"":
561 try:
568 try:
562 self.cache[key] = parsestring(val)
569 self.cache[key] = parsestring(val)
563 except SyntaxError, inst:
570 except SyntaxError, inst:
564 raise SyntaxError('%s: %s' %
571 raise SyntaxError('%s: %s' %
565 (conf.source('', key), inst.args[0]))
572 (conf.source('', key), inst.args[0]))
566 else:
573 else:
567 val = 'default', val
574 val = 'default', val
568 if ':' in val[1]:
575 if ':' in val[1]:
569 val = val[1].split(':', 1)
576 val = val[1].split(':', 1)
570 self.map[key] = val[0], os.path.join(self.base, val[1])
577 self.map[key] = val[0], os.path.join(self.base, val[1])
571
578
572 def __contains__(self, key):
579 def __contains__(self, key):
573 return key in self.cache or key in self.map
580 return key in self.cache or key in self.map
574
581
575 def load(self, t):
582 def load(self, t):
576 '''Get the template for the given template name. Use a local cache.'''
583 '''Get the template for the given template name. Use a local cache.'''
577 if t not in self.cache:
584 if t not in self.cache:
578 try:
585 try:
579 self.cache[t] = util.readfile(self.map[t][1])
586 self.cache[t] = util.readfile(self.map[t][1])
580 except KeyError, inst:
587 except KeyError, inst:
581 raise TemplateNotFound(_('"%s" not in template map') %
588 raise TemplateNotFound(_('"%s" not in template map') %
582 inst.args[0])
589 inst.args[0])
583 except IOError, inst:
590 except IOError, inst:
584 raise IOError(inst.args[0], _('template file %s: %s') %
591 raise IOError(inst.args[0], _('template file %s: %s') %
585 (self.map[t][1], inst.args[1]))
592 (self.map[t][1], inst.args[1]))
586 return self.cache[t]
593 return self.cache[t]
587
594
588 def __call__(self, t, **mapping):
595 def __call__(self, t, **mapping):
589 ttype = t in self.map and self.map[t][0] or 'default'
596 ttype = t in self.map and self.map[t][0] or 'default'
590 if ttype not in self.ecache:
597 if ttype not in self.ecache:
591 self.ecache[ttype] = engines[ttype](self.load,
598 self.ecache[ttype] = engines[ttype](self.load,
592 self.filters, self.defaults)
599 self.filters, self.defaults)
593 proc = self.ecache[ttype]
600 proc = self.ecache[ttype]
594
601
595 stream = proc.process(t, mapping)
602 stream = proc.process(t, mapping)
596 if self.minchunk:
603 if self.minchunk:
597 stream = util.increasingchunks(stream, min=self.minchunk,
604 stream = util.increasingchunks(stream, min=self.minchunk,
598 max=self.maxchunk)
605 max=self.maxchunk)
599 return stream
606 return stream
600
607
601 def templatepath(name=None):
608 def templatepath(name=None):
602 '''return location of template file or directory (if no name).
609 '''return location of template file or directory (if no name).
603 returns None if not found.'''
610 returns None if not found.'''
604 normpaths = []
611 normpaths = []
605
612
606 # executable version (py2exe) doesn't support __file__
613 # executable version (py2exe) doesn't support __file__
607 if util.mainfrozen():
614 if util.mainfrozen():
608 module = sys.executable
615 module = sys.executable
609 else:
616 else:
610 module = __file__
617 module = __file__
611 for f in path:
618 for f in path:
612 if f.startswith('/'):
619 if f.startswith('/'):
613 p = f
620 p = f
614 else:
621 else:
615 fl = f.split('/')
622 fl = f.split('/')
616 p = os.path.join(os.path.dirname(module), *fl)
623 p = os.path.join(os.path.dirname(module), *fl)
617 if name:
624 if name:
618 p = os.path.join(p, name)
625 p = os.path.join(p, name)
619 if name and os.path.exists(p):
626 if name and os.path.exists(p):
620 return os.path.normpath(p)
627 return os.path.normpath(p)
621 elif os.path.isdir(p):
628 elif os.path.isdir(p):
622 normpaths.append(os.path.normpath(p))
629 normpaths.append(os.path.normpath(p))
623
630
624 return normpaths
631 return normpaths
625
632
626 def stylemap(styles, paths=None):
633 def stylemap(styles, paths=None):
627 """Return path to mapfile for a given style.
634 """Return path to mapfile for a given style.
628
635
629 Searches mapfile in the following locations:
636 Searches mapfile in the following locations:
630 1. templatepath/style/map
637 1. templatepath/style/map
631 2. templatepath/map-style
638 2. templatepath/map-style
632 3. templatepath/map
639 3. templatepath/map
633 """
640 """
634
641
635 if paths is None:
642 if paths is None:
636 paths = templatepath()
643 paths = templatepath()
637 elif isinstance(paths, str):
644 elif isinstance(paths, str):
638 paths = [paths]
645 paths = [paths]
639
646
640 if isinstance(styles, str):
647 if isinstance(styles, str):
641 styles = [styles]
648 styles = [styles]
642
649
643 for style in styles:
650 for style in styles:
644 if not style:
651 if not style:
645 continue
652 continue
646 locations = [os.path.join(style, 'map'), 'map-' + style]
653 locations = [os.path.join(style, 'map'), 'map-' + style]
647 locations.append('map')
654 locations.append('map')
648
655
649 for path in paths:
656 for path in paths:
650 for location in locations:
657 for location in locations:
651 mapfile = os.path.join(path, location)
658 mapfile = os.path.join(path, location)
652 if os.path.isfile(mapfile):
659 if os.path.isfile(mapfile):
653 return style, mapfile
660 return style, mapfile
654
661
655 raise RuntimeError("No hgweb templates found in %r" % paths)
662 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now