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