templater: use the parser.py parser to extend the templater syntax
authorMatt Mackall <mpm@selenic.com>
Mon, 20 Dec 2010 12:09:00 -0600
changeset 13176 895f54a79c6e
parent 13175 09cde75e0613
child 13177 04c8f3787546
templater: use the parser.py parser to extend the templater syntax
mercurial/parser.py
mercurial/templater.py
mercurial/templates/paper/branches.tmpl
mercurial/templates/paper/shortlogentry.tmpl
--- a/mercurial/parser.py	Mon Dec 20 12:09:00 2010 -0600
+++ b/mercurial/parser.py	Mon Dec 20 12:09:00 2010 -0600
@@ -22,6 +22,7 @@
         self._tokenizer = tokenizer
         self._elements = elements
         self._methods = methods
+        self.current = None
     def _advance(self):
         'advance the tokenizer'
         t = self.current
@@ -76,7 +77,7 @@
     def parse(self, message):
         'generate a parse tree from a message'
         self._iter = self._tokenizer(message)
-        self.current = self._iter.next()
+        self._advance()
         return self._parse()
     def eval(self, tree):
         'recursively evaluate a parse tree using node methods'
--- a/mercurial/templater.py	Mon Dec 20 12:09:00 2010 -0600
+++ b/mercurial/templater.py	Mon Dec 20 12:09:00 2010 -0600
@@ -7,7 +7,192 @@
 
 from i18n import _
 import sys, os
-import util, config, templatefilters
+import util, config, templatefilters, parser, error
+
+# template parsing
+
+elements = {
+    "(": (20, ("group", 1, ")"), ("func", 1, ")")),
+    ",": (2, None, ("list", 2)),
+    "|": (5, None, ("|", 5)),
+    "%": (6, None, ("%", 6)),
+    ")": (0, None, None),
+    "symbol": (0, ("symbol",), None),
+    "string": (0, ("string",), None),
+    "end": (0, None, None),
+}
+
+def tokenizer(data):
+    program, start, end = data
+    pos = start
+    while pos < end:
+        c = program[pos]
+        if c.isspace(): # skip inter-token whitespace
+            pass
+        elif c in "(,)%|": # handle simple operators
+            yield (c, None, pos)
+        elif (c in '"\'' or c == 'r' and
+              program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
+            if c == 'r':
+                pos += 1
+                c = program[pos]
+                decode = lambda x: x
+            else:
+                decode = lambda x: x.decode('string-escape')
+            pos += 1
+            s = pos
+            while pos < end: # find closing quote
+                d = program[pos]
+                if d == '\\': # skip over escaped characters
+                    pos += 2
+                    continue
+                if d == c:
+                    yield ('string', decode(program[s:pos]), s)
+                    break
+                pos += 1
+            else:
+                raise error.ParseError(_("unterminated string"), s)
+        elif c.isalnum() or c in '_':
+            s = pos
+            pos += 1
+            while pos < end: # find end of symbol
+                d = program[pos]
+                if not (d.isalnum() or d == "_"):
+                    break
+                pos += 1
+            sym = program[s:pos]
+            yield ('symbol', sym, s)
+            pos -= 1
+        elif c == '}':
+            pos += 1
+            break
+        else:
+            raise error.ParseError(_("syntax error"), pos)
+        pos += 1
+    data[2] = pos
+    yield ('end', None, pos)
+
+def compiletemplate(tmpl, context):
+    parsed = []
+    pos, stop = 0, len(tmpl)
+    p = parser.parser(tokenizer, elements)
+
+    while pos < stop:
+        n = tmpl.find('{', pos)
+        if n < 0:
+            parsed.append(("string", tmpl[pos:]))
+            break
+        if n > 0 and tmpl[n - 1] == '\\':
+            # escaped
+            parsed.append(("string", tmpl[pos:n - 1] + "{"))
+            pos = n + 1
+            continue
+        if n > pos:
+            parsed.append(("string", tmpl[pos:n]))
+
+        pd = [tmpl, n + 1, stop]
+        parsed.append(p.parse(pd))
+        pos = pd[2]
+
+    return [compileexp(e, context) for e in parsed]
+
+def compileexp(exp, context):
+    t = exp[0]
+    if t in methods:
+        return methods[t](exp, context)
+    raise error.ParseError(_("unknown method '%s'") % t)
+
+# template evaluation
+
+def getsymbol(exp):
+    if exp[0] == 'symbol':
+        return exp[1]
+    raise error.ParseError(_("expected a symbol"))
+
+def getlist(x):
+    if not x:
+        return []
+    if x[0] == 'list':
+        return getlist(x[1]) + [x[2]]
+    return [x]
+
+def getfilter(exp, context):
+    f = getsymbol(exp)
+    if f not in context._filters:
+        raise error.ParseError(_("unknown function '%s'") % f)
+    return context._filters[f]
+
+def gettemplate(exp, context):
+    if exp[0] == 'string':
+        return compiletemplate(exp[1], context)
+    if exp[0] == 'symbol':
+        return context._load(exp[1])
+    raise error.ParseError(_("expected template specifier"))
+
+def runstring(context, mapping, data):
+    return data
+
+def runsymbol(context, mapping, key):
+    v = mapping.get(key)
+    if v is None:
+        v = context._defaults.get(key, '')
+    if hasattr(v, '__call__'):
+        return v(**mapping)
+    return v
+
+def buildfilter(exp, context):
+    func, data = compileexp(exp[1], context)
+    filt = getfilter(exp[2], context)
+    return (runfilter, (func, data, filt))
+
+def runfilter(context, mapping, data):
+    func, data, filt = data
+    return filt(func(context, mapping, data))
+
+def buildmap(exp, context):
+    func, data = compileexp(exp[1], context)
+    ctmpl = gettemplate(exp[2], context)
+    return (runmap, (func, data, ctmpl))
+
+def runmap(context, mapping, data):
+    func, data, ctmpl = data
+    d = func(context, mapping, data)
+    lm = mapping.copy()
+
+    for i in d:
+        if isinstance(i, dict):
+            lm.update(i)
+            for f, d in ctmpl:
+                yield f(context, lm, d)
+        else:
+            # v is not an iterable of dicts, this happen when 'key'
+            # has been fully expanded already and format is useless.
+            # If so, return the expanded value.
+            yield i
+
+def buildfunc(exp, context):
+    n = getsymbol(exp[1])
+    args = [compileexp(x, context) for x in getlist(exp[2])]
+    if n in context._filters:
+        if len(args) != 1:
+            raise error.ParseError(_("filter %s expects one argument") % n)
+        f = context._filters[n]
+        return (runfilter, (args[0][0], args[0][1], f))
+    elif n in context._funcs:
+        f = context._funcs[n]
+        return (f, args)
+
+methods = {
+    "string": lambda e, c: (runstring, e[1]),
+    "symbol": lambda e, c: (runsymbol, e[1]),
+    "group": lambda e, c: compileexp(e[1], c),
+#    ".": buildmember,
+    "|": buildfilter,
+    "%": buildmap,
+    "func": buildfunc,
+    }
+
+# template engine
 
 path = ['templates', '../templates']
 stringify = templatefilters.stringify
@@ -66,104 +251,18 @@
         self._defaults = defaults
         self._cache = {}
 
+    def _load(self, t):
+        '''load, parse, and cache a template'''
+        if t not in self._cache:
+            self._cache[t] = compiletemplate(self._loader(t), self)
+        return self._cache[t]
+
     def process(self, t, mapping):
         '''Perform expansion. t is name of map element to expand.
         mapping contains added elements for use during expansion. Is a
         generator.'''
-        return _flatten(self._process(self._load(t), mapping))
-
-    def _load(self, t):
-        '''load, parse, and cache a template'''
-        if t not in self._cache:
-            self._cache[t] = self._parse(self._loader(t))
-        return self._cache[t]
-
-    def _get(self, mapping, key):
-        v = mapping.get(key)
-        if v is None:
-            v = self._defaults.get(key, '')
-        if hasattr(v, '__call__'):
-            v = v(**mapping)
-        return v
-
-    def _filter(self, mapping, parts):
-        filters, val = parts
-        x = self._get(mapping, val)
-        for f in filters:
-            x = f(x)
-        return x
-
-    def _format(self, mapping, args):
-        key, parsed = args
-        v = self._get(mapping, key)
-        if not hasattr(v, '__iter__'):
-            raise SyntaxError(_("error expanding '%s%%%s'")
-                              % (key, parsed))
-        lm = mapping.copy()
-        for i in v:
-            if isinstance(i, dict):
-                lm.update(i)
-                yield self._process(parsed, lm)
-            else:
-                # v is not an iterable of dicts, this happen when 'key'
-                # has been fully expanded already and format is useless.
-                # If so, return the expanded value.
-                yield i
-
-    def _parse(self, tmpl):
-        '''preparse a template'''
-        parsed = []
-        pos, stop = 0, len(tmpl)
-        while pos < stop:
-            n = tmpl.find('{', pos)
-            if n < 0:
-                parsed.append((None, tmpl[pos:stop]))
-                break
-            if n > 0 and tmpl[n - 1] == '\\':
-                # escaped
-                parsed.append((None, tmpl[pos:n - 1] + "{"))
-                pos = n + 1
-                continue
-            if n > pos:
-                parsed.append((None, tmpl[pos:n]))
-
-            pos = n
-            n = tmpl.find('}', pos)
-            if n < 0:
-                # no closing
-                parsed.append((None, tmpl[pos:stop]))
-                break
-
-            expr = tmpl[pos + 1:n]
-            pos = n + 1
-
-            if '%' in expr:
-                # the keyword should be formatted with a template
-                key, t = expr.split('%')
-                parsed.append((self._format, (key.strip(),
-                                              self._load(t.strip()))))
-            elif '|' in expr:
-                # process the keyword value with one or more filters
-                parts = expr.split('|')
-                val = parts[0].strip()
-                try:
-                    filters = [self._filters[f.strip()] for f in parts[1:]]
-                except KeyError, i:
-                    raise SyntaxError(_("unknown filter '%s'") % i[0])
-                parsed.append((self._filter, (filters, val)))
-            else:
-                # just get the keyword
-                parsed.append((self._get, expr.strip()))
-
-        return parsed
-
-    def _process(self, parsed, mapping):
-        '''Render a template. Returns a generator.'''
-        for f, e in parsed:
-            if f:
-                yield f(mapping, e)
-            else:
-                yield e
+        return _flatten(func(self, mapping, data) for func, data in
+                         self._load(t))
 
 engines = {'default': engine}
 
--- a/mercurial/templates/paper/branches.tmpl	Mon Dec 20 12:09:00 2010 -0600
+++ b/mercurial/templates/paper/branches.tmpl	Mon Dec 20 12:09:00 2010 -0600
@@ -40,7 +40,18 @@
  <th>branch</th>
  <th>node</th>
 </tr>
-{entries%branchentry}
+{entries %
+' <tr class="tagEntry parity{parity}">
+    <td>
+      <a href="{url}shortlog/{node|short}{sessionvars%urlparameter}" class="{status}">
+        {branch|escape}
+      </a>
+    </td>
+    <td class="node">
+      {node|short}
+    </td>
+  </tr>'
+}
 </table>
 </div>
 </div>
--- a/mercurial/templates/paper/shortlogentry.tmpl	Mon Dec 20 12:09:00 2010 -0600
+++ b/mercurial/templates/paper/shortlogentry.tmpl	Mon Dec 20 12:09:00 2010 -0600
@@ -1,5 +1,5 @@
  <tr class="parity{parity}">
-  <td class="age">{date|age}</td>
+  <td class="age">{age(date)}</td>
   <td class="author">{author|person}</td>
-  <td class="description"><a href="{url}rev/{node|short}{sessionvars%urlparameter}">{desc|strip|firstline|escape|nonempty}</a>{inbranch%changelogbranchname}{branches%changelogbranchhead}{tags%changelogtag}</td>
+  <td class="description"><a href="{url}rev/{node|short}{sessionvars%urlparameter}">{desc|strip|firstline|escape|nonempty}</a>{inbranch%changelogbranchname}{branches%changelogbranchhead}{tags % '<span class="tag">{name|escape}</span> '}</td>
  </tr>