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