##// END OF EJS Templates
templater: add indentation arguments to the fill function
Sean Farley -
r19228:889807c7 default
parent child Browse files
Show More
@@ -1,404 +1,405
1 1 # template-filters.py - common template expansion filters
2 2 #
3 3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import cgi, re, os, time, urllib
9 9 import encoding, node, util
10 10 import hbisect
11 11
12 12 def addbreaks(text):
13 13 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
14 14 every line except the last.
15 15 """
16 16 return text.replace('\n', '<br/>\n')
17 17
18 18 agescales = [("year", 3600 * 24 * 365),
19 19 ("month", 3600 * 24 * 30),
20 20 ("week", 3600 * 24 * 7),
21 21 ("day", 3600 * 24),
22 22 ("hour", 3600),
23 23 ("minute", 60),
24 24 ("second", 1)]
25 25
26 26 def age(date):
27 27 """:age: Date. Returns a human-readable date/time difference between the
28 28 given date/time and the current date/time.
29 29 """
30 30
31 31 def plural(t, c):
32 32 if c == 1:
33 33 return t
34 34 return t + "s"
35 35 def fmt(t, c):
36 36 return "%d %s" % (c, plural(t, c))
37 37
38 38 now = time.time()
39 39 then = date[0]
40 40 future = False
41 41 if then > now:
42 42 future = True
43 43 delta = max(1, int(then - now))
44 44 if delta > agescales[0][1] * 30:
45 45 return 'in the distant future'
46 46 else:
47 47 delta = max(1, int(now - then))
48 48 if delta > agescales[0][1] * 2:
49 49 return util.shortdate(date)
50 50
51 51 for t, s in agescales:
52 52 n = delta // s
53 53 if n >= 2 or s == 1:
54 54 if future:
55 55 return '%s from now' % fmt(t, n)
56 56 return '%s ago' % fmt(t, n)
57 57
58 58 def basename(path):
59 59 """:basename: Any text. Treats the text as a path, and returns the last
60 60 component of the path after splitting by the path separator
61 61 (ignoring trailing separators). For example, "foo/bar/baz" becomes
62 62 "baz" and "foo/bar//" becomes "bar".
63 63 """
64 64 return os.path.basename(path)
65 65
66 66 def datefilter(text):
67 67 """:date: Date. Returns a date in a Unix date format, including the
68 68 timezone: "Mon Sep 04 15:13:13 2006 0700".
69 69 """
70 70 return util.datestr(text)
71 71
72 72 def domain(author):
73 73 """:domain: Any text. Finds the first string that looks like an email
74 74 address, and extracts just the domain component. Example: ``User
75 75 <user@example.com>`` becomes ``example.com``.
76 76 """
77 77 f = author.find('@')
78 78 if f == -1:
79 79 return ''
80 80 author = author[f + 1:]
81 81 f = author.find('>')
82 82 if f >= 0:
83 83 author = author[:f]
84 84 return author
85 85
86 86 def email(text):
87 87 """:email: Any text. Extracts the first string that looks like an email
88 88 address. Example: ``User <user@example.com>`` becomes
89 89 ``user@example.com``.
90 90 """
91 91 return util.email(text)
92 92
93 93 def escape(text):
94 94 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
95 95 and ">" with XML entities, and filters out NUL characters.
96 96 """
97 97 return cgi.escape(text.replace('\0', ''), True)
98 98
99 99 para_re = None
100 100 space_re = None
101 101
102 def fill(text, width):
103 '''fill many paragraphs.'''
102 def fill(text, width, initindent = '', hangindent = ''):
103 '''fill many paragraphs with optional indentation.'''
104 104 global para_re, space_re
105 105 if para_re is None:
106 106 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
107 107 space_re = re.compile(r' +')
108 108
109 109 def findparas():
110 110 start = 0
111 111 while True:
112 112 m = para_re.search(text, start)
113 113 if not m:
114 114 uctext = unicode(text[start:], encoding.encoding)
115 115 w = len(uctext)
116 116 while 0 < w and uctext[w - 1].isspace():
117 117 w -= 1
118 118 yield (uctext[:w].encode(encoding.encoding),
119 119 uctext[w:].encode(encoding.encoding))
120 120 break
121 121 yield text[start:m.start(0)], m.group(1)
122 122 start = m.end(1)
123 123
124 return "".join([space_re.sub(' ', util.wrap(para, width=width)) + rest
124 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
125 width, initindent, hangindent) + rest
125 126 for para, rest in findparas()])
126 127
127 128 def fill68(text):
128 129 """:fill68: Any text. Wraps the text to fit in 68 columns."""
129 130 return fill(text, 68)
130 131
131 132 def fill76(text):
132 133 """:fill76: Any text. Wraps the text to fit in 76 columns."""
133 134 return fill(text, 76)
134 135
135 136 def firstline(text):
136 137 """:firstline: Any text. Returns the first line of text."""
137 138 try:
138 139 return text.splitlines(True)[0].rstrip('\r\n')
139 140 except IndexError:
140 141 return ''
141 142
142 143 def hexfilter(text):
143 144 """:hex: Any text. Convert a binary Mercurial node identifier into
144 145 its long hexadecimal representation.
145 146 """
146 147 return node.hex(text)
147 148
148 149 def hgdate(text):
149 150 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
150 151 25200" (Unix timestamp, timezone offset).
151 152 """
152 153 return "%d %d" % text
153 154
154 155 def isodate(text):
155 156 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
156 157 +0200".
157 158 """
158 159 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
159 160
160 161 def isodatesec(text):
161 162 """:isodatesec: Date. Returns the date in ISO 8601 format, including
162 163 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
163 164 filter.
164 165 """
165 166 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
166 167
167 168 def indent(text, prefix):
168 169 '''indent each non-empty line of text after first with prefix.'''
169 170 lines = text.splitlines()
170 171 num_lines = len(lines)
171 172 endswithnewline = text[-1:] == '\n'
172 173 def indenter():
173 174 for i in xrange(num_lines):
174 175 l = lines[i]
175 176 if i and l.strip():
176 177 yield prefix
177 178 yield l
178 179 if i < num_lines - 1 or endswithnewline:
179 180 yield '\n'
180 181 return "".join(indenter())
181 182
182 183 def json(obj):
183 184 if obj is None or obj is False or obj is True:
184 185 return {None: 'null', False: 'false', True: 'true'}[obj]
185 186 elif isinstance(obj, int) or isinstance(obj, float):
186 187 return str(obj)
187 188 elif isinstance(obj, str):
188 189 u = unicode(obj, encoding.encoding, 'replace')
189 190 return '"%s"' % jsonescape(u)
190 191 elif isinstance(obj, unicode):
191 192 return '"%s"' % jsonescape(obj)
192 193 elif util.safehasattr(obj, 'keys'):
193 194 out = []
194 195 for k, v in obj.iteritems():
195 196 s = '%s: %s' % (json(k), json(v))
196 197 out.append(s)
197 198 return '{' + ', '.join(out) + '}'
198 199 elif util.safehasattr(obj, '__iter__'):
199 200 out = []
200 201 for i in obj:
201 202 out.append(json(i))
202 203 return '[' + ', '.join(out) + ']'
203 204 else:
204 205 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
205 206
206 207 def _uescape(c):
207 208 if ord(c) < 0x80:
208 209 return c
209 210 else:
210 211 return '\\u%04x' % ord(c)
211 212
212 213 _escapes = [
213 214 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
214 215 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
215 216 ]
216 217
217 218 def jsonescape(s):
218 219 for k, v in _escapes:
219 220 s = s.replace(k, v)
220 221 return ''.join(_uescape(c) for c in s)
221 222
222 223 def localdate(text):
223 224 """:localdate: Date. Converts a date to local date."""
224 225 return (util.parsedate(text)[0], util.makedate()[1])
225 226
226 227 def nonempty(str):
227 228 """:nonempty: Any text. Returns '(none)' if the string is empty."""
228 229 return str or "(none)"
229 230
230 231 def obfuscate(text):
231 232 """:obfuscate: Any text. Returns the input text rendered as a sequence of
232 233 XML entities.
233 234 """
234 235 text = unicode(text, encoding.encoding, 'replace')
235 236 return ''.join(['&#%d;' % ord(c) for c in text])
236 237
237 238 def permissions(flags):
238 239 if "l" in flags:
239 240 return "lrwxrwxrwx"
240 241 if "x" in flags:
241 242 return "-rwxr-xr-x"
242 243 return "-rw-r--r--"
243 244
244 245 def person(author):
245 246 """:person: Any text. Returns the name before an email address,
246 247 interpreting it as per RFC 5322.
247 248
248 249 >>> person('foo@bar')
249 250 'foo'
250 251 >>> person('Foo Bar <foo@bar>')
251 252 'Foo Bar'
252 253 >>> person('"Foo Bar" <foo@bar>')
253 254 'Foo Bar'
254 255 >>> person('"Foo \"buz\" Bar" <foo@bar>')
255 256 'Foo "buz" Bar'
256 257 >>> # The following are invalid, but do exist in real-life
257 258 ...
258 259 >>> person('Foo "buz" Bar <foo@bar>')
259 260 'Foo "buz" Bar'
260 261 >>> person('"Foo Bar <foo@bar>')
261 262 'Foo Bar'
262 263 """
263 264 if '@' not in author:
264 265 return author
265 266 f = author.find('<')
266 267 if f != -1:
267 268 return author[:f].strip(' "').replace('\\"', '"')
268 269 f = author.find('@')
269 270 return author[:f].replace('.', ' ')
270 271
271 272 def rfc3339date(text):
272 273 """:rfc3339date: Date. Returns a date using the Internet date format
273 274 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
274 275 """
275 276 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
276 277
277 278 def rfc822date(text):
278 279 """:rfc822date: Date. Returns a date using the same format used in email
279 280 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
280 281 """
281 282 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
282 283
283 284 def short(text):
284 285 """:short: Changeset hash. Returns the short form of a changeset hash,
285 286 i.e. a 12 hexadecimal digit string.
286 287 """
287 288 return text[:12]
288 289
289 290 def shortbisect(text):
290 291 """:shortbisect: Any text. Treats `text` as a bisection status, and
291 292 returns a single-character representing the status (G: good, B: bad,
292 293 S: skipped, U: untested, I: ignored). Returns single space if `text`
293 294 is not a valid bisection status.
294 295 """
295 296 return hbisect.shortlabel(text) or ' '
296 297
297 298 def shortdate(text):
298 299 """:shortdate: Date. Returns a date like "2006-09-18"."""
299 300 return util.shortdate(text)
300 301
301 302 def stringescape(text):
302 303 return text.encode('string_escape')
303 304
304 305 def stringify(thing):
305 306 """:stringify: Any type. Turns the value into text by converting values into
306 307 text and concatenating them.
307 308 """
308 309 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
309 310 return "".join([stringify(t) for t in thing if t is not None])
310 311 return str(thing)
311 312
312 313 def strip(text):
313 314 """:strip: Any text. Strips all leading and trailing whitespace."""
314 315 return text.strip()
315 316
316 317 def stripdir(text):
317 318 """:stripdir: Treat the text as path and strip a directory level, if
318 319 possible. For example, "foo" and "foo/bar" becomes "foo".
319 320 """
320 321 dir = os.path.dirname(text)
321 322 if dir == "":
322 323 return os.path.basename(text)
323 324 else:
324 325 return dir
325 326
326 327 def tabindent(text):
327 328 """:tabindent: Any text. Returns the text, with every line except the
328 329 first starting with a tab character.
329 330 """
330 331 return indent(text, '\t')
331 332
332 333 def urlescape(text):
333 334 """:urlescape: Any text. Escapes all "special" characters. For example,
334 335 "foo bar" becomes "foo%20bar".
335 336 """
336 337 return urllib.quote(text)
337 338
338 339 def userfilter(text):
339 340 """:user: Any text. Returns a short representation of a user name or email
340 341 address."""
341 342 return util.shortuser(text)
342 343
343 344 def emailuser(text):
344 345 """:emailuser: Any text. Returns the user portion of an email address."""
345 346 return util.emailuser(text)
346 347
347 348 def xmlescape(text):
348 349 text = (text
349 350 .replace('&', '&amp;')
350 351 .replace('<', '&lt;')
351 352 .replace('>', '&gt;')
352 353 .replace('"', '&quot;')
353 354 .replace("'", '&#39;')) # &apos; invalid in HTML
354 355 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
355 356
356 357 filters = {
357 358 "addbreaks": addbreaks,
358 359 "age": age,
359 360 "basename": basename,
360 361 "date": datefilter,
361 362 "domain": domain,
362 363 "email": email,
363 364 "escape": escape,
364 365 "fill68": fill68,
365 366 "fill76": fill76,
366 367 "firstline": firstline,
367 368 "hex": hexfilter,
368 369 "hgdate": hgdate,
369 370 "isodate": isodate,
370 371 "isodatesec": isodatesec,
371 372 "json": json,
372 373 "jsonescape": jsonescape,
373 374 "localdate": localdate,
374 375 "nonempty": nonempty,
375 376 "obfuscate": obfuscate,
376 377 "permissions": permissions,
377 378 "person": person,
378 379 "rfc3339date": rfc3339date,
379 380 "rfc822date": rfc822date,
380 381 "short": short,
381 382 "shortbisect": shortbisect,
382 383 "shortdate": shortdate,
383 384 "stringescape": stringescape,
384 385 "stringify": stringify,
385 386 "strip": strip,
386 387 "stripdir": stripdir,
387 388 "tabindent": tabindent,
388 389 "urlescape": urlescape,
389 390 "user": userfilter,
390 391 "emailuser": emailuser,
391 392 "xmlescape": xmlescape,
392 393 }
393 394
394 395 def websub(text, websubtable):
395 396 """:websub: Any text. Only applies to hgweb. Applies the regular
396 397 expression replacements defined in the websub section.
397 398 """
398 399 if websubtable:
399 400 for regexp, format in websubtable:
400 401 text = regexp.sub(format, text)
401 402 return text
402 403
403 404 # tell hggettext to extract docstrings from these functions:
404 405 i18nfunctions = filters.values()
@@ -1,554 +1,565
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 of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import sys, os, re
10 10 import util, config, templatefilters, parser, error
11 11 import types
12 12 import minirst
13 13
14 14 # template parsing
15 15
16 16 elements = {
17 17 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
18 18 ",": (2, None, ("list", 2)),
19 19 "|": (5, None, ("|", 5)),
20 20 "%": (6, None, ("%", 6)),
21 21 ")": (0, None, None),
22 22 "symbol": (0, ("symbol",), None),
23 23 "string": (0, ("string",), None),
24 24 "end": (0, None, None),
25 25 }
26 26
27 27 def tokenizer(data):
28 28 program, start, end = data
29 29 pos = start
30 30 while pos < end:
31 31 c = program[pos]
32 32 if c.isspace(): # skip inter-token whitespace
33 33 pass
34 34 elif c in "(,)%|": # handle simple operators
35 35 yield (c, None, pos)
36 36 elif (c in '"\'' or c == 'r' and
37 37 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
38 38 if c == 'r':
39 39 pos += 1
40 40 c = program[pos]
41 41 decode = False
42 42 else:
43 43 decode = True
44 44 pos += 1
45 45 s = pos
46 46 while pos < end: # find closing quote
47 47 d = program[pos]
48 48 if decode and d == '\\': # skip over escaped characters
49 49 pos += 2
50 50 continue
51 51 if d == c:
52 52 if not decode:
53 53 yield ('string', program[s:pos].replace('\\', r'\\'), s)
54 54 break
55 55 yield ('string', program[s:pos].decode('string-escape'), s)
56 56 break
57 57 pos += 1
58 58 else:
59 59 raise error.ParseError(_("unterminated string"), s)
60 60 elif c.isalnum() or c in '_':
61 61 s = pos
62 62 pos += 1
63 63 while pos < end: # find end of symbol
64 64 d = program[pos]
65 65 if not (d.isalnum() or d == "_"):
66 66 break
67 67 pos += 1
68 68 sym = program[s:pos]
69 69 yield ('symbol', sym, s)
70 70 pos -= 1
71 71 elif c == '}':
72 72 pos += 1
73 73 break
74 74 else:
75 75 raise error.ParseError(_("syntax error"), pos)
76 76 pos += 1
77 77 yield ('end', None, pos)
78 78
79 79 def compiletemplate(tmpl, context):
80 80 parsed = []
81 81 pos, stop = 0, len(tmpl)
82 82 p = parser.parser(tokenizer, elements)
83 83
84 84 while pos < stop:
85 85 n = tmpl.find('{', pos)
86 86 if n < 0:
87 87 parsed.append(("string", tmpl[pos:]))
88 88 break
89 89 if n > 0 and tmpl[n - 1] == '\\':
90 90 # escaped
91 91 parsed.append(("string", tmpl[pos:n - 1] + "{"))
92 92 pos = n + 1
93 93 continue
94 94 if n > pos:
95 95 parsed.append(("string", tmpl[pos:n]))
96 96
97 97 pd = [tmpl, n + 1, stop]
98 98 parseres, pos = p.parse(pd)
99 99 parsed.append(parseres)
100 100
101 101 return [compileexp(e, context) for e in parsed]
102 102
103 103 def compileexp(exp, context):
104 104 t = exp[0]
105 105 if t in methods:
106 106 return methods[t](exp, context)
107 107 raise error.ParseError(_("unknown method '%s'") % t)
108 108
109 109 # template evaluation
110 110
111 111 def getsymbol(exp):
112 112 if exp[0] == 'symbol':
113 113 return exp[1]
114 114 raise error.ParseError(_("expected a symbol"))
115 115
116 116 def getlist(x):
117 117 if not x:
118 118 return []
119 119 if x[0] == 'list':
120 120 return getlist(x[1]) + [x[2]]
121 121 return [x]
122 122
123 123 def getfilter(exp, context):
124 124 f = getsymbol(exp)
125 125 if f not in context._filters:
126 126 raise error.ParseError(_("unknown function '%s'") % f)
127 127 return context._filters[f]
128 128
129 129 def gettemplate(exp, context):
130 130 if exp[0] == 'string':
131 131 return compiletemplate(exp[1], context)
132 132 if exp[0] == 'symbol':
133 133 return context._load(exp[1])
134 134 raise error.ParseError(_("expected template specifier"))
135 135
136 136 def runstring(context, mapping, data):
137 137 return data
138 138
139 139 def runsymbol(context, mapping, key):
140 140 v = mapping.get(key)
141 141 if v is None:
142 142 v = context._defaults.get(key, '')
143 143 if util.safehasattr(v, '__call__'):
144 144 return v(**mapping)
145 145 if isinstance(v, types.GeneratorType):
146 146 v = list(v)
147 147 mapping[key] = v
148 148 return v
149 149 return v
150 150
151 151 def buildfilter(exp, context):
152 152 func, data = compileexp(exp[1], context)
153 153 filt = getfilter(exp[2], context)
154 154 return (runfilter, (func, data, filt))
155 155
156 156 def runfilter(context, mapping, data):
157 157 func, data, filt = data
158 158 try:
159 159 return filt(func(context, mapping, data))
160 160 except (ValueError, AttributeError, TypeError):
161 161 if isinstance(data, tuple):
162 162 dt = data[1]
163 163 else:
164 164 dt = data
165 165 raise util.Abort(_("template filter '%s' is not compatible with "
166 166 "keyword '%s'") % (filt.func_name, dt))
167 167
168 168 def buildmap(exp, context):
169 169 func, data = compileexp(exp[1], context)
170 170 ctmpl = gettemplate(exp[2], context)
171 171 return (runmap, (func, data, ctmpl))
172 172
173 173 def runtemplate(context, mapping, template):
174 174 for func, data in template:
175 175 yield func(context, mapping, data)
176 176
177 177 def runmap(context, mapping, data):
178 178 func, data, ctmpl = data
179 179 d = func(context, mapping, data)
180 180 if util.safehasattr(d, '__call__'):
181 181 d = d()
182 182
183 183 lm = mapping.copy()
184 184
185 185 for i in d:
186 186 if isinstance(i, dict):
187 187 lm.update(i)
188 188 lm['originalnode'] = mapping.get('node')
189 189 yield runtemplate(context, lm, ctmpl)
190 190 else:
191 191 # v is not an iterable of dicts, this happen when 'key'
192 192 # has been fully expanded already and format is useless.
193 193 # If so, return the expanded value.
194 194 yield i
195 195
196 196 def buildfunc(exp, context):
197 197 n = getsymbol(exp[1])
198 198 args = [compileexp(x, context) for x in getlist(exp[2])]
199 199 if n in funcs:
200 200 f = funcs[n]
201 201 return (f, args)
202 202 if n in context._filters:
203 203 if len(args) != 1:
204 204 raise error.ParseError(_("filter %s expects one argument") % n)
205 205 f = context._filters[n]
206 206 return (runfilter, (args[0][0], args[0][1], f))
207 207
208 208 def get(context, mapping, args):
209 209 if len(args) != 2:
210 210 # i18n: "get" is a keyword
211 211 raise error.ParseError(_("get() expects two arguments"))
212 212
213 213 dictarg = args[0][0](context, mapping, args[0][1])
214 214 if not util.safehasattr(dictarg, 'get'):
215 215 # i18n: "get" is a keyword
216 216 raise error.ParseError(_("get() expects a dict as first argument"))
217 217
218 218 key = args[1][0](context, mapping, args[1][1])
219 219 yield dictarg.get(key)
220 220
221 221 def join(context, mapping, args):
222 222 if not (1 <= len(args) <= 2):
223 223 # i18n: "join" is a keyword
224 224 raise error.ParseError(_("join expects one or two arguments"))
225 225
226 226 joinset = args[0][0](context, mapping, args[0][1])
227 227 if util.safehasattr(joinset, '__call__'):
228 228 jf = joinset.joinfmt
229 229 joinset = [jf(x) for x in joinset()]
230 230
231 231 joiner = " "
232 232 if len(args) > 1:
233 233 joiner = args[1][0](context, mapping, args[1][1])
234 234
235 235 first = True
236 236 for x in joinset:
237 237 if first:
238 238 first = False
239 239 else:
240 240 yield joiner
241 241 yield x
242 242
243 243 def sub(context, mapping, args):
244 244 if len(args) != 3:
245 245 # i18n: "sub" is a keyword
246 246 raise error.ParseError(_("sub expects three arguments"))
247 247
248 248 pat = stringify(args[0][0](context, mapping, args[0][1]))
249 249 rpl = stringify(args[1][0](context, mapping, args[1][1]))
250 250 src = stringify(args[2][0](context, mapping, args[2][1]))
251 251 src = stringify(runtemplate(context, mapping,
252 252 compiletemplate(src, context)))
253 253 yield re.sub(pat, rpl, src)
254 254
255 255 def if_(context, mapping, args):
256 256 if not (2 <= len(args) <= 3):
257 257 # i18n: "if" is a keyword
258 258 raise error.ParseError(_("if expects two or three arguments"))
259 259
260 260 test = stringify(args[0][0](context, mapping, args[0][1]))
261 261 if test:
262 262 t = stringify(args[1][0](context, mapping, args[1][1]))
263 263 yield runtemplate(context, mapping, compiletemplate(t, context))
264 264 elif len(args) == 3:
265 265 t = stringify(args[2][0](context, mapping, args[2][1]))
266 266 yield runtemplate(context, mapping, compiletemplate(t, context))
267 267
268 268 def ifeq(context, mapping, args):
269 269 if not (3 <= len(args) <= 4):
270 270 # i18n: "ifeq" is a keyword
271 271 raise error.ParseError(_("ifeq expects three or four arguments"))
272 272
273 273 test = stringify(args[0][0](context, mapping, args[0][1]))
274 274 match = stringify(args[1][0](context, mapping, args[1][1]))
275 275 if test == match:
276 276 t = stringify(args[2][0](context, mapping, args[2][1]))
277 277 yield runtemplate(context, mapping, compiletemplate(t, context))
278 278 elif len(args) == 4:
279 279 t = stringify(args[3][0](context, mapping, args[3][1]))
280 280 yield runtemplate(context, mapping, compiletemplate(t, context))
281 281
282 282 def label(context, mapping, args):
283 283 if len(args) != 2:
284 284 # i18n: "label" is a keyword
285 285 raise error.ParseError(_("label expects two arguments"))
286 286
287 287 # ignore args[0] (the label string) since this is supposed to be a a no-op
288 288 t = stringify(args[1][0](context, mapping, args[1][1]))
289 289 yield runtemplate(context, mapping, compiletemplate(t, context))
290 290
291 291 def rstdoc(context, mapping, args):
292 292 if len(args) != 2:
293 293 # i18n: "rstdoc" is a keyword
294 294 raise error.ParseError(_("rstdoc expects two arguments"))
295 295
296 296 text = stringify(args[0][0](context, mapping, args[0][1]))
297 297 style = stringify(args[1][0](context, mapping, args[1][1]))
298 298
299 299 return minirst.format(text, style=style, keep=['verbose'])
300 300
301 301 def fill(context, mapping, args):
302 if not (1 <= len(args) <= 2):
303 raise error.ParseError(_("fill expects one or two arguments"))
302 if not (1 <= len(args) <= 4):
303 raise error.ParseError(_("fill expects one to four arguments"))
304 304
305 305 text = stringify(args[0][0](context, mapping, args[0][1]))
306 306 width = 76
307 if len(args) == 2:
307 initindent = ''
308 hangindent = ''
309 if 2 <= len(args) <= 4:
308 310 try:
309 311 width = int(stringify(args[1][0](context, mapping, args[1][1])))
310 312 except ValueError:
311 313 raise error.ParseError(_("fill expects an integer width"))
314 try:
315 initindent = stringify(args[2][0](context, mapping, args[2][1]))
316 initindent = stringify(runtemplate(context, mapping,
317 compiletemplate(initindent, context)))
318 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
319 hangindent = stringify(runtemplate(context, mapping,
320 compiletemplate(hangindent, context)))
321 except IndexError:
322 pass
312 323
313 return templatefilters.fill(text, width)
324 return templatefilters.fill(text, width, initindent, hangindent)
314 325
315 326 def date(context, mapping, args):
316 327 if not (1 <= len(args) <= 2):
317 328 raise error.ParseError(_("date expects one or two arguments"))
318 329
319 330 date = args[0][0](context, mapping, args[0][1])
320 331 if len(args) == 2:
321 332 fmt = stringify(args[1][0](context, mapping, args[1][1]))
322 333 return util.datestr(date, fmt)
323 334 return util.datestr(date)
324 335
325 336 methods = {
326 337 "string": lambda e, c: (runstring, e[1]),
327 338 "symbol": lambda e, c: (runsymbol, e[1]),
328 339 "group": lambda e, c: compileexp(e[1], c),
329 340 # ".": buildmember,
330 341 "|": buildfilter,
331 342 "%": buildmap,
332 343 "func": buildfunc,
333 344 }
334 345
335 346 funcs = {
336 347 "get": get,
337 348 "if": if_,
338 349 "ifeq": ifeq,
339 350 "join": join,
340 351 "label": label,
341 352 "rstdoc": rstdoc,
342 353 "sub": sub,
343 354 "fill": fill,
344 355 "date": date,
345 356 }
346 357
347 358 # template engine
348 359
349 360 path = ['templates', '../templates']
350 361 stringify = templatefilters.stringify
351 362
352 363 def _flatten(thing):
353 364 '''yield a single stream from a possibly nested set of iterators'''
354 365 if isinstance(thing, str):
355 366 yield thing
356 367 elif not util.safehasattr(thing, '__iter__'):
357 368 if thing is not None:
358 369 yield str(thing)
359 370 else:
360 371 for i in thing:
361 372 if isinstance(i, str):
362 373 yield i
363 374 elif not util.safehasattr(i, '__iter__'):
364 375 if i is not None:
365 376 yield str(i)
366 377 elif i is not None:
367 378 for j in _flatten(i):
368 379 yield j
369 380
370 381 def parsestring(s, quoted=True):
371 382 '''parse a string using simple c-like syntax.
372 383 string must be in quotes if quoted is True.'''
373 384 if quoted:
374 385 if len(s) < 2 or s[0] != s[-1]:
375 386 raise SyntaxError(_('unmatched quotes'))
376 387 return s[1:-1].decode('string_escape')
377 388
378 389 return s.decode('string_escape')
379 390
380 391 class engine(object):
381 392 '''template expansion engine.
382 393
383 394 template expansion works like this. a map file contains key=value
384 395 pairs. if value is quoted, it is treated as string. otherwise, it
385 396 is treated as name of template file.
386 397
387 398 templater is asked to expand a key in map. it looks up key, and
388 399 looks for strings like this: {foo}. it expands {foo} by looking up
389 400 foo in map, and substituting it. expansion is recursive: it stops
390 401 when there is no more {foo} to replace.
391 402
392 403 expansion also allows formatting and filtering.
393 404
394 405 format uses key to expand each item in list. syntax is
395 406 {key%format}.
396 407
397 408 filter uses function to transform value. syntax is
398 409 {key|filter1|filter2|...}.'''
399 410
400 411 def __init__(self, loader, filters={}, defaults={}):
401 412 self._loader = loader
402 413 self._filters = filters
403 414 self._defaults = defaults
404 415 self._cache = {}
405 416
406 417 def _load(self, t):
407 418 '''load, parse, and cache a template'''
408 419 if t not in self._cache:
409 420 self._cache[t] = compiletemplate(self._loader(t), self)
410 421 return self._cache[t]
411 422
412 423 def process(self, t, mapping):
413 424 '''Perform expansion. t is name of map element to expand.
414 425 mapping contains added elements for use during expansion. Is a
415 426 generator.'''
416 427 return _flatten(runtemplate(self, mapping, self._load(t)))
417 428
418 429 engines = {'default': engine}
419 430
420 431 def stylelist():
421 432 path = templatepath()[0]
422 433 dirlist = os.listdir(path)
423 434 stylelist = []
424 435 for file in dirlist:
425 436 split = file.split(".")
426 437 if split[0] == "map-cmdline":
427 438 stylelist.append(split[1])
428 439 return ", ".join(sorted(stylelist))
429 440
430 441 class templater(object):
431 442
432 443 def __init__(self, mapfile, filters={}, defaults={}, cache={},
433 444 minchunk=1024, maxchunk=65536):
434 445 '''set up template engine.
435 446 mapfile is name of file to read map definitions from.
436 447 filters is dict of functions. each transforms a value into another.
437 448 defaults is dict of default map definitions.'''
438 449 self.mapfile = mapfile or 'template'
439 450 self.cache = cache.copy()
440 451 self.map = {}
441 452 self.base = (mapfile and os.path.dirname(mapfile)) or ''
442 453 self.filters = templatefilters.filters.copy()
443 454 self.filters.update(filters)
444 455 self.defaults = defaults
445 456 self.minchunk, self.maxchunk = minchunk, maxchunk
446 457 self.ecache = {}
447 458
448 459 if not mapfile:
449 460 return
450 461 if not os.path.exists(mapfile):
451 462 raise util.Abort(_("style '%s' not found") % mapfile,
452 463 hint=_("available styles: %s") % stylelist())
453 464
454 465 conf = config.config()
455 466 conf.read(mapfile)
456 467
457 468 for key, val in conf[''].items():
458 469 if not val:
459 470 raise SyntaxError(_('%s: missing value') % conf.source('', key))
460 471 if val[0] in "'\"":
461 472 try:
462 473 self.cache[key] = parsestring(val)
463 474 except SyntaxError, inst:
464 475 raise SyntaxError('%s: %s' %
465 476 (conf.source('', key), inst.args[0]))
466 477 else:
467 478 val = 'default', val
468 479 if ':' in val[1]:
469 480 val = val[1].split(':', 1)
470 481 self.map[key] = val[0], os.path.join(self.base, val[1])
471 482
472 483 def __contains__(self, key):
473 484 return key in self.cache or key in self.map
474 485
475 486 def load(self, t):
476 487 '''Get the template for the given template name. Use a local cache.'''
477 488 if t not in self.cache:
478 489 try:
479 490 self.cache[t] = util.readfile(self.map[t][1])
480 491 except KeyError, inst:
481 492 raise util.Abort(_('"%s" not in template map') % inst.args[0])
482 493 except IOError, inst:
483 494 raise IOError(inst.args[0], _('template file %s: %s') %
484 495 (self.map[t][1], inst.args[1]))
485 496 return self.cache[t]
486 497
487 498 def __call__(self, t, **mapping):
488 499 ttype = t in self.map and self.map[t][0] or 'default'
489 500 if ttype not in self.ecache:
490 501 self.ecache[ttype] = engines[ttype](self.load,
491 502 self.filters, self.defaults)
492 503 proc = self.ecache[ttype]
493 504
494 505 stream = proc.process(t, mapping)
495 506 if self.minchunk:
496 507 stream = util.increasingchunks(stream, min=self.minchunk,
497 508 max=self.maxchunk)
498 509 return stream
499 510
500 511 def templatepath(name=None):
501 512 '''return location of template file or directory (if no name).
502 513 returns None if not found.'''
503 514 normpaths = []
504 515
505 516 # executable version (py2exe) doesn't support __file__
506 517 if util.mainfrozen():
507 518 module = sys.executable
508 519 else:
509 520 module = __file__
510 521 for f in path:
511 522 if f.startswith('/'):
512 523 p = f
513 524 else:
514 525 fl = f.split('/')
515 526 p = os.path.join(os.path.dirname(module), *fl)
516 527 if name:
517 528 p = os.path.join(p, name)
518 529 if name and os.path.exists(p):
519 530 return os.path.normpath(p)
520 531 elif os.path.isdir(p):
521 532 normpaths.append(os.path.normpath(p))
522 533
523 534 return normpaths
524 535
525 536 def stylemap(styles, paths=None):
526 537 """Return path to mapfile for a given style.
527 538
528 539 Searches mapfile in the following locations:
529 540 1. templatepath/style/map
530 541 2. templatepath/map-style
531 542 3. templatepath/map
532 543 """
533 544
534 545 if paths is None:
535 546 paths = templatepath()
536 547 elif isinstance(paths, str):
537 548 paths = [paths]
538 549
539 550 if isinstance(styles, str):
540 551 styles = [styles]
541 552
542 553 for style in styles:
543 554 if not style:
544 555 continue
545 556 locations = [os.path.join(style, 'map'), 'map-' + style]
546 557 locations.append('map')
547 558
548 559 for path in paths:
549 560 for location in locations:
550 561 mapfile = os.path.join(path, location)
551 562 if os.path.isfile(mapfile):
552 563 return style, mapfile
553 564
554 565 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now