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