eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/templater.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     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 
       
     8 from i18n import _
       
     9 import sys, os
       
    10 import util, config, templatefilters
       
    11 
       
    12 path = ['templates', '../templates']
       
    13 stringify = templatefilters.stringify
       
    14 
       
    15 def _flatten(thing):
       
    16     '''yield a single stream from a possibly nested set of iterators'''
       
    17     if isinstance(thing, str):
       
    18         yield thing
       
    19     elif not hasattr(thing, '__iter__'):
       
    20         if thing is not None:
       
    21             yield str(thing)
       
    22     else:
       
    23         for i in thing:
       
    24             if isinstance(i, str):
       
    25                 yield i
       
    26             elif not hasattr(i, '__iter__'):
       
    27                 if i is not None:
       
    28                     yield str(i)
       
    29             elif i is not None:
       
    30                 for j in _flatten(i):
       
    31                     yield j
       
    32 
       
    33 def parsestring(s, quoted=True):
       
    34     '''parse a string using simple c-like syntax.
       
    35     string must be in quotes if quoted is True.'''
       
    36     if quoted:
       
    37         if len(s) < 2 or s[0] != s[-1]:
       
    38             raise SyntaxError(_('unmatched quotes'))
       
    39         return s[1:-1].decode('string_escape')
       
    40 
       
    41     return s.decode('string_escape')
       
    42 
       
    43 class engine(object):
       
    44     '''template expansion engine.
       
    45 
       
    46     template expansion works like this. a map file contains key=value
       
    47     pairs. if value is quoted, it is treated as string. otherwise, it
       
    48     is treated as name of template file.
       
    49 
       
    50     templater is asked to expand a key in map. it looks up key, and
       
    51     looks for strings like this: {foo}. it expands {foo} by looking up
       
    52     foo in map, and substituting it. expansion is recursive: it stops
       
    53     when there is no more {foo} to replace.
       
    54 
       
    55     expansion also allows formatting and filtering.
       
    56 
       
    57     format uses key to expand each item in list. syntax is
       
    58     {key%format}.
       
    59 
       
    60     filter uses function to transform value. syntax is
       
    61     {key|filter1|filter2|...}.'''
       
    62 
       
    63     def __init__(self, loader, filters={}, defaults={}):
       
    64         self._loader = loader
       
    65         self._filters = filters
       
    66         self._defaults = defaults
       
    67         self._cache = {}
       
    68 
       
    69     def process(self, t, mapping):
       
    70         '''Perform expansion. t is name of map element to expand.
       
    71         mapping contains added elements for use during expansion. Is a
       
    72         generator.'''
       
    73         return _flatten(self._process(self._load(t), mapping))
       
    74 
       
    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 
       
   168 engines = {'default': engine}
       
   169 
       
   170 class templater(object):
       
   171 
       
   172     def __init__(self, mapfile, filters={}, defaults={}, cache={},
       
   173                  minchunk=1024, maxchunk=65536):
       
   174         '''set up template engine.
       
   175         mapfile is name of file to read map definitions from.
       
   176         filters is dict of functions. each transforms a value into another.
       
   177         defaults is dict of default map definitions.'''
       
   178         self.mapfile = mapfile or 'template'
       
   179         self.cache = cache.copy()
       
   180         self.map = {}
       
   181         self.base = (mapfile and os.path.dirname(mapfile)) or ''
       
   182         self.filters = templatefilters.filters.copy()
       
   183         self.filters.update(filters)
       
   184         self.defaults = defaults
       
   185         self.minchunk, self.maxchunk = minchunk, maxchunk
       
   186         self.engines = {}
       
   187 
       
   188         if not mapfile:
       
   189             return
       
   190         if not os.path.exists(mapfile):
       
   191             raise util.Abort(_('style not found: %s') % mapfile)
       
   192 
       
   193         conf = config.config()
       
   194         conf.read(mapfile)
       
   195 
       
   196         for key, val in conf[''].items():
       
   197             if val[0] in "'\"":
       
   198                 try:
       
   199                     self.cache[key] = parsestring(val)
       
   200                 except SyntaxError, inst:
       
   201                     raise SyntaxError('%s: %s' %
       
   202                                       (conf.source('', key), inst.args[0]))
       
   203             else:
       
   204                 val = 'default', val
       
   205                 if ':' in val[1]:
       
   206                     val = val[1].split(':', 1)
       
   207                 self.map[key] = val[0], os.path.join(self.base, val[1])
       
   208 
       
   209     def __contains__(self, key):
       
   210         return key in self.cache or key in self.map
       
   211 
       
   212     def load(self, t):
       
   213         '''Get the template for the given template name. Use a local cache.'''
       
   214         if not t in self.cache:
       
   215             try:
       
   216                 self.cache[t] = open(self.map[t][1]).read()
       
   217             except IOError, inst:
       
   218                 raise IOError(inst.args[0], _('template file %s: %s') %
       
   219                               (self.map[t][1], inst.args[1]))
       
   220         return self.cache[t]
       
   221 
       
   222     def __call__(self, t, **mapping):
       
   223         ttype = t in self.map and self.map[t][0] or 'default'
       
   224         proc = self.engines.get(ttype)
       
   225         if proc is None:
       
   226             proc = engines[ttype](self.load, self.filters, self.defaults)
       
   227             self.engines[ttype] = proc
       
   228 
       
   229         stream = proc.process(t, mapping)
       
   230         if self.minchunk:
       
   231             stream = util.increasingchunks(stream, min=self.minchunk,
       
   232                                            max=self.maxchunk)
       
   233         return stream
       
   234 
       
   235 def templatepath(name=None):
       
   236     '''return location of template file or directory (if no name).
       
   237     returns None if not found.'''
       
   238     normpaths = []
       
   239 
       
   240     # executable version (py2exe) doesn't support __file__
       
   241     if hasattr(sys, 'frozen'):
       
   242         module = sys.executable
       
   243     else:
       
   244         module = __file__
       
   245     for f in path:
       
   246         if f.startswith('/'):
       
   247             p = f
       
   248         else:
       
   249             fl = f.split('/')
       
   250             p = os.path.join(os.path.dirname(module), *fl)
       
   251         if name:
       
   252             p = os.path.join(p, name)
       
   253         if name and os.path.exists(p):
       
   254             return os.path.normpath(p)
       
   255         elif os.path.isdir(p):
       
   256             normpaths.append(os.path.normpath(p))
       
   257 
       
   258     return normpaths
       
   259 
       
   260 def stylemap(styles, paths=None):
       
   261     """Return path to mapfile for a given style.
       
   262 
       
   263     Searches mapfile in the following locations:
       
   264     1. templatepath/style/map
       
   265     2. templatepath/map-style
       
   266     3. templatepath/map
       
   267     """
       
   268 
       
   269     if paths is None:
       
   270         paths = templatepath()
       
   271     elif isinstance(paths, str):
       
   272         paths = [paths]
       
   273 
       
   274     if isinstance(styles, str):
       
   275         styles = [styles]
       
   276 
       
   277     for style in styles:
       
   278         if not style:
       
   279             continue
       
   280         locations = [os.path.join(style, 'map'), 'map-' + style]
       
   281         locations.append('map')
       
   282 
       
   283         for path in paths:
       
   284             for location in locations:
       
   285                 mapfile = os.path.join(path, location)
       
   286                 if os.path.isfile(mapfile):
       
   287                     return style, mapfile
       
   288 
       
   289     raise RuntimeError("No hgweb templates found in %r" % paths)