##// END OF EJS Templates
templater: strip whitespace inside template methods
Matt Mackall -
r10854:40366345 default
parent child Browse files
Show More
@@ -1,285 +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 _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 i is not None:
20 if i 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 key, t = expr.split('%')
141 key, t = expr.split('%')
142 parsed.append((self._format, (key, self._load(t))))
142 parsed.append((self._format, (key.strip(),
143 self._load(t.strip()))))
143 elif '|' in expr:
144 elif '|' in expr:
144 parts = expr.split('|')
145 parts = expr.split('|')
145 val = parts[0]
146 val = parts[0].strip()
146 try:
147 try:
147 filters = [self._filters[f] for f in parts[1:]]
148 filters = [self._filters[f.strip()] for f in parts[1:]]
148 except KeyError, i:
149 except KeyError, i:
149 raise SyntaxError(_("unknown filter '%s'") % i[0])
150 raise SyntaxError(_("unknown filter '%s'") % i[0])
150 parsed.append((self._filter, (filters, val)))
151 parsed.append((self._filter, (filters, val)))
151 else:
152 else:
152 parsed.append((self._get, expr))
153 parsed.append((self._get, expr.strip()))
153
154
154 return parsed
155 return parsed
155
156
156 def _process(self, parsed, mapping):
157 def _process(self, parsed, mapping):
157 '''Render a template. Returns a generator.'''
158 '''Render a template. Returns a generator.'''
158 for f, e in parsed:
159 for f, e in parsed:
159 if f:
160 if f:
160 yield f(mapping, e)
161 yield f(mapping, e)
161 else:
162 else:
162 yield e
163 yield e
163
164
164 engines = {'default': engine}
165 engines = {'default': engine}
165
166
166 class templater(object):
167 class templater(object):
167
168
168 def __init__(self, mapfile, filters={}, defaults={}, cache={},
169 def __init__(self, mapfile, filters={}, defaults={}, cache={},
169 minchunk=1024, maxchunk=65536):
170 minchunk=1024, maxchunk=65536):
170 '''set up template engine.
171 '''set up template engine.
171 mapfile is name of file to read map definitions from.
172 mapfile is name of file to read map definitions from.
172 filters is dict of functions. each transforms a value into another.
173 filters is dict of functions. each transforms a value into another.
173 defaults is dict of default map definitions.'''
174 defaults is dict of default map definitions.'''
174 self.mapfile = mapfile or 'template'
175 self.mapfile = mapfile or 'template'
175 self.cache = cache.copy()
176 self.cache = cache.copy()
176 self.map = {}
177 self.map = {}
177 self.base = (mapfile and os.path.dirname(mapfile)) or ''
178 self.base = (mapfile and os.path.dirname(mapfile)) or ''
178 self.filters = templatefilters.filters.copy()
179 self.filters = templatefilters.filters.copy()
179 self.filters.update(filters)
180 self.filters.update(filters)
180 self.defaults = defaults
181 self.defaults = defaults
181 self.minchunk, self.maxchunk = minchunk, maxchunk
182 self.minchunk, self.maxchunk = minchunk, maxchunk
182 self.engines = {}
183 self.engines = {}
183
184
184 if not mapfile:
185 if not mapfile:
185 return
186 return
186 if not os.path.exists(mapfile):
187 if not os.path.exists(mapfile):
187 raise util.Abort(_('style not found: %s') % mapfile)
188 raise util.Abort(_('style not found: %s') % mapfile)
188
189
189 conf = config.config()
190 conf = config.config()
190 conf.read(mapfile)
191 conf.read(mapfile)
191
192
192 for key, val in conf[''].items():
193 for key, val in conf[''].items():
193 if val[0] in "'\"":
194 if val[0] in "'\"":
194 try:
195 try:
195 self.cache[key] = parsestring(val)
196 self.cache[key] = parsestring(val)
196 except SyntaxError, inst:
197 except SyntaxError, inst:
197 raise SyntaxError('%s: %s' %
198 raise SyntaxError('%s: %s' %
198 (conf.source('', key), inst.args[0]))
199 (conf.source('', key), inst.args[0]))
199 else:
200 else:
200 val = 'default', val
201 val = 'default', val
201 if ':' in val[1]:
202 if ':' in val[1]:
202 val = val[1].split(':', 1)
203 val = val[1].split(':', 1)
203 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])
204
205
205 def __contains__(self, key):
206 def __contains__(self, key):
206 return key in self.cache or key in self.map
207 return key in self.cache or key in self.map
207
208
208 def load(self, t):
209 def load(self, t):
209 '''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.'''
210 if not t in self.cache:
211 if not t in self.cache:
211 try:
212 try:
212 self.cache[t] = open(self.map[t][1]).read()
213 self.cache[t] = open(self.map[t][1]).read()
213 except IOError, inst:
214 except IOError, inst:
214 raise IOError(inst.args[0], _('template file %s: %s') %
215 raise IOError(inst.args[0], _('template file %s: %s') %
215 (self.map[t][1], inst.args[1]))
216 (self.map[t][1], inst.args[1]))
216 return self.cache[t]
217 return self.cache[t]
217
218
218 def __call__(self, t, **mapping):
219 def __call__(self, t, **mapping):
219 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'
220 proc = self.engines.get(ttype)
221 proc = self.engines.get(ttype)
221 if proc is None:
222 if proc is None:
222 proc = engines[ttype](self.load, self.filters, self.defaults)
223 proc = engines[ttype](self.load, self.filters, self.defaults)
223 self.engines[ttype] = proc
224 self.engines[ttype] = proc
224
225
225 stream = proc.process(t, mapping)
226 stream = proc.process(t, mapping)
226 if self.minchunk:
227 if self.minchunk:
227 stream = util.increasingchunks(stream, min=self.minchunk,
228 stream = util.increasingchunks(stream, min=self.minchunk,
228 max=self.maxchunk)
229 max=self.maxchunk)
229 return stream
230 return stream
230
231
231 def templatepath(name=None):
232 def templatepath(name=None):
232 '''return location of template file or directory (if no name).
233 '''return location of template file or directory (if no name).
233 returns None if not found.'''
234 returns None if not found.'''
234 normpaths = []
235 normpaths = []
235
236
236 # executable version (py2exe) doesn't support __file__
237 # executable version (py2exe) doesn't support __file__
237 if hasattr(sys, 'frozen'):
238 if hasattr(sys, 'frozen'):
238 module = sys.executable
239 module = sys.executable
239 else:
240 else:
240 module = __file__
241 module = __file__
241 for f in path:
242 for f in path:
242 if f.startswith('/'):
243 if f.startswith('/'):
243 p = f
244 p = f
244 else:
245 else:
245 fl = f.split('/')
246 fl = f.split('/')
246 p = os.path.join(os.path.dirname(module), *fl)
247 p = os.path.join(os.path.dirname(module), *fl)
247 if name:
248 if name:
248 p = os.path.join(p, name)
249 p = os.path.join(p, name)
249 if name and os.path.exists(p):
250 if name and os.path.exists(p):
250 return os.path.normpath(p)
251 return os.path.normpath(p)
251 elif os.path.isdir(p):
252 elif os.path.isdir(p):
252 normpaths.append(os.path.normpath(p))
253 normpaths.append(os.path.normpath(p))
253
254
254 return normpaths
255 return normpaths
255
256
256 def stylemap(styles, paths=None):
257 def stylemap(styles, paths=None):
257 """Return path to mapfile for a given style.
258 """Return path to mapfile for a given style.
258
259
259 Searches mapfile in the following locations:
260 Searches mapfile in the following locations:
260 1. templatepath/style/map
261 1. templatepath/style/map
261 2. templatepath/map-style
262 2. templatepath/map-style
262 3. templatepath/map
263 3. templatepath/map
263 """
264 """
264
265
265 if paths is None:
266 if paths is None:
266 paths = templatepath()
267 paths = templatepath()
267 elif isinstance(paths, str):
268 elif isinstance(paths, str):
268 paths = [paths]
269 paths = [paths]
269
270
270 if isinstance(styles, str):
271 if isinstance(styles, str):
271 styles = [styles]
272 styles = [styles]
272
273
273 for style in styles:
274 for style in styles:
274 if not style:
275 if not style:
275 continue
276 continue
276 locations = [os.path.join(style, 'map'), 'map-' + style]
277 locations = [os.path.join(style, 'map'), 'map-' + style]
277 locations.append('map')
278 locations.append('map')
278
279
279 for path in paths:
280 for path in paths:
280 for location in locations:
281 for location in locations:
281 mapfile = os.path.join(path, location)
282 mapfile = os.path.join(path, location)
282 if os.path.isfile(mapfile):
283 if os.path.isfile(mapfile):
283 return style, mapfile
284 return style, mapfile
284
285
285 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