##// END OF EJS Templates
minor typo fix in templater's docstring
TK Soh -
r4334:66a3fe30 default
parent child Browse files
Show More
@@ -1,291 +1,291 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 from demandload import demandload
9 9 from i18n import gettext as _
10 10 from node import *
11 11 demandload(globals(), "cgi re sys os time urllib util textwrap")
12 12
13 13 def parsestring(s, quoted=True):
14 14 '''parse a string using simple c-like syntax.
15 15 string must be in quotes if quoted is True.'''
16 16 if quoted:
17 17 if len(s) < 2 or s[0] != s[-1]:
18 18 raise SyntaxError(_('unmatched quotes'))
19 19 return s[1:-1].decode('string_escape')
20 20
21 21 return s.decode('string_escape')
22 22
23 23 class templater(object):
24 24 '''template expansion engine.
25 25
26 26 template expansion works like this. a map file contains key=value
27 27 pairs. if value is quoted, it is treated as string. otherwise, it
28 28 is treated as name of template file.
29 29
30 30 templater is asked to expand a key in map. it looks up key, and
31 looks for atrings like this: {foo}. it expands {foo} by looking up
31 looks for strings like this: {foo}. it expands {foo} by looking up
32 32 foo in map, and substituting it. expansion is recursive: it stops
33 33 when there is no more {foo} to replace.
34 34
35 35 expansion also allows formatting and filtering.
36 36
37 37 format uses key to expand each item in list. syntax is
38 38 {key%format}.
39 39
40 40 filter uses function to transform value. syntax is
41 41 {key|filter1|filter2|...}.'''
42 42
43 43 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
44 44 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
45 45
46 46 def __init__(self, mapfile, filters={}, defaults={}, cache={}):
47 47 '''set up template engine.
48 48 mapfile is name of file to read map definitions from.
49 49 filters is dict of functions. each transforms a value into another.
50 50 defaults is dict of default map definitions.'''
51 51 self.mapfile = mapfile or 'template'
52 52 self.cache = cache.copy()
53 53 self.map = {}
54 54 self.base = (mapfile and os.path.dirname(mapfile)) or ''
55 55 self.filters = filters
56 56 self.defaults = defaults
57 57
58 58 if not mapfile:
59 59 return
60 60 i = 0
61 61 for l in file(mapfile):
62 62 l = l.strip()
63 63 i += 1
64 64 if not l or l[0] in '#;': continue
65 65 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
66 66 if m:
67 67 key, val = m.groups()
68 68 if val[0] in "'\"":
69 69 try:
70 70 self.cache[key] = parsestring(val)
71 71 except SyntaxError, inst:
72 72 raise SyntaxError('%s:%s: %s' %
73 73 (mapfile, i, inst.args[0]))
74 74 else:
75 75 self.map[key] = os.path.join(self.base, val)
76 76 else:
77 77 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
78 78
79 79 def __contains__(self, key):
80 80 return key in self.cache or key in self.map
81 81
82 82 def __call__(self, t, **map):
83 83 '''perform expansion.
84 84 t is name of map element to expand.
85 85 map is added elements to use during expansion.'''
86 86 if not self.cache.has_key(t):
87 87 try:
88 88 self.cache[t] = file(self.map[t]).read()
89 89 except IOError, inst:
90 90 raise IOError(inst.args[0], _('template file %s: %s') %
91 91 (self.map[t], inst.args[1]))
92 92 tmpl = self.cache[t]
93 93
94 94 while tmpl:
95 95 m = self.template_re.search(tmpl)
96 96 if not m:
97 97 yield tmpl
98 98 break
99 99
100 100 start, end = m.span(0)
101 101 key, format, fl = m.groups()
102 102
103 103 if start:
104 104 yield tmpl[:start]
105 105 tmpl = tmpl[end:]
106 106
107 107 if key in map:
108 108 v = map[key]
109 109 else:
110 110 v = self.defaults.get(key, "")
111 111 if callable(v):
112 112 v = v(**map)
113 113 if format:
114 114 if not hasattr(v, '__iter__'):
115 115 raise SyntaxError(_("Error expanding '%s%s'")
116 116 % (key, format))
117 117 lm = map.copy()
118 118 for i in v:
119 119 lm.update(i)
120 120 yield self(format, **lm)
121 121 else:
122 122 if fl:
123 123 for f in fl.split("|")[1:]:
124 124 v = self.filters[f](v)
125 125 yield v
126 126
127 127 agescales = [("second", 1),
128 128 ("minute", 60),
129 129 ("hour", 3600),
130 130 ("day", 3600 * 24),
131 131 ("week", 3600 * 24 * 7),
132 132 ("month", 3600 * 24 * 30),
133 133 ("year", 3600 * 24 * 365)]
134 134
135 135 agescales.reverse()
136 136
137 137 def age(date):
138 138 '''turn a (timestamp, tzoff) tuple into an age string.'''
139 139
140 140 def plural(t, c):
141 141 if c == 1:
142 142 return t
143 143 return t + "s"
144 144 def fmt(t, c):
145 145 return "%d %s" % (c, plural(t, c))
146 146
147 147 now = time.time()
148 148 then = date[0]
149 149 delta = max(1, int(now - then))
150 150
151 151 for t, s in agescales:
152 152 n = delta / s
153 153 if n >= 2 or s == 1:
154 154 return fmt(t, n)
155 155
156 156 def stringify(thing):
157 157 '''turn nested template iterator into string.'''
158 158 if hasattr(thing, '__iter__'):
159 159 return "".join([stringify(t) for t in thing if t is not None])
160 160 return str(thing)
161 161
162 162 para_re = None
163 163 space_re = None
164 164
165 165 def fill(text, width):
166 166 '''fill many paragraphs.'''
167 167 global para_re, space_re
168 168 if para_re is None:
169 169 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
170 170 space_re = re.compile(r' +')
171 171
172 172 def findparas():
173 173 start = 0
174 174 while True:
175 175 m = para_re.search(text, start)
176 176 if not m:
177 177 w = len(text)
178 178 while w > start and text[w-1].isspace(): w -= 1
179 179 yield text[start:w], text[w:]
180 180 break
181 181 yield text[start:m.start(0)], m.group(1)
182 182 start = m.end(1)
183 183
184 184 return "".join([space_re.sub(' ', textwrap.fill(para, width)) + rest
185 185 for para, rest in findparas()])
186 186
187 187 def firstline(text):
188 188 '''return the first line of text'''
189 189 try:
190 190 return text.splitlines(1)[0].rstrip('\r\n')
191 191 except IndexError:
192 192 return ''
193 193
194 194 def isodate(date):
195 195 '''turn a (timestamp, tzoff) tuple into an iso 8631 date and time.'''
196 196 return util.datestr(date, format='%Y-%m-%d %H:%M')
197 197
198 198 def hgdate(date):
199 199 '''turn a (timestamp, tzoff) tuple into an hg cset timestamp.'''
200 200 return "%d %d" % date
201 201
202 202 def nl2br(text):
203 203 '''replace raw newlines with xhtml line breaks.'''
204 204 return text.replace('\n', '<br/>\n')
205 205
206 206 def obfuscate(text):
207 207 text = unicode(text, util._encoding, 'replace')
208 208 return ''.join(['&#%d;' % ord(c) for c in text])
209 209
210 210 def domain(author):
211 211 '''get domain of author, or empty string if none.'''
212 212 f = author.find('@')
213 213 if f == -1: return ''
214 214 author = author[f+1:]
215 215 f = author.find('>')
216 216 if f >= 0: author = author[:f]
217 217 return author
218 218
219 219 def email(author):
220 220 '''get email of author.'''
221 221 r = author.find('>')
222 222 if r == -1: r = None
223 223 return author[author.find('<')+1:r]
224 224
225 225 def person(author):
226 226 '''get name of author, or else username.'''
227 227 f = author.find('<')
228 228 if f == -1: return util.shortuser(author)
229 229 return author[:f].rstrip()
230 230
231 231 def shortdate(date):
232 232 '''turn (timestamp, tzoff) tuple into iso 8631 date.'''
233 233 return util.datestr(date, format='%Y-%m-%d', timezone=False)
234 234
235 235 def indent(text, prefix):
236 236 '''indent each non-empty line of text after first with prefix.'''
237 237 lines = text.splitlines()
238 238 num_lines = len(lines)
239 239 def indenter():
240 240 for i in xrange(num_lines):
241 241 l = lines[i]
242 242 if i and l.strip():
243 243 yield prefix
244 244 yield l
245 245 if i < num_lines - 1 or text.endswith('\n'):
246 246 yield '\n'
247 247 return "".join(indenter())
248 248
249 249 common_filters = {
250 250 "addbreaks": nl2br,
251 251 "basename": os.path.basename,
252 252 "age": age,
253 253 "date": lambda x: util.datestr(x),
254 254 "domain": domain,
255 255 "email": email,
256 256 "escape": lambda x: cgi.escape(x, True),
257 257 "fill68": lambda x: fill(x, width=68),
258 258 "fill76": lambda x: fill(x, width=76),
259 259 "firstline": firstline,
260 260 "tabindent": lambda x: indent(x, '\t'),
261 261 "hgdate": hgdate,
262 262 "isodate": isodate,
263 263 "obfuscate": obfuscate,
264 264 "permissions": lambda x: x and "-rwxr-xr-x" or "-rw-r--r--",
265 265 "person": person,
266 266 "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"),
267 267 "short": lambda x: x[:12],
268 268 "shortdate": shortdate,
269 269 "stringify": stringify,
270 270 "strip": lambda x: x.strip(),
271 271 "urlescape": lambda x: urllib.quote(x),
272 272 "user": lambda x: util.shortuser(x),
273 273 "stringescape": lambda x: x.encode('string_escape'),
274 274 }
275 275
276 276 def templatepath(name=None):
277 277 '''return location of template file or directory (if no name).
278 278 returns None if not found.'''
279 279
280 280 # executable version (py2exe) doesn't support __file__
281 281 if hasattr(sys, 'frozen'):
282 282 module = sys.executable
283 283 else:
284 284 module = __file__
285 285 for f in 'templates', '../templates':
286 286 fl = f.split('/')
287 287 if name: fl.append(name)
288 288 p = os.path.join(os.path.dirname(module), *fl)
289 289 if (name and os.path.exists(p)) or os.path.isdir(p):
290 290 return os.path.normpath(p)
291 291
General Comments 0
You need to be logged in to leave comments. Login now