PageRenderTime 60ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/mercurial/templater.py

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