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