mercurial/templater.py
changeset 13176 895f54a79c6e
parent 13175 09cde75e0613
child 13187 e3b87fb34d00
equal deleted inserted replaced
13175:09cde75e0613 13176:895f54a79c6e
     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
     9 import sys, os
    10 import util, config, templatefilters
    10 import util, config, templatefilters, parser, error
       
    11 
       
    12 # template parsing
       
    13 
       
    14 elements = {
       
    15     "(": (20, ("group", 1, ")"), ("func", 1, ")")),
       
    16     ",": (2, None, ("list", 2)),
       
    17     "|": (5, None, ("|", 5)),
       
    18     "%": (6, None, ("%", 6)),
       
    19     ")": (0, None, None),
       
    20     "symbol": (0, ("symbol",), None),
       
    21     "string": (0, ("string",), None),
       
    22     "end": (0, None, None),
       
    23 }
       
    24 
       
    25 def tokenizer(data):
       
    26     program, start, end = data
       
    27     pos = start
       
    28     while pos < end:
       
    29         c = program[pos]
       
    30         if c.isspace(): # skip inter-token whitespace
       
    31             pass
       
    32         elif c in "(,)%|": # handle simple operators
       
    33             yield (c, None, pos)
       
    34         elif (c in '"\'' or c == 'r' and
       
    35               program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
       
    36             if c == 'r':
       
    37                 pos += 1
       
    38                 c = program[pos]
       
    39                 decode = lambda x: x
       
    40             else:
       
    41                 decode = lambda x: x.decode('string-escape')
       
    42             pos += 1
       
    43             s = pos
       
    44             while pos < end: # find closing quote
       
    45                 d = program[pos]
       
    46                 if d == '\\': # skip over escaped characters
       
    47                     pos += 2
       
    48                     continue
       
    49                 if d == c:
       
    50                     yield ('string', decode(program[s:pos]), s)
       
    51                     break
       
    52                 pos += 1
       
    53             else:
       
    54                 raise error.ParseError(_("unterminated string"), s)
       
    55         elif c.isalnum() or c in '_':
       
    56             s = pos
       
    57             pos += 1
       
    58             while pos < end: # find end of symbol
       
    59                 d = program[pos]
       
    60                 if not (d.isalnum() or d == "_"):
       
    61                     break
       
    62                 pos += 1
       
    63             sym = program[s:pos]
       
    64             yield ('symbol', sym, s)
       
    65             pos -= 1
       
    66         elif c == '}':
       
    67             pos += 1
       
    68             break
       
    69         else:
       
    70             raise error.ParseError(_("syntax error"), pos)
       
    71         pos += 1
       
    72     data[2] = pos
       
    73     yield ('end', None, pos)
       
    74 
       
    75 def compiletemplate(tmpl, context):
       
    76     parsed = []
       
    77     pos, stop = 0, len(tmpl)
       
    78     p = parser.parser(tokenizer, elements)
       
    79 
       
    80     while pos < stop:
       
    81         n = tmpl.find('{', pos)
       
    82         if n < 0:
       
    83             parsed.append(("string", tmpl[pos:]))
       
    84             break
       
    85         if n > 0 and tmpl[n - 1] == '\\':
       
    86             # escaped
       
    87             parsed.append(("string", tmpl[pos:n - 1] + "{"))
       
    88             pos = n + 1
       
    89             continue
       
    90         if n > pos:
       
    91             parsed.append(("string", tmpl[pos:n]))
       
    92 
       
    93         pd = [tmpl, n + 1, stop]
       
    94         parsed.append(p.parse(pd))
       
    95         pos = pd[2]
       
    96 
       
    97     return [compileexp(e, context) for e in parsed]
       
    98 
       
    99 def compileexp(exp, context):
       
   100     t = exp[0]
       
   101     if t in methods:
       
   102         return methods[t](exp, context)
       
   103     raise error.ParseError(_("unknown method '%s'") % t)
       
   104 
       
   105 # template evaluation
       
   106 
       
   107 def getsymbol(exp):
       
   108     if exp[0] == 'symbol':
       
   109         return exp[1]
       
   110     raise error.ParseError(_("expected a symbol"))
       
   111 
       
   112 def getlist(x):
       
   113     if not x:
       
   114         return []
       
   115     if x[0] == 'list':
       
   116         return getlist(x[1]) + [x[2]]
       
   117     return [x]
       
   118 
       
   119 def getfilter(exp, context):
       
   120     f = getsymbol(exp)
       
   121     if f not in context._filters:
       
   122         raise error.ParseError(_("unknown function '%s'") % f)
       
   123     return context._filters[f]
       
   124 
       
   125 def gettemplate(exp, context):
       
   126     if exp[0] == 'string':
       
   127         return compiletemplate(exp[1], context)
       
   128     if exp[0] == 'symbol':
       
   129         return context._load(exp[1])
       
   130     raise error.ParseError(_("expected template specifier"))
       
   131 
       
   132 def runstring(context, mapping, data):
       
   133     return data
       
   134 
       
   135 def runsymbol(context, mapping, key):
       
   136     v = mapping.get(key)
       
   137     if v is None:
       
   138         v = context._defaults.get(key, '')
       
   139     if hasattr(v, '__call__'):
       
   140         return v(**mapping)
       
   141     return v
       
   142 
       
   143 def buildfilter(exp, context):
       
   144     func, data = compileexp(exp[1], context)
       
   145     filt = getfilter(exp[2], context)
       
   146     return (runfilter, (func, data, filt))
       
   147 
       
   148 def runfilter(context, mapping, data):
       
   149     func, data, filt = data
       
   150     return filt(func(context, mapping, data))
       
   151 
       
   152 def buildmap(exp, context):
       
   153     func, data = compileexp(exp[1], context)
       
   154     ctmpl = gettemplate(exp[2], context)
       
   155     return (runmap, (func, data, ctmpl))
       
   156 
       
   157 def runmap(context, mapping, data):
       
   158     func, data, ctmpl = data
       
   159     d = func(context, mapping, data)
       
   160     lm = mapping.copy()
       
   161 
       
   162     for i in d:
       
   163         if isinstance(i, dict):
       
   164             lm.update(i)
       
   165             for f, d in ctmpl:
       
   166                 yield f(context, lm, d)
       
   167         else:
       
   168             # v is not an iterable of dicts, this happen when 'key'
       
   169             # has been fully expanded already and format is useless.
       
   170             # If so, return the expanded value.
       
   171             yield i
       
   172 
       
   173 def buildfunc(exp, context):
       
   174     n = getsymbol(exp[1])
       
   175     args = [compileexp(x, context) for x in getlist(exp[2])]
       
   176     if n in context._filters:
       
   177         if len(args) != 1:
       
   178             raise error.ParseError(_("filter %s expects one argument") % n)
       
   179         f = context._filters[n]
       
   180         return (runfilter, (args[0][0], args[0][1], f))
       
   181     elif n in context._funcs:
       
   182         f = context._funcs[n]
       
   183         return (f, args)
       
   184 
       
   185 methods = {
       
   186     "string": lambda e, c: (runstring, e[1]),
       
   187     "symbol": lambda e, c: (runsymbol, e[1]),
       
   188     "group": lambda e, c: compileexp(e[1], c),
       
   189 #    ".": buildmember,
       
   190     "|": buildfilter,
       
   191     "%": buildmap,
       
   192     "func": buildfunc,
       
   193     }
       
   194 
       
   195 # template engine
    11 
   196 
    12 path = ['templates', '../templates']
   197 path = ['templates', '../templates']
    13 stringify = templatefilters.stringify
   198 stringify = templatefilters.stringify
    14 
   199 
    15 def _flatten(thing):
   200 def _flatten(thing):
    64         self._loader = loader
   249         self._loader = loader
    65         self._filters = filters
   250         self._filters = filters
    66         self._defaults = defaults
   251         self._defaults = defaults
    67         self._cache = {}
   252         self._cache = {}
    68 
   253 
       
   254     def _load(self, t):
       
   255         '''load, parse, and cache a template'''
       
   256         if t not in self._cache:
       
   257             self._cache[t] = compiletemplate(self._loader(t), self)
       
   258         return self._cache[t]
       
   259 
    69     def process(self, t, mapping):
   260     def process(self, t, mapping):
    70         '''Perform expansion. t is name of map element to expand.
   261         '''Perform expansion. t is name of map element to expand.
    71         mapping contains added elements for use during expansion. Is a
   262         mapping contains added elements for use during expansion. Is a
    72         generator.'''
   263         generator.'''
    73         return _flatten(self._process(self._load(t), mapping))
   264         return _flatten(func(self, mapping, data) for func, data in
    74 
   265                          self._load(t))
    75     def _load(self, t):
       
    76         '''load, parse, and cache a template'''
       
    77         if t not in self._cache:
       
    78             self._cache[t] = self._parse(self._loader(t))
       
    79         return self._cache[t]
       
    80 
       
    81     def _get(self, mapping, key):
       
    82         v = mapping.get(key)
       
    83         if v is None:
       
    84             v = self._defaults.get(key, '')
       
    85         if hasattr(v, '__call__'):
       
    86             v = v(**mapping)
       
    87         return v
       
    88 
       
    89     def _filter(self, mapping, parts):
       
    90         filters, val = parts
       
    91         x = self._get(mapping, val)
       
    92         for f in filters:
       
    93             x = f(x)
       
    94         return x
       
    95 
       
    96     def _format(self, mapping, args):
       
    97         key, parsed = args
       
    98         v = self._get(mapping, key)
       
    99         if not hasattr(v, '__iter__'):
       
   100             raise SyntaxError(_("error expanding '%s%%%s'")
       
   101                               % (key, parsed))
       
   102         lm = mapping.copy()
       
   103         for i in v:
       
   104             if isinstance(i, dict):
       
   105                 lm.update(i)
       
   106                 yield self._process(parsed, lm)
       
   107             else:
       
   108                 # v is not an iterable of dicts, this happen when 'key'
       
   109                 # has been fully expanded already and format is useless.
       
   110                 # If so, return the expanded value.
       
   111                 yield i
       
   112 
       
   113     def _parse(self, tmpl):
       
   114         '''preparse a template'''
       
   115         parsed = []
       
   116         pos, stop = 0, len(tmpl)
       
   117         while pos < stop:
       
   118             n = tmpl.find('{', pos)
       
   119             if n < 0:
       
   120                 parsed.append((None, tmpl[pos:stop]))
       
   121                 break
       
   122             if n > 0 and tmpl[n - 1] == '\\':
       
   123                 # escaped
       
   124                 parsed.append((None, tmpl[pos:n - 1] + "{"))
       
   125                 pos = n + 1
       
   126                 continue
       
   127             if n > pos:
       
   128                 parsed.append((None, tmpl[pos:n]))
       
   129 
       
   130             pos = n
       
   131             n = tmpl.find('}', pos)
       
   132             if n < 0:
       
   133                 # no closing
       
   134                 parsed.append((None, tmpl[pos:stop]))
       
   135                 break
       
   136 
       
   137             expr = tmpl[pos + 1:n]
       
   138             pos = n + 1
       
   139 
       
   140             if '%' in expr:
       
   141                 # the keyword should be formatted with a template
       
   142                 key, t = expr.split('%')
       
   143                 parsed.append((self._format, (key.strip(),
       
   144                                               self._load(t.strip()))))
       
   145             elif '|' in expr:
       
   146                 # process the keyword value with one or more filters
       
   147                 parts = expr.split('|')
       
   148                 val = parts[0].strip()
       
   149                 try:
       
   150                     filters = [self._filters[f.strip()] for f in parts[1:]]
       
   151                 except KeyError, i:
       
   152                     raise SyntaxError(_("unknown filter '%s'") % i[0])
       
   153                 parsed.append((self._filter, (filters, val)))
       
   154             else:
       
   155                 # just get the keyword
       
   156                 parsed.append((self._get, expr.strip()))
       
   157 
       
   158         return parsed
       
   159 
       
   160     def _process(self, parsed, mapping):
       
   161         '''Render a template. Returns a generator.'''
       
   162         for f, e in parsed:
       
   163             if f:
       
   164                 yield f(mapping, e)
       
   165             else:
       
   166                 yield e
       
   167 
   266 
   168 engines = {'default': engine}
   267 engines = {'default': engine}
   169 
   268 
   170 class templater(object):
   269 class templater(object):
   171 
   270