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