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