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