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