##// END OF EJS Templates
templater: raise nested functions
Matt Mackall -
r10852:0d50586a default
parent child Browse files
Show More
@@ -1,277 +1,287 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 i18n import _
9 9 import sys, os
10 10 import util, config, templatefilters
11 11
12 12 path = ['templates', '../templates']
13 13 stringify = templatefilters.stringify
14 14
15 15 def _flatten(thing):
16 16 '''yield a single stream from a possibly nested set of iterators'''
17 17 if isinstance(thing, str):
18 18 yield thing
19 19 elif not hasattr(thing, '__iter__'):
20 if i is not None:
20 21 yield str(thing)
21 elif thing is not None:
22 else:
22 23 for i in thing:
23 24 if isinstance(i, str):
24 25 yield i
25 26 elif not hasattr(i, '__iter__'):
27 if i is not None:
26 28 yield str(i)
27 29 elif i is not None:
28 30 for j in _flatten(i):
29 31 yield j
30 32
31 33 def parsestring(s, quoted=True):
32 34 '''parse a string using simple c-like syntax.
33 35 string must be in quotes if quoted is True.'''
34 36 if quoted:
35 37 if len(s) < 2 or s[0] != s[-1]:
36 38 raise SyntaxError(_('unmatched quotes'))
37 39 return s[1:-1].decode('string_escape')
38 40
39 41 return s.decode('string_escape')
40 42
41 43 class engine(object):
42 44 '''template expansion engine.
43 45
44 46 template expansion works like this. a map file contains key=value
45 47 pairs. if value is quoted, it is treated as string. otherwise, it
46 48 is treated as name of template file.
47 49
48 50 templater is asked to expand a key in map. it looks up key, and
49 51 looks for strings like this: {foo}. it expands {foo} by looking up
50 52 foo in map, and substituting it. expansion is recursive: it stops
51 53 when there is no more {foo} to replace.
52 54
53 55 expansion also allows formatting and filtering.
54 56
55 57 format uses key to expand each item in list. syntax is
56 58 {key%format}.
57 59
58 60 filter uses function to transform value. syntax is
59 61 {key|filter1|filter2|...}.'''
60 62
61 63 def __init__(self, loader, filters={}, defaults={}):
62 64 self._loader = loader
63 65 self._filters = filters
64 66 self._defaults = defaults
65 67 self._cache = {}
66 68
67 def process(self, t, mapping):
68 '''Perform expansion. t is name of map element to expand. mapping contains
69 added elements for use during expansion. Is a generator.'''
69 def _load(self, t):
70 '''load, parse, and cache a template'''
70 71 if t not in self._cache:
71 72 self._cache[t] = self._parse(self._loader(t))
72 return _flatten(self._process(self._cache[t], mapping))
73 return self._cache[t]
73 74
74 def _parse(self, tmpl):
75 '''preparse a template'''
75 def process(self, t, mapping):
76 '''Perform expansion. t is name of map element to expand.
77 mapping contains added elements for use during expansion. Is a
78 generator.'''
76 79
77 defget = self._defaults.get
78 def getter(mapping, key):
80 return _flatten(self._process(self._load(t), mapping))
81
82 def _get(self, mapping, key):
79 83 v = mapping.get(key)
80 84 if v is None:
81 v = defget(key, '')
85 v = self._defaults.get(key, '')
82 86 if hasattr(v, '__call__'):
83 87 v = v(**mapping)
84 88 return v
85 89
86 def raw(mapping, x):
90 def _raw(self, mapping, x):
87 91 return x
88 def filt(mapping, parts):
92
93 def _filter(self, mapping, parts):
89 94 filters, val = parts
90 x = getter(mapping, val)
95 x = self._get(mapping, val)
91 96 for f in filters:
92 97 x = f(x)
93 98 return x
94 def formatter(mapping, args):
95 key, format = args
96 v = getter(mapping, key)
99
100 def _format(self, mapping, args):
101 key, parsed = args
102 v = self._get(mapping, key)
97 103 if not hasattr(v, '__iter__'):
98 104 raise SyntaxError(_("error expanding '%s%%%s'")
99 105 % (key, format))
100 106 lm = mapping.copy()
101 107 for i in v:
102 108 if isinstance(i, dict):
103 109 lm.update(i)
104 yield self.process(format, lm)
110 yield self._process(parsed, lm)
105 111 else:
106 112 # v is not an iterable of dicts, this happen when 'key'
107 113 # has been fully expanded already and format is useless.
108 114 # If so, return the expanded value.
109 115 yield i
110 116
117 def _parse(self, tmpl):
118 '''preparse a template'''
119
111 120 parsed = []
112 121 pos, stop = 0, len(tmpl)
113 122 while pos < stop:
114 123 n = tmpl.find('{', pos)
115 124 if n < 0:
116 parsed.append((raw, tmpl[pos:stop]))
125 parsed.append((self._raw, tmpl[pos:stop]))
117 126 break
118 127 if n > 0 and tmpl[n - 1] == '\\':
119 128 # escaped
120 parsed.append((raw, tmpl[pos:n + 1]))
129 parsed.append((self._raw, tmpl[pos:n + 1]))
121 130 pos = n + 1
122 131 continue
123 132 if n > pos:
124 parsed.append((raw, tmpl[pos:n]))
133 parsed.append((self._raw, tmpl[pos:n]))
125 134
126 135 pos = n
127 136 n = tmpl.find('}', pos)
128 137 if n < 0:
129 138 # no closing
130 parsed.append((raw, tmpl[pos:stop]))
139 parsed.append((self._raw, tmpl[pos:stop]))
131 140 break
132 141
133 142 expr = tmpl[pos + 1:n]
134 143 pos = n + 1
135 144
136 145 if '%' in expr:
137 parsed.append((formatter, expr.split('%')))
146 key, t = expr.split('%')
147 parsed.append((self._format, (key, self._load(t))))
138 148 elif '|' in expr:
139 149 parts = expr.split('|')
140 150 val = parts[0]
141 151 try:
142 152 filters = [self._filters[f] for f in parts[1:]]
143 153 except KeyError, i:
144 154 raise SyntaxError(_("unknown filter '%s'") % i[0])
145 parsed.append((filt, (filters, val)))
155 parsed.append((self._filter, (filters, val)))
146 156 else:
147 parsed.append((getter, expr))
157 parsed.append((self._get, expr))
148 158
149 159 return parsed
150 160
151 161 def _process(self, parsed, mapping):
152 162 '''Render a template. Returns a generator.'''
153 163 for f, e in parsed:
154 164 yield f(mapping, e)
155 165
156 166 engines = {'default': engine}
157 167
158 168 class templater(object):
159 169
160 170 def __init__(self, mapfile, filters={}, defaults={}, cache={},
161 171 minchunk=1024, maxchunk=65536):
162 172 '''set up template engine.
163 173 mapfile is name of file to read map definitions from.
164 174 filters is dict of functions. each transforms a value into another.
165 175 defaults is dict of default map definitions.'''
166 176 self.mapfile = mapfile or 'template'
167 177 self.cache = cache.copy()
168 178 self.map = {}
169 179 self.base = (mapfile and os.path.dirname(mapfile)) or ''
170 180 self.filters = templatefilters.filters.copy()
171 181 self.filters.update(filters)
172 182 self.defaults = defaults
173 183 self.minchunk, self.maxchunk = minchunk, maxchunk
174 184 self.engines = {}
175 185
176 186 if not mapfile:
177 187 return
178 188 if not os.path.exists(mapfile):
179 189 raise util.Abort(_('style not found: %s') % mapfile)
180 190
181 191 conf = config.config()
182 192 conf.read(mapfile)
183 193
184 194 for key, val in conf[''].items():
185 195 if val[0] in "'\"":
186 196 try:
187 197 self.cache[key] = parsestring(val)
188 198 except SyntaxError, inst:
189 199 raise SyntaxError('%s: %s' %
190 200 (conf.source('', key), inst.args[0]))
191 201 else:
192 202 val = 'default', val
193 203 if ':' in val[1]:
194 204 val = val[1].split(':', 1)
195 205 self.map[key] = val[0], os.path.join(self.base, val[1])
196 206
197 207 def __contains__(self, key):
198 208 return key in self.cache or key in self.map
199 209
200 210 def load(self, t):
201 211 '''Get the template for the given template name. Use a local cache.'''
202 212 if not t in self.cache:
203 213 try:
204 214 self.cache[t] = open(self.map[t][1]).read()
205 215 except IOError, inst:
206 216 raise IOError(inst.args[0], _('template file %s: %s') %
207 217 (self.map[t][1], inst.args[1]))
208 218 return self.cache[t]
209 219
210 220 def __call__(self, t, **mapping):
211 221 ttype = t in self.map and self.map[t][0] or 'default'
212 222 proc = self.engines.get(ttype)
213 223 if proc is None:
214 224 proc = engines[ttype](self.load, self.filters, self.defaults)
215 225 self.engines[ttype] = proc
216 226
217 227 stream = proc.process(t, mapping)
218 228 if self.minchunk:
219 229 stream = util.increasingchunks(stream, min=self.minchunk,
220 230 max=self.maxchunk)
221 231 return stream
222 232
223 233 def templatepath(name=None):
224 234 '''return location of template file or directory (if no name).
225 235 returns None if not found.'''
226 236 normpaths = []
227 237
228 238 # executable version (py2exe) doesn't support __file__
229 239 if hasattr(sys, 'frozen'):
230 240 module = sys.executable
231 241 else:
232 242 module = __file__
233 243 for f in path:
234 244 if f.startswith('/'):
235 245 p = f
236 246 else:
237 247 fl = f.split('/')
238 248 p = os.path.join(os.path.dirname(module), *fl)
239 249 if name:
240 250 p = os.path.join(p, name)
241 251 if name and os.path.exists(p):
242 252 return os.path.normpath(p)
243 253 elif os.path.isdir(p):
244 254 normpaths.append(os.path.normpath(p))
245 255
246 256 return normpaths
247 257
248 258 def stylemap(styles, paths=None):
249 259 """Return path to mapfile for a given style.
250 260
251 261 Searches mapfile in the following locations:
252 262 1. templatepath/style/map
253 263 2. templatepath/map-style
254 264 3. templatepath/map
255 265 """
256 266
257 267 if paths is None:
258 268 paths = templatepath()
259 269 elif isinstance(paths, str):
260 270 paths = [paths]
261 271
262 272 if isinstance(styles, str):
263 273 styles = [styles]
264 274
265 275 for style in styles:
266 276 if not style:
267 277 continue
268 278 locations = [os.path.join(style, 'map'), 'map-' + style]
269 279 locations.append('map')
270 280
271 281 for path in paths:
272 282 for location in locations:
273 283 mapfile = os.path.join(path, location)
274 284 if os.path.isfile(mapfile):
275 285 return style, mapfile
276 286
277 287 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now