##// END OF EJS Templates
templatefilters: strings are iterators, but shouldn't be joined anyway
Dirkjan Ochtman -
r8015:b5db7dcc default
parent child Browse files
Show More
@@ -1,214 +1,214 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
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 import re, sys, os
9 import re, sys, os
10 from mercurial import util
10 from mercurial import util
11
11
12 path = ['templates', '../templates']
12 path = ['templates', '../templates']
13
13
14 def parsestring(s, quoted=True):
14 def parsestring(s, quoted=True):
15 '''parse a string using simple c-like syntax.
15 '''parse a string using simple c-like syntax.
16 string must be in quotes if quoted is True.'''
16 string must be in quotes if quoted is True.'''
17 if quoted:
17 if quoted:
18 if len(s) < 2 or s[0] != s[-1]:
18 if len(s) < 2 or s[0] != s[-1]:
19 raise SyntaxError(_('unmatched quotes'))
19 raise SyntaxError(_('unmatched quotes'))
20 return s[1:-1].decode('string_escape')
20 return s[1:-1].decode('string_escape')
21
21
22 return s.decode('string_escape')
22 return s.decode('string_escape')
23
23
24 class templater(object):
24 class templater(object):
25 '''template expansion engine.
25 '''template expansion engine.
26
26
27 template expansion works like this. a map file contains key=value
27 template expansion works like this. a map file contains key=value
28 pairs. if value is quoted, it is treated as string. otherwise, it
28 pairs. if value is quoted, it is treated as string. otherwise, it
29 is treated as name of template file.
29 is treated as name of template file.
30
30
31 templater is asked to expand a key in map. it looks up key, and
31 templater is asked to expand a key in map. it looks up key, and
32 looks for strings like this: {foo}. it expands {foo} by looking up
32 looks for strings like this: {foo}. it expands {foo} by looking up
33 foo in map, and substituting it. expansion is recursive: it stops
33 foo in map, and substituting it. expansion is recursive: it stops
34 when there is no more {foo} to replace.
34 when there is no more {foo} to replace.
35
35
36 expansion also allows formatting and filtering.
36 expansion also allows formatting and filtering.
37
37
38 format uses key to expand each item in list. syntax is
38 format uses key to expand each item in list. syntax is
39 {key%format}.
39 {key%format}.
40
40
41 filter uses function to transform value. syntax is
41 filter uses function to transform value. syntax is
42 {key|filter1|filter2|...}.'''
42 {key|filter1|filter2|...}.'''
43
43
44 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
44 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
45 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
45 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
46
46
47 def __init__(self, mapfile, filters={}, defaults={}, cache={},
47 def __init__(self, mapfile, filters={}, defaults={}, cache={},
48 minchunk=1024, maxchunk=65536):
48 minchunk=1024, maxchunk=65536):
49 '''set up template engine.
49 '''set up template engine.
50 mapfile is name of file to read map definitions from.
50 mapfile is name of file to read map definitions from.
51 filters is dict of functions. each transforms a value into another.
51 filters is dict of functions. each transforms a value into another.
52 defaults is dict of default map definitions.'''
52 defaults is dict of default map definitions.'''
53 self.mapfile = mapfile or 'template'
53 self.mapfile = mapfile or 'template'
54 self.cache = cache.copy()
54 self.cache = cache.copy()
55 self.map = {}
55 self.map = {}
56 self.base = (mapfile and os.path.dirname(mapfile)) or ''
56 self.base = (mapfile and os.path.dirname(mapfile)) or ''
57 self.filters = filters
57 self.filters = filters
58 self.defaults = defaults
58 self.defaults = defaults
59 self.minchunk, self.maxchunk = minchunk, maxchunk
59 self.minchunk, self.maxchunk = minchunk, maxchunk
60
60
61 if not mapfile:
61 if not mapfile:
62 return
62 return
63 if not os.path.exists(mapfile):
63 if not os.path.exists(mapfile):
64 raise util.Abort(_('style not found: %s') % mapfile)
64 raise util.Abort(_('style not found: %s') % mapfile)
65
65
66 i = 0
66 i = 0
67 for l in file(mapfile):
67 for l in file(mapfile):
68 l = l.strip()
68 l = l.strip()
69 i += 1
69 i += 1
70 if not l or l[0] in '#;': continue
70 if not l or l[0] in '#;': continue
71 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
71 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
72 if m:
72 if m:
73 key, val = m.groups()
73 key, val = m.groups()
74 if val[0] in "'\"":
74 if val[0] in "'\"":
75 try:
75 try:
76 self.cache[key] = parsestring(val)
76 self.cache[key] = parsestring(val)
77 except SyntaxError, inst:
77 except SyntaxError, inst:
78 raise SyntaxError('%s:%s: %s' %
78 raise SyntaxError('%s:%s: %s' %
79 (mapfile, i, inst.args[0]))
79 (mapfile, i, inst.args[0]))
80 else:
80 else:
81 self.map[key] = os.path.join(self.base, val)
81 self.map[key] = os.path.join(self.base, val)
82 else:
82 else:
83 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
83 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
84
84
85 def __contains__(self, key):
85 def __contains__(self, key):
86 return key in self.cache or key in self.map
86 return key in self.cache or key in self.map
87
87
88 def _template(self, t):
88 def _template(self, t):
89 '''Get the template for the given template name. Use a local cache.'''
89 '''Get the template for the given template name. Use a local cache.'''
90 if not t in self.cache:
90 if not t in self.cache:
91 try:
91 try:
92 self.cache[t] = file(self.map[t]).read()
92 self.cache[t] = file(self.map[t]).read()
93 except IOError, inst:
93 except IOError, inst:
94 raise IOError(inst.args[0], _('template file %s: %s') %
94 raise IOError(inst.args[0], _('template file %s: %s') %
95 (self.map[t], inst.args[1]))
95 (self.map[t], inst.args[1]))
96 return self.cache[t]
96 return self.cache[t]
97
97
98 def _process(self, tmpl, map):
98 def _process(self, tmpl, map):
99 '''Render a template. Returns a generator.'''
99 '''Render a template. Returns a generator.'''
100 while tmpl:
100 while tmpl:
101 m = self.template_re.search(tmpl)
101 m = self.template_re.search(tmpl)
102 if not m:
102 if not m:
103 yield tmpl
103 yield tmpl
104 break
104 break
105
105
106 start, end = m.span(0)
106 start, end = m.span(0)
107 key, format, fl = m.groups()
107 key, format, fl = m.groups()
108
108
109 if start:
109 if start:
110 yield tmpl[:start]
110 yield tmpl[:start]
111 tmpl = tmpl[end:]
111 tmpl = tmpl[end:]
112
112
113 if key in map:
113 if key in map:
114 v = map[key]
114 v = map[key]
115 else:
115 else:
116 v = self.defaults.get(key, "")
116 v = self.defaults.get(key, "")
117 if callable(v):
117 if callable(v):
118 v = v(**map)
118 v = v(**map)
119 if format:
119 if format:
120 if not hasattr(v, '__iter__'):
120 if not hasattr(v, '__iter__'):
121 raise SyntaxError(_("Error expanding '%s%%%s'")
121 raise SyntaxError(_("Error expanding '%s%%%s'")
122 % (key, format))
122 % (key, format))
123 lm = map.copy()
123 lm = map.copy()
124 for i in v:
124 for i in v:
125 lm.update(i)
125 lm.update(i)
126 t = self._template(format)
126 t = self._template(format)
127 yield self._process(t, lm)
127 yield self._process(t, lm)
128 else:
128 else:
129 if fl:
129 if fl:
130 for f in fl.split("|")[1:]:
130 for f in fl.split("|")[1:]:
131 v = self.filters[f](v)
131 v = self.filters[f](v)
132 yield v
132 yield v
133
133
134 def __call__(self, t, **map):
134 def __call__(self, t, **map):
135 stream = self.expand(t, **map)
135 stream = self.expand(t, **map)
136 if self.minchunk:
136 if self.minchunk:
137 stream = util.increasingchunks(stream, min=self.minchunk,
137 stream = util.increasingchunks(stream, min=self.minchunk,
138 max=self.maxchunk)
138 max=self.maxchunk)
139 return stream
139 return stream
140
140
141 def expand(self, t, **map):
141 def expand(self, t, **map):
142 '''Perform expansion. t is name of map element to expand. map contains
142 '''Perform expansion. t is name of map element to expand. map contains
143 added elements for use during expansion. Is a generator.'''
143 added elements for use during expansion. Is a generator.'''
144 tmpl = self._template(t)
144 tmpl = self._template(t)
145 iters = [self._process(tmpl, map)]
145 iters = [self._process(tmpl, map)]
146 while iters:
146 while iters:
147 try:
147 try:
148 item = iters[0].next()
148 item = iters[0].next()
149 except StopIteration:
149 except StopIteration:
150 iters.pop(0)
150 iters.pop(0)
151 continue
151 continue
152 if isinstance(item, str):
152 if isinstance(item, str):
153 yield item
153 yield item
154 elif item is None:
154 elif item is None:
155 yield ''
155 yield ''
156 elif hasattr(item, '__iter__'):
156 elif hasattr(item, '__iter__'):
157 iters.insert(0, iter(item))
157 iters.insert(0, iter(item))
158 else:
158 else:
159 yield str(item)
159 yield str(item)
160
160
161 def templatepath(name=None):
161 def templatepath(name=None):
162 '''return location of template file or directory (if no name).
162 '''return location of template file or directory (if no name).
163 returns None if not found.'''
163 returns None if not found.'''
164 normpaths = []
164 normpaths = []
165
165
166 # executable version (py2exe) doesn't support __file__
166 # executable version (py2exe) doesn't support __file__
167 if hasattr(sys, 'frozen'):
167 if hasattr(sys, 'frozen'):
168 module = sys.executable
168 module = sys.executable
169 else:
169 else:
170 module = __file__
170 module = __file__
171 for f in path:
171 for f in path:
172 if f.startswith('/'):
172 if f.startswith('/'):
173 p = f
173 p = f
174 else:
174 else:
175 fl = f.split('/')
175 fl = f.split('/')
176 p = os.path.join(os.path.dirname(module), *fl)
176 p = os.path.join(os.path.dirname(module), *fl)
177 if name:
177 if name:
178 p = os.path.join(p, name)
178 p = os.path.join(p, name)
179 if name and os.path.exists(p):
179 if name and os.path.exists(p):
180 return os.path.normpath(p)
180 return os.path.normpath(p)
181 elif os.path.isdir(p):
181 elif os.path.isdir(p):
182 normpaths.append(os.path.normpath(p))
182 normpaths.append(os.path.normpath(p))
183
183
184 return normpaths
184 return normpaths
185
185
186 def stylemap(style, paths=None):
186 def stylemap(style, paths=None):
187 """Return path to mapfile for a given style.
187 """Return path to mapfile for a given style.
188
188
189 Searches mapfile in the following locations:
189 Searches mapfile in the following locations:
190 1. templatepath/style/map
190 1. templatepath/style/map
191 2. templatepath/map-style
191 2. templatepath/map-style
192 3. templatepath/map
192 3. templatepath/map
193 """
193 """
194
194
195 if paths is None:
195 if paths is None:
196 paths = templatepath()
196 paths = templatepath()
197 elif isinstance(paths, str):
197 elif isinstance(paths, str):
198 paths = [templatepath]
198 paths = [templatepath]
199
199
200 locations = style and [os.path.join(style, "map"), "map-" + style] or []
200 locations = style and [os.path.join(style, "map"), "map-" + style] or []
201 locations.append("map")
201 locations.append("map")
202 for path in paths:
202 for path in paths:
203 for location in locations:
203 for location in locations:
204 mapfile = os.path.join(path, location)
204 mapfile = os.path.join(path, location)
205 if os.path.isfile(mapfile):
205 if os.path.isfile(mapfile):
206 return mapfile
206 return mapfile
207
207
208 raise RuntimeError("No hgweb templates found in %r" % paths)
208 raise RuntimeError("No hgweb templates found in %r" % paths)
209
209
210 def stringify(thing):
210 def stringify(thing):
211 '''turn nested template iterator into string.'''
211 '''turn nested template iterator into string.'''
212 if hasattr(thing, '__iter__'):
212 if hasattr(thing, '__iter__') and not isinstance(thing, str):
213 return "".join([stringify(t) for t in thing if t is not None])
213 return "".join([stringify(t) for t in thing if t is not None])
214 return str(thing)
214 return str(thing)
General Comments 0
You need to be logged in to leave comments. Login now