##// END OF EJS Templates
hide some functions behind lambdas, so demandload is useful.
Vadim Gelfer -
r1912:b288b4bb default
parent child Browse files
Show More
@@ -1,236 +1,236 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 import re
8 import re
9 from demandload import demandload
9 from demandload import demandload
10 from i18n import gettext as _
10 from i18n import gettext as _
11 demandload(globals(), "cStringIO cgi os time urllib util")
11 demandload(globals(), "cStringIO cgi os time urllib util")
12
12
13 esctable = {
13 esctable = {
14 '\\': '\\',
14 '\\': '\\',
15 'r': '\r',
15 'r': '\r',
16 't': '\t',
16 't': '\t',
17 'n': '\n',
17 'n': '\n',
18 'v': '\v',
18 'v': '\v',
19 }
19 }
20
20
21 def parsestring(s, quoted=True):
21 def parsestring(s, quoted=True):
22 '''parse a string using simple c-like syntax.
22 '''parse a string using simple c-like syntax.
23 string must be in quotes if quoted is True.'''
23 string must be in quotes if quoted is True.'''
24 fp = cStringIO.StringIO()
24 fp = cStringIO.StringIO()
25 if quoted:
25 if quoted:
26 first = s[0]
26 first = s[0]
27 if len(s) < 2: raise SyntaxError(_('string too short'))
27 if len(s) < 2: raise SyntaxError(_('string too short'))
28 if first not in "'\"": raise SyntaxError(_('invalid quote'))
28 if first not in "'\"": raise SyntaxError(_('invalid quote'))
29 if s[-1] != first: raise SyntaxError(_('unmatched quotes'))
29 if s[-1] != first: raise SyntaxError(_('unmatched quotes'))
30 s = s[1:-1]
30 s = s[1:-1]
31 escape = False
31 escape = False
32 for c in s:
32 for c in s:
33 if escape:
33 if escape:
34 fp.write(esctable.get(c, c))
34 fp.write(esctable.get(c, c))
35 escape = False
35 escape = False
36 elif c == '\\': escape = True
36 elif c == '\\': escape = True
37 elif quoted and c == first: raise SyntaxError(_('string ends early'))
37 elif quoted and c == first: raise SyntaxError(_('string ends early'))
38 else: fp.write(c)
38 else: fp.write(c)
39 if escape: raise SyntaxError(_('unterminated escape'))
39 if escape: raise SyntaxError(_('unterminated escape'))
40 return fp.getvalue()
40 return fp.getvalue()
41
41
42 class templater(object):
42 class templater(object):
43 '''template expansion engine.
43 '''template expansion engine.
44
44
45 template expansion works like this. a map file contains key=value
45 template expansion works like this. a map file contains key=value
46 pairs. if value is quoted, it is treated as string. otherwise, it
46 pairs. if value is quoted, it is treated as string. otherwise, it
47 is treated as name of template file.
47 is treated as name of template file.
48
48
49 templater is asked to expand a key in map. it looks up key, and
49 templater is asked to expand a key in map. it looks up key, and
50 looks for atrings like this: {foo}. it expands {foo} by looking up
50 looks for atrings like this: {foo}. it expands {foo} by looking up
51 foo in map, and substituting it. expansion is recursive: it stops
51 foo in map, and substituting it. expansion is recursive: it stops
52 when there is no more {foo} to replace.
52 when there is no more {foo} to replace.
53
53
54 expansion also allows formatting and filtering.
54 expansion also allows formatting and filtering.
55
55
56 format uses key to expand each item in list. syntax is
56 format uses key to expand each item in list. syntax is
57 {key%format}.
57 {key%format}.
58
58
59 filter uses function to transform value. syntax is
59 filter uses function to transform value. syntax is
60 {key|filter1|filter2|...}.'''
60 {key|filter1|filter2|...}.'''
61
61
62 def __init__(self, mapfile, filters={}, defaults={}):
62 def __init__(self, mapfile, filters={}, defaults={}):
63 '''set up template engine.
63 '''set up template engine.
64 mapfile is name of file to read map definitions from.
64 mapfile is name of file to read map definitions from.
65 filters is dict of functions. each transforms a value into another.
65 filters is dict of functions. each transforms a value into another.
66 defaults is dict of default map definitions.'''
66 defaults is dict of default map definitions.'''
67 self.mapfile = mapfile or 'template'
67 self.mapfile = mapfile or 'template'
68 self.cache = {}
68 self.cache = {}
69 self.map = {}
69 self.map = {}
70 self.base = (mapfile and os.path.dirname(mapfile)) or ''
70 self.base = (mapfile and os.path.dirname(mapfile)) or ''
71 self.filters = filters
71 self.filters = filters
72 self.defaults = defaults
72 self.defaults = defaults
73
73
74 if not mapfile:
74 if not mapfile:
75 return
75 return
76 i = 0
76 i = 0
77 for l in file(mapfile):
77 for l in file(mapfile):
78 l = l.rstrip('\r\n')
78 l = l.rstrip('\r\n')
79 i += 1
79 i += 1
80 if l.startswith('#') or not l.strip(): continue
80 if l.startswith('#') or not l.strip(): continue
81 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
81 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
82 if m:
82 if m:
83 key, val = m.groups()
83 key, val = m.groups()
84 if val[0] in "'\"":
84 if val[0] in "'\"":
85 try:
85 try:
86 self.cache[key] = parsestring(val)
86 self.cache[key] = parsestring(val)
87 except SyntaxError, inst:
87 except SyntaxError, inst:
88 raise SyntaxError('%s:%s: %s' %
88 raise SyntaxError('%s:%s: %s' %
89 (mapfile, i, inst.args[0]))
89 (mapfile, i, inst.args[0]))
90 else:
90 else:
91 self.map[key] = os.path.join(self.base, val)
91 self.map[key] = os.path.join(self.base, val)
92 else:
92 else:
93 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
93 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
94
94
95 def __contains__(self, key):
95 def __contains__(self, key):
96 return key in self.cache
96 return key in self.cache
97
97
98 def __call__(self, t, **map):
98 def __call__(self, t, **map):
99 '''perform expansion.
99 '''perform expansion.
100 t is name of map element to expand.
100 t is name of map element to expand.
101 map is added elements to use during expansion.'''
101 map is added elements to use during expansion.'''
102 m = self.defaults.copy()
102 m = self.defaults.copy()
103 m.update(map)
103 m.update(map)
104 try:
104 try:
105 tmpl = self.cache[t]
105 tmpl = self.cache[t]
106 except KeyError:
106 except KeyError:
107 try:
107 try:
108 tmpl = self.cache[t] = file(self.map[t]).read()
108 tmpl = self.cache[t] = file(self.map[t]).read()
109 except IOError, inst:
109 except IOError, inst:
110 raise IOError(inst.args[0], _('template file %s: %s') %
110 raise IOError(inst.args[0], _('template file %s: %s') %
111 (self.map[t], inst.args[1]))
111 (self.map[t], inst.args[1]))
112 return self.template(tmpl, self.filters, **m)
112 return self.template(tmpl, self.filters, **m)
113
113
114 template_re = re.compile(r"[#{]([a-zA-Z_][a-zA-Z0-9_]*)"
114 template_re = re.compile(r"[#{]([a-zA-Z_][a-zA-Z0-9_]*)"
115 r"((%[a-zA-Z_][a-zA-Z0-9_]*)*)"
115 r"((%[a-zA-Z_][a-zA-Z0-9_]*)*)"
116 r"((\|[a-zA-Z_][a-zA-Z0-9_]*)*)[#}]")
116 r"((\|[a-zA-Z_][a-zA-Z0-9_]*)*)[#}]")
117
117
118 def template(self, tmpl, filters={}, **map):
118 def template(self, tmpl, filters={}, **map):
119 lm = map.copy()
119 lm = map.copy()
120 while tmpl:
120 while tmpl:
121 m = self.template_re.search(tmpl)
121 m = self.template_re.search(tmpl)
122 if m:
122 if m:
123 start, end = m.span(0)
123 start, end = m.span(0)
124 s, e = tmpl[start], tmpl[end - 1]
124 s, e = tmpl[start], tmpl[end - 1]
125 key = m.group(1)
125 key = m.group(1)
126 if ((s == '#' and e != '#') or (s == '{' and e != '}')):
126 if ((s == '#' and e != '#') or (s == '{' and e != '}')):
127 raise SyntaxError(_("'%s'/'%s' mismatch expanding '%s'") %
127 raise SyntaxError(_("'%s'/'%s' mismatch expanding '%s'") %
128 (s, e, key))
128 (s, e, key))
129 if start:
129 if start:
130 yield tmpl[:start]
130 yield tmpl[:start]
131 v = map.get(key, "")
131 v = map.get(key, "")
132 v = callable(v) and v(**map) or v
132 v = callable(v) and v(**map) or v
133
133
134 format = m.group(2)
134 format = m.group(2)
135 fl = m.group(4)
135 fl = m.group(4)
136
136
137 if format:
137 if format:
138 q = v.__iter__
138 q = v.__iter__
139 for i in q():
139 for i in q():
140 lm.update(i)
140 lm.update(i)
141 yield self(format[1:], **lm)
141 yield self(format[1:], **lm)
142
142
143 v = ""
143 v = ""
144
144
145 elif fl:
145 elif fl:
146 for f in fl.split("|")[1:]:
146 for f in fl.split("|")[1:]:
147 v = filters[f](v)
147 v = filters[f](v)
148
148
149 yield v
149 yield v
150 tmpl = tmpl[end:]
150 tmpl = tmpl[end:]
151 else:
151 else:
152 yield tmpl
152 yield tmpl
153 break
153 break
154
154
155 agescales = [("second", 1),
155 agescales = [("second", 1),
156 ("minute", 60),
156 ("minute", 60),
157 ("hour", 3600),
157 ("hour", 3600),
158 ("day", 3600 * 24),
158 ("day", 3600 * 24),
159 ("week", 3600 * 24 * 7),
159 ("week", 3600 * 24 * 7),
160 ("month", 3600 * 24 * 30),
160 ("month", 3600 * 24 * 30),
161 ("year", 3600 * 24 * 365)]
161 ("year", 3600 * 24 * 365)]
162
162
163 agescales.reverse()
163 agescales.reverse()
164
164
165 def age(date):
165 def age(date):
166 '''turn a (timestamp, tzoff) tuple into an age string.'''
166 '''turn a (timestamp, tzoff) tuple into an age string.'''
167
167
168 def plural(t, c):
168 def plural(t, c):
169 if c == 1:
169 if c == 1:
170 return t
170 return t
171 return t + "s"
171 return t + "s"
172 def fmt(t, c):
172 def fmt(t, c):
173 return "%d %s" % (c, plural(t, c))
173 return "%d %s" % (c, plural(t, c))
174
174
175 now = time.time()
175 now = time.time()
176 then = date[0]
176 then = date[0]
177 delta = max(1, int(now - then))
177 delta = max(1, int(now - then))
178
178
179 for t, s in agescales:
179 for t, s in agescales:
180 n = delta / s
180 n = delta / s
181 if n >= 2 or s == 1:
181 if n >= 2 or s == 1:
182 return fmt(t, n)
182 return fmt(t, n)
183
183
184 def isodate(date):
184 def isodate(date):
185 '''turn a (timestamp, tzoff) tuple into an iso 8631 date.'''
185 '''turn a (timestamp, tzoff) tuple into an iso 8631 date.'''
186 return util.datestr(date, format='%Y-%m-%d %H:%M')
186 return util.datestr(date, format='%Y-%m-%d %H:%M')
187
187
188 def nl2br(text):
188 def nl2br(text):
189 '''replace raw newlines with xhtml line breaks.'''
189 '''replace raw newlines with xhtml line breaks.'''
190 return text.replace('\n', '<br/>\n')
190 return text.replace('\n', '<br/>\n')
191
191
192 def obfuscate(text):
192 def obfuscate(text):
193 return ''.join(['&#%d;' % ord(c) for c in text])
193 return ''.join(['&#%d;' % ord(c) for c in text])
194
194
195 def domain(author):
195 def domain(author):
196 '''get domain of author, or empty string if none.'''
196 '''get domain of author, or empty string if none.'''
197 f = author.find('@')
197 f = author.find('@')
198 if f == -1: return ''
198 if f == -1: return ''
199 author = author[f+1:]
199 author = author[f+1:]
200 f = author.find('>')
200 f = author.find('>')
201 if f >= 0: author = author[:f]
201 if f >= 0: author = author[:f]
202 return author
202 return author
203
203
204 def person(author):
204 def person(author):
205 '''get name of author, or else username.'''
205 '''get name of author, or else username.'''
206 f = author.find('<')
206 f = author.find('<')
207 if f == -1: return util.shortuser(author)
207 if f == -1: return util.shortuser(author)
208 return author[:f].rstrip()
208 return author[:f].rstrip()
209
209
210 common_filters = {
210 common_filters = {
211 "addbreaks": nl2br,
211 "addbreaks": nl2br,
212 "age": age,
212 "age": age,
213 "date": lambda x: util.datestr(x),
213 "date": lambda x: util.datestr(x),
214 "domain": domain,
214 "domain": domain,
215 "escape": lambda x: cgi.escape(x, True),
215 "escape": lambda x: cgi.escape(x, True),
216 "firstline": (lambda x: x.splitlines(1)[0]),
216 "firstline": lambda x: x.splitlines(1)[0].rstrip('\r\n'),
217 "isodate": isodate,
217 "isodate": isodate,
218 "obfuscate": obfuscate,
218 "obfuscate": obfuscate,
219 "permissions": (lambda x: x and "-rwxr-xr-x" or "-rw-r--r--"),
219 "permissions": lambda x: x and "-rwxr-xr-x" or "-rw-r--r--",
220 "person": person,
220 "person": person,
221 "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"),
221 "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"),
222 "short": (lambda x: x[:12]),
222 "short": lambda x: x[:12],
223 "strip": lambda x: x.strip(),
223 "strip": lambda x: x.strip(),
224 "urlescape": urllib.quote,
224 "urlescape": lambda x: urllib.quote(x),
225 "user": util.shortuser,
225 "user": lambda x: util.shortuser(x),
226 }
226 }
227
227
228 def templatepath(name=None):
228 def templatepath(name=None):
229 '''return location of template file or directory (if no name).
229 '''return location of template file or directory (if no name).
230 returns None if not found.'''
230 returns None if not found.'''
231 for f in 'templates', '../templates':
231 for f in 'templates', '../templates':
232 fl = f.split('/')
232 fl = f.split('/')
233 if name: fl.append(name)
233 if name: fl.append(name)
234 p = os.path.join(os.path.dirname(__file__), *fl)
234 p = os.path.join(os.path.dirname(__file__), *fl)
235 if (name and os.path.exists(p)) or os.path.isdir(p):
235 if (name and os.path.exists(p)) or os.path.isdir(p):
236 return os.path.normpath(p)
236 return os.path.normpath(p)
General Comments 0
You need to be logged in to leave comments. Login now