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