##// END OF EJS Templates
parser: move unescape helper from templater...
Yuya Nishihara -
r26231:87c9c562 default
parent child Browse files
Show More
@@ -1,215 +1,222
1 # parser.py - simple top-down operator precedence parser for mercurial
1 # parser.py - simple top-down operator precedence parser for mercurial
2 #
2 #
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # see http://effbot.org/zone/simple-top-down-parsing.htm and
8 # see http://effbot.org/zone/simple-top-down-parsing.htm and
9 # http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing/
9 # http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing/
10 # for background
10 # for background
11
11
12 # takes a tokenizer and elements
12 # takes a tokenizer and elements
13 # tokenizer is an iterator that returns (type, value, pos) tuples
13 # tokenizer is an iterator that returns (type, value, pos) tuples
14 # elements is a mapping of types to binding strength, primary, prefix, infix
14 # elements is a mapping of types to binding strength, primary, prefix, infix
15 # and suffix actions
15 # and suffix actions
16 # an action is a tree node name, a tree label, and an optional match
16 # an action is a tree node name, a tree label, and an optional match
17 # __call__(program) parses program into a labeled tree
17 # __call__(program) parses program into a labeled tree
18
18
19 from __future__ import absolute_import
19 from __future__ import absolute_import
20
20
21 from .i18n import _
21 from .i18n import _
22 from . import error
22 from . import error
23
23
24 class parser(object):
24 class parser(object):
25 def __init__(self, elements, methods=None):
25 def __init__(self, elements, methods=None):
26 self._elements = elements
26 self._elements = elements
27 self._methods = methods
27 self._methods = methods
28 self.current = None
28 self.current = None
29 def _advance(self):
29 def _advance(self):
30 'advance the tokenizer'
30 'advance the tokenizer'
31 t = self.current
31 t = self.current
32 self.current = next(self._iter, None)
32 self.current = next(self._iter, None)
33 return t
33 return t
34 def _hasnewterm(self):
34 def _hasnewterm(self):
35 'True if next token may start new term'
35 'True if next token may start new term'
36 return any(self._elements[self.current[0]][1:3])
36 return any(self._elements[self.current[0]][1:3])
37 def _match(self, m):
37 def _match(self, m):
38 'make sure the tokenizer matches an end condition'
38 'make sure the tokenizer matches an end condition'
39 if self.current[0] != m:
39 if self.current[0] != m:
40 raise error.ParseError(_("unexpected token: %s") % self.current[0],
40 raise error.ParseError(_("unexpected token: %s") % self.current[0],
41 self.current[2])
41 self.current[2])
42 self._advance()
42 self._advance()
43 def _parseoperand(self, bind, m=None):
43 def _parseoperand(self, bind, m=None):
44 'gather right-hand-side operand until an end condition or binding met'
44 'gather right-hand-side operand until an end condition or binding met'
45 if m and self.current[0] == m:
45 if m and self.current[0] == m:
46 expr = None
46 expr = None
47 else:
47 else:
48 expr = self._parse(bind)
48 expr = self._parse(bind)
49 if m:
49 if m:
50 self._match(m)
50 self._match(m)
51 return expr
51 return expr
52 def _parse(self, bind=0):
52 def _parse(self, bind=0):
53 token, value, pos = self._advance()
53 token, value, pos = self._advance()
54 # handle prefix rules on current token, take as primary if unambiguous
54 # handle prefix rules on current token, take as primary if unambiguous
55 primary, prefix = self._elements[token][1:3]
55 primary, prefix = self._elements[token][1:3]
56 if primary and not (prefix and self._hasnewterm()):
56 if primary and not (prefix and self._hasnewterm()):
57 expr = (primary, value)
57 expr = (primary, value)
58 elif prefix:
58 elif prefix:
59 expr = (prefix[0], self._parseoperand(*prefix[1:]))
59 expr = (prefix[0], self._parseoperand(*prefix[1:]))
60 else:
60 else:
61 raise error.ParseError(_("not a prefix: %s") % token, pos)
61 raise error.ParseError(_("not a prefix: %s") % token, pos)
62 # gather tokens until we meet a lower binding strength
62 # gather tokens until we meet a lower binding strength
63 while bind < self._elements[self.current[0]][0]:
63 while bind < self._elements[self.current[0]][0]:
64 token, value, pos = self._advance()
64 token, value, pos = self._advance()
65 # handle infix rules, take as suffix if unambiguous
65 # handle infix rules, take as suffix if unambiguous
66 infix, suffix = self._elements[token][3:]
66 infix, suffix = self._elements[token][3:]
67 if suffix and not (infix and self._hasnewterm()):
67 if suffix and not (infix and self._hasnewterm()):
68 expr = (suffix[0], expr)
68 expr = (suffix[0], expr)
69 elif infix:
69 elif infix:
70 expr = (infix[0], expr, self._parseoperand(*infix[1:]))
70 expr = (infix[0], expr, self._parseoperand(*infix[1:]))
71 else:
71 else:
72 raise error.ParseError(_("not an infix: %s") % token, pos)
72 raise error.ParseError(_("not an infix: %s") % token, pos)
73 return expr
73 return expr
74 def parse(self, tokeniter):
74 def parse(self, tokeniter):
75 'generate a parse tree from tokens'
75 'generate a parse tree from tokens'
76 self._iter = tokeniter
76 self._iter = tokeniter
77 self._advance()
77 self._advance()
78 res = self._parse()
78 res = self._parse()
79 token, value, pos = self.current
79 token, value, pos = self.current
80 return res, pos
80 return res, pos
81 def eval(self, tree):
81 def eval(self, tree):
82 'recursively evaluate a parse tree using node methods'
82 'recursively evaluate a parse tree using node methods'
83 if not isinstance(tree, tuple):
83 if not isinstance(tree, tuple):
84 return tree
84 return tree
85 return self._methods[tree[0]](*[self.eval(t) for t in tree[1:]])
85 return self._methods[tree[0]](*[self.eval(t) for t in tree[1:]])
86 def __call__(self, tokeniter):
86 def __call__(self, tokeniter):
87 'parse tokens into a parse tree and evaluate if methods given'
87 'parse tokens into a parse tree and evaluate if methods given'
88 t = self.parse(tokeniter)
88 t = self.parse(tokeniter)
89 if self._methods:
89 if self._methods:
90 return self.eval(t)
90 return self.eval(t)
91 return t
91 return t
92
92
93 def buildargsdict(trees, funcname, keys, keyvaluenode, keynode):
93 def buildargsdict(trees, funcname, keys, keyvaluenode, keynode):
94 """Build dict from list containing positional and keyword arguments
94 """Build dict from list containing positional and keyword arguments
95
95
96 Invalid keywords or too many positional arguments are rejected, but
96 Invalid keywords or too many positional arguments are rejected, but
97 missing arguments are just omitted.
97 missing arguments are just omitted.
98 """
98 """
99 if len(trees) > len(keys):
99 if len(trees) > len(keys):
100 raise error.ParseError(_("%(func)s takes at most %(nargs)d arguments")
100 raise error.ParseError(_("%(func)s takes at most %(nargs)d arguments")
101 % {'func': funcname, 'nargs': len(keys)})
101 % {'func': funcname, 'nargs': len(keys)})
102 args = {}
102 args = {}
103 # consume positional arguments
103 # consume positional arguments
104 for k, x in zip(keys, trees):
104 for k, x in zip(keys, trees):
105 if x[0] == keyvaluenode:
105 if x[0] == keyvaluenode:
106 break
106 break
107 args[k] = x
107 args[k] = x
108 # remainder should be keyword arguments
108 # remainder should be keyword arguments
109 for x in trees[len(args):]:
109 for x in trees[len(args):]:
110 if x[0] != keyvaluenode or x[1][0] != keynode:
110 if x[0] != keyvaluenode or x[1][0] != keynode:
111 raise error.ParseError(_("%(func)s got an invalid argument")
111 raise error.ParseError(_("%(func)s got an invalid argument")
112 % {'func': funcname})
112 % {'func': funcname})
113 k = x[1][1]
113 k = x[1][1]
114 if k not in keys:
114 if k not in keys:
115 raise error.ParseError(_("%(func)s got an unexpected keyword "
115 raise error.ParseError(_("%(func)s got an unexpected keyword "
116 "argument '%(key)s'")
116 "argument '%(key)s'")
117 % {'func': funcname, 'key': k})
117 % {'func': funcname, 'key': k})
118 if k in args:
118 if k in args:
119 raise error.ParseError(_("%(func)s got multiple values for keyword "
119 raise error.ParseError(_("%(func)s got multiple values for keyword "
120 "argument '%(key)s'")
120 "argument '%(key)s'")
121 % {'func': funcname, 'key': k})
121 % {'func': funcname, 'key': k})
122 args[k] = x[2]
122 args[k] = x[2]
123 return args
123 return args
124
124
125 def unescapestr(s):
126 try:
127 return s.decode("string_escape")
128 except ValueError as e:
129 # mangle Python's exception into our format
130 raise error.ParseError(str(e).lower())
131
125 def _prettyformat(tree, leafnodes, level, lines):
132 def _prettyformat(tree, leafnodes, level, lines):
126 if not isinstance(tree, tuple) or tree[0] in leafnodes:
133 if not isinstance(tree, tuple) or tree[0] in leafnodes:
127 lines.append((level, str(tree)))
134 lines.append((level, str(tree)))
128 else:
135 else:
129 lines.append((level, '(%s' % tree[0]))
136 lines.append((level, '(%s' % tree[0]))
130 for s in tree[1:]:
137 for s in tree[1:]:
131 _prettyformat(s, leafnodes, level + 1, lines)
138 _prettyformat(s, leafnodes, level + 1, lines)
132 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
139 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
133
140
134 def prettyformat(tree, leafnodes):
141 def prettyformat(tree, leafnodes):
135 lines = []
142 lines = []
136 _prettyformat(tree, leafnodes, 0, lines)
143 _prettyformat(tree, leafnodes, 0, lines)
137 output = '\n'.join((' ' * l + s) for l, s in lines)
144 output = '\n'.join((' ' * l + s) for l, s in lines)
138 return output
145 return output
139
146
140 def simplifyinfixops(tree, targetnodes):
147 def simplifyinfixops(tree, targetnodes):
141 """Flatten chained infix operations to reduce usage of Python stack
148 """Flatten chained infix operations to reduce usage of Python stack
142
149
143 >>> def f(tree):
150 >>> def f(tree):
144 ... print prettyformat(simplifyinfixops(tree, ('or',)), ('symbol',))
151 ... print prettyformat(simplifyinfixops(tree, ('or',)), ('symbol',))
145 >>> f(('or',
152 >>> f(('or',
146 ... ('or',
153 ... ('or',
147 ... ('symbol', '1'),
154 ... ('symbol', '1'),
148 ... ('symbol', '2')),
155 ... ('symbol', '2')),
149 ... ('symbol', '3')))
156 ... ('symbol', '3')))
150 (or
157 (or
151 ('symbol', '1')
158 ('symbol', '1')
152 ('symbol', '2')
159 ('symbol', '2')
153 ('symbol', '3'))
160 ('symbol', '3'))
154 >>> f(('func',
161 >>> f(('func',
155 ... ('symbol', 'p1'),
162 ... ('symbol', 'p1'),
156 ... ('or',
163 ... ('or',
157 ... ('or',
164 ... ('or',
158 ... ('func',
165 ... ('func',
159 ... ('symbol', 'sort'),
166 ... ('symbol', 'sort'),
160 ... ('list',
167 ... ('list',
161 ... ('or',
168 ... ('or',
162 ... ('or',
169 ... ('or',
163 ... ('symbol', '1'),
170 ... ('symbol', '1'),
164 ... ('symbol', '2')),
171 ... ('symbol', '2')),
165 ... ('symbol', '3')),
172 ... ('symbol', '3')),
166 ... ('negate',
173 ... ('negate',
167 ... ('symbol', 'rev')))),
174 ... ('symbol', 'rev')))),
168 ... ('and',
175 ... ('and',
169 ... ('symbol', '4'),
176 ... ('symbol', '4'),
170 ... ('group',
177 ... ('group',
171 ... ('or',
178 ... ('or',
172 ... ('or',
179 ... ('or',
173 ... ('symbol', '5'),
180 ... ('symbol', '5'),
174 ... ('symbol', '6')),
181 ... ('symbol', '6')),
175 ... ('symbol', '7'))))),
182 ... ('symbol', '7'))))),
176 ... ('symbol', '8'))))
183 ... ('symbol', '8'))))
177 (func
184 (func
178 ('symbol', 'p1')
185 ('symbol', 'p1')
179 (or
186 (or
180 (func
187 (func
181 ('symbol', 'sort')
188 ('symbol', 'sort')
182 (list
189 (list
183 (or
190 (or
184 ('symbol', '1')
191 ('symbol', '1')
185 ('symbol', '2')
192 ('symbol', '2')
186 ('symbol', '3'))
193 ('symbol', '3'))
187 (negate
194 (negate
188 ('symbol', 'rev'))))
195 ('symbol', 'rev'))))
189 (and
196 (and
190 ('symbol', '4')
197 ('symbol', '4')
191 (group
198 (group
192 (or
199 (or
193 ('symbol', '5')
200 ('symbol', '5')
194 ('symbol', '6')
201 ('symbol', '6')
195 ('symbol', '7'))))
202 ('symbol', '7'))))
196 ('symbol', '8')))
203 ('symbol', '8')))
197 """
204 """
198 if not isinstance(tree, tuple):
205 if not isinstance(tree, tuple):
199 return tree
206 return tree
200 op = tree[0]
207 op = tree[0]
201 if op not in targetnodes:
208 if op not in targetnodes:
202 return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:])
209 return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:])
203
210
204 # walk down left nodes taking each right node. no recursion to left nodes
211 # walk down left nodes taking each right node. no recursion to left nodes
205 # because infix operators are left-associative, i.e. left tree is deep.
212 # because infix operators are left-associative, i.e. left tree is deep.
206 # e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3)
213 # e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3)
207 simplified = []
214 simplified = []
208 x = tree
215 x = tree
209 while x[0] == op:
216 while x[0] == op:
210 l, r = x[1:]
217 l, r = x[1:]
211 simplified.append(simplifyinfixops(r, targetnodes))
218 simplified.append(simplifyinfixops(r, targetnodes))
212 x = l
219 x = l
213 simplified.append(simplifyinfixops(x, targetnodes))
220 simplified.append(simplifyinfixops(x, targetnodes))
214 simplified.append(op)
221 simplified.append(op)
215 return tuple(reversed(simplified))
222 return tuple(reversed(simplified))
@@ -1,962 +1,955
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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12 import types
12 import types
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 config,
16 config,
17 error,
17 error,
18 minirst,
18 minirst,
19 parser,
19 parser,
20 revset as revsetmod,
20 revset as revsetmod,
21 templatefilters,
21 templatefilters,
22 templatekw,
22 templatekw,
23 util,
23 util,
24 )
24 )
25
25
26 # template parsing
26 # template parsing
27
27
28 elements = {
28 elements = {
29 # token-type: binding-strength, primary, prefix, infix, suffix
29 # token-type: binding-strength, primary, prefix, infix, suffix
30 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
30 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
31 ",": (2, None, None, ("list", 2), None),
31 ",": (2, None, None, ("list", 2), None),
32 "|": (5, None, None, ("|", 5), None),
32 "|": (5, None, None, ("|", 5), None),
33 "%": (6, None, None, ("%", 6), None),
33 "%": (6, None, None, ("%", 6), None),
34 ")": (0, None, None, None, None),
34 ")": (0, None, None, None, None),
35 "integer": (0, "integer", None, None, None),
35 "integer": (0, "integer", None, None, None),
36 "symbol": (0, "symbol", None, None, None),
36 "symbol": (0, "symbol", None, None, None),
37 "string": (0, "string", None, None, None),
37 "string": (0, "string", None, None, None),
38 "template": (0, "template", None, None, None),
38 "template": (0, "template", None, None, None),
39 "end": (0, None, None, None, None),
39 "end": (0, None, None, None, None),
40 }
40 }
41
41
42 def _unescape(s):
43 try:
44 return s.decode("string_escape")
45 except ValueError as e:
46 # mangle Python's exception into our format
47 raise error.ParseError(str(e).lower())
48
49 def tokenize(program, start, end):
42 def tokenize(program, start, end):
50 pos = start
43 pos = start
51 while pos < end:
44 while pos < end:
52 c = program[pos]
45 c = program[pos]
53 if c.isspace(): # skip inter-token whitespace
46 if c.isspace(): # skip inter-token whitespace
54 pass
47 pass
55 elif c in "(,)%|": # handle simple operators
48 elif c in "(,)%|": # handle simple operators
56 yield (c, None, pos)
49 yield (c, None, pos)
57 elif c in '"\'': # handle quoted templates
50 elif c in '"\'': # handle quoted templates
58 s = pos + 1
51 s = pos + 1
59 data, pos = _parsetemplate(program, s, end, c)
52 data, pos = _parsetemplate(program, s, end, c)
60 yield ('template', data, s)
53 yield ('template', data, s)
61 pos -= 1
54 pos -= 1
62 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
55 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
63 # handle quoted strings
56 # handle quoted strings
64 c = program[pos + 1]
57 c = program[pos + 1]
65 s = pos = pos + 2
58 s = pos = pos + 2
66 while pos < end: # find closing quote
59 while pos < end: # find closing quote
67 d = program[pos]
60 d = program[pos]
68 if d == '\\': # skip over escaped characters
61 if d == '\\': # skip over escaped characters
69 pos += 2
62 pos += 2
70 continue
63 continue
71 if d == c:
64 if d == c:
72 yield ('string', program[s:pos], s)
65 yield ('string', program[s:pos], s)
73 break
66 break
74 pos += 1
67 pos += 1
75 else:
68 else:
76 raise error.ParseError(_("unterminated string"), s)
69 raise error.ParseError(_("unterminated string"), s)
77 elif c.isdigit() or c == '-':
70 elif c.isdigit() or c == '-':
78 s = pos
71 s = pos
79 if c == '-': # simply take negate operator as part of integer
72 if c == '-': # simply take negate operator as part of integer
80 pos += 1
73 pos += 1
81 if pos >= end or not program[pos].isdigit():
74 if pos >= end or not program[pos].isdigit():
82 raise error.ParseError(_("integer literal without digits"), s)
75 raise error.ParseError(_("integer literal without digits"), s)
83 pos += 1
76 pos += 1
84 while pos < end:
77 while pos < end:
85 d = program[pos]
78 d = program[pos]
86 if not d.isdigit():
79 if not d.isdigit():
87 break
80 break
88 pos += 1
81 pos += 1
89 yield ('integer', program[s:pos], s)
82 yield ('integer', program[s:pos], s)
90 pos -= 1
83 pos -= 1
91 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
84 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
92 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
85 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
93 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
86 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
94 # where some of nested templates were preprocessed as strings and
87 # where some of nested templates were preprocessed as strings and
95 # then compiled. therefore, \"...\" was allowed. (issue4733)
88 # then compiled. therefore, \"...\" was allowed. (issue4733)
96 #
89 #
97 # processing flow of _evalifliteral() at 5ab28a2e9962:
90 # processing flow of _evalifliteral() at 5ab28a2e9962:
98 # outer template string -> stringify() -> compiletemplate()
91 # outer template string -> stringify() -> compiletemplate()
99 # ------------------------ ------------ ------------------
92 # ------------------------ ------------ ------------------
100 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
93 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
101 # ~~~~~~~~
94 # ~~~~~~~~
102 # escaped quoted string
95 # escaped quoted string
103 if c == 'r':
96 if c == 'r':
104 pos += 1
97 pos += 1
105 token = 'string'
98 token = 'string'
106 else:
99 else:
107 token = 'template'
100 token = 'template'
108 quote = program[pos:pos + 2]
101 quote = program[pos:pos + 2]
109 s = pos = pos + 2
102 s = pos = pos + 2
110 while pos < end: # find closing escaped quote
103 while pos < end: # find closing escaped quote
111 if program.startswith('\\\\\\', pos, end):
104 if program.startswith('\\\\\\', pos, end):
112 pos += 4 # skip over double escaped characters
105 pos += 4 # skip over double escaped characters
113 continue
106 continue
114 if program.startswith(quote, pos, end):
107 if program.startswith(quote, pos, end):
115 # interpret as if it were a part of an outer string
108 # interpret as if it were a part of an outer string
116 data = _unescape(program[s:pos])
109 data = parser.unescapestr(program[s:pos])
117 if token == 'template':
110 if token == 'template':
118 data = _parsetemplate(data, 0, len(data))[0]
111 data = _parsetemplate(data, 0, len(data))[0]
119 yield (token, data, s)
112 yield (token, data, s)
120 pos += 1
113 pos += 1
121 break
114 break
122 pos += 1
115 pos += 1
123 else:
116 else:
124 raise error.ParseError(_("unterminated string"), s)
117 raise error.ParseError(_("unterminated string"), s)
125 elif c.isalnum() or c in '_':
118 elif c.isalnum() or c in '_':
126 s = pos
119 s = pos
127 pos += 1
120 pos += 1
128 while pos < end: # find end of symbol
121 while pos < end: # find end of symbol
129 d = program[pos]
122 d = program[pos]
130 if not (d.isalnum() or d == "_"):
123 if not (d.isalnum() or d == "_"):
131 break
124 break
132 pos += 1
125 pos += 1
133 sym = program[s:pos]
126 sym = program[s:pos]
134 yield ('symbol', sym, s)
127 yield ('symbol', sym, s)
135 pos -= 1
128 pos -= 1
136 elif c == '}':
129 elif c == '}':
137 yield ('end', None, pos + 1)
130 yield ('end', None, pos + 1)
138 return
131 return
139 else:
132 else:
140 raise error.ParseError(_("syntax error"), pos)
133 raise error.ParseError(_("syntax error"), pos)
141 pos += 1
134 pos += 1
142 raise error.ParseError(_("unterminated template expansion"), start)
135 raise error.ParseError(_("unterminated template expansion"), start)
143
136
144 def _parsetemplate(tmpl, start, stop, quote=''):
137 def _parsetemplate(tmpl, start, stop, quote=''):
145 r"""
138 r"""
146 >>> _parsetemplate('foo{bar}"baz', 0, 12)
139 >>> _parsetemplate('foo{bar}"baz', 0, 12)
147 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
140 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
148 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
141 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
149 ([('string', 'foo'), ('symbol', 'bar')], 9)
142 ([('string', 'foo'), ('symbol', 'bar')], 9)
150 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
143 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
151 ([('string', 'foo')], 4)
144 ([('string', 'foo')], 4)
152 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
145 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
153 ([('string', 'foo"'), ('string', 'bar')], 9)
146 ([('string', 'foo"'), ('string', 'bar')], 9)
154 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
147 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
155 ([('string', 'foo\\')], 6)
148 ([('string', 'foo\\')], 6)
156 """
149 """
157 parsed = []
150 parsed = []
158 sepchars = '{' + quote
151 sepchars = '{' + quote
159 pos = start
152 pos = start
160 p = parser.parser(elements)
153 p = parser.parser(elements)
161 while pos < stop:
154 while pos < stop:
162 n = min((tmpl.find(c, pos, stop) for c in sepchars),
155 n = min((tmpl.find(c, pos, stop) for c in sepchars),
163 key=lambda n: (n < 0, n))
156 key=lambda n: (n < 0, n))
164 if n < 0:
157 if n < 0:
165 parsed.append(('string', _unescape(tmpl[pos:stop])))
158 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
166 pos = stop
159 pos = stop
167 break
160 break
168 c = tmpl[n]
161 c = tmpl[n]
169 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
162 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
170 if bs % 2 == 1:
163 if bs % 2 == 1:
171 # escaped (e.g. '\{', '\\\{', but not '\\{')
164 # escaped (e.g. '\{', '\\\{', but not '\\{')
172 parsed.append(('string', _unescape(tmpl[pos:n - 1]) + c))
165 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
173 pos = n + 1
166 pos = n + 1
174 continue
167 continue
175 if n > pos:
168 if n > pos:
176 parsed.append(('string', _unescape(tmpl[pos:n])))
169 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
177 if c == quote:
170 if c == quote:
178 return parsed, n + 1
171 return parsed, n + 1
179
172
180 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop))
173 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop))
181 parsed.append(parseres)
174 parsed.append(parseres)
182
175
183 if quote:
176 if quote:
184 raise error.ParseError(_("unterminated string"), start)
177 raise error.ParseError(_("unterminated string"), start)
185 return parsed, pos
178 return parsed, pos
186
179
187 def compiletemplate(tmpl, context):
180 def compiletemplate(tmpl, context):
188 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
181 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
189 return [compileexp(e, context, methods) for e in parsed]
182 return [compileexp(e, context, methods) for e in parsed]
190
183
191 def compileexp(exp, context, curmethods):
184 def compileexp(exp, context, curmethods):
192 t = exp[0]
185 t = exp[0]
193 if t in curmethods:
186 if t in curmethods:
194 return curmethods[t](exp, context)
187 return curmethods[t](exp, context)
195 raise error.ParseError(_("unknown method '%s'") % t)
188 raise error.ParseError(_("unknown method '%s'") % t)
196
189
197 # template evaluation
190 # template evaluation
198
191
199 def getsymbol(exp):
192 def getsymbol(exp):
200 if exp[0] == 'symbol':
193 if exp[0] == 'symbol':
201 return exp[1]
194 return exp[1]
202 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
195 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
203
196
204 def getlist(x):
197 def getlist(x):
205 if not x:
198 if not x:
206 return []
199 return []
207 if x[0] == 'list':
200 if x[0] == 'list':
208 return getlist(x[1]) + [x[2]]
201 return getlist(x[1]) + [x[2]]
209 return [x]
202 return [x]
210
203
211 def gettemplate(exp, context):
204 def gettemplate(exp, context):
212 if exp[0] == 'template':
205 if exp[0] == 'template':
213 return [compileexp(e, context, methods) for e in exp[1]]
206 return [compileexp(e, context, methods) for e in exp[1]]
214 if exp[0] == 'symbol':
207 if exp[0] == 'symbol':
215 # unlike runsymbol(), here 'symbol' is always taken as template name
208 # unlike runsymbol(), here 'symbol' is always taken as template name
216 # even if it exists in mapping. this allows us to override mapping
209 # even if it exists in mapping. this allows us to override mapping
217 # by web templates, e.g. 'changelogtag' is redefined in map file.
210 # by web templates, e.g. 'changelogtag' is redefined in map file.
218 return context._load(exp[1])
211 return context._load(exp[1])
219 raise error.ParseError(_("expected template specifier"))
212 raise error.ParseError(_("expected template specifier"))
220
213
221 def evalfuncarg(context, mapping, arg):
214 def evalfuncarg(context, mapping, arg):
222 func, data = arg
215 func, data = arg
223 # func() may return string, generator of strings or arbitrary object such
216 # func() may return string, generator of strings or arbitrary object such
224 # as date tuple, but filter does not want generator.
217 # as date tuple, but filter does not want generator.
225 thing = func(context, mapping, data)
218 thing = func(context, mapping, data)
226 if isinstance(thing, types.GeneratorType):
219 if isinstance(thing, types.GeneratorType):
227 thing = stringify(thing)
220 thing = stringify(thing)
228 return thing
221 return thing
229
222
230 def runinteger(context, mapping, data):
223 def runinteger(context, mapping, data):
231 return int(data)
224 return int(data)
232
225
233 def runstring(context, mapping, data):
226 def runstring(context, mapping, data):
234 return data
227 return data
235
228
236 def runsymbol(context, mapping, key):
229 def runsymbol(context, mapping, key):
237 v = mapping.get(key)
230 v = mapping.get(key)
238 if v is None:
231 if v is None:
239 v = context._defaults.get(key)
232 v = context._defaults.get(key)
240 if v is None:
233 if v is None:
241 try:
234 try:
242 v = context.process(key, mapping)
235 v = context.process(key, mapping)
243 except TemplateNotFound:
236 except TemplateNotFound:
244 v = ''
237 v = ''
245 if callable(v):
238 if callable(v):
246 return v(**mapping)
239 return v(**mapping)
247 if isinstance(v, types.GeneratorType):
240 if isinstance(v, types.GeneratorType):
248 v = list(v)
241 v = list(v)
249 return v
242 return v
250
243
251 def buildtemplate(exp, context):
244 def buildtemplate(exp, context):
252 ctmpl = [compileexp(e, context, methods) for e in exp[1]]
245 ctmpl = [compileexp(e, context, methods) for e in exp[1]]
253 if len(ctmpl) == 1:
246 if len(ctmpl) == 1:
254 return ctmpl[0] # fast path for string with no template fragment
247 return ctmpl[0] # fast path for string with no template fragment
255 return (runtemplate, ctmpl)
248 return (runtemplate, ctmpl)
256
249
257 def runtemplate(context, mapping, template):
250 def runtemplate(context, mapping, template):
258 for func, data in template:
251 for func, data in template:
259 yield func(context, mapping, data)
252 yield func(context, mapping, data)
260
253
261 def buildfilter(exp, context):
254 def buildfilter(exp, context):
262 arg = compileexp(exp[1], context, methods)
255 arg = compileexp(exp[1], context, methods)
263 n = getsymbol(exp[2])
256 n = getsymbol(exp[2])
264 if n in context._filters:
257 if n in context._filters:
265 filt = context._filters[n]
258 filt = context._filters[n]
266 return (runfilter, (arg, filt))
259 return (runfilter, (arg, filt))
267 if n in funcs:
260 if n in funcs:
268 f = funcs[n]
261 f = funcs[n]
269 return (f, [arg])
262 return (f, [arg])
270 raise error.ParseError(_("unknown function '%s'") % n)
263 raise error.ParseError(_("unknown function '%s'") % n)
271
264
272 def runfilter(context, mapping, data):
265 def runfilter(context, mapping, data):
273 arg, filt = data
266 arg, filt = data
274 thing = evalfuncarg(context, mapping, arg)
267 thing = evalfuncarg(context, mapping, arg)
275 try:
268 try:
276 return filt(thing)
269 return filt(thing)
277 except (ValueError, AttributeError, TypeError):
270 except (ValueError, AttributeError, TypeError):
278 if isinstance(arg[1], tuple):
271 if isinstance(arg[1], tuple):
279 dt = arg[1][1]
272 dt = arg[1][1]
280 else:
273 else:
281 dt = arg[1]
274 dt = arg[1]
282 raise util.Abort(_("template filter '%s' is not compatible with "
275 raise util.Abort(_("template filter '%s' is not compatible with "
283 "keyword '%s'") % (filt.func_name, dt))
276 "keyword '%s'") % (filt.func_name, dt))
284
277
285 def buildmap(exp, context):
278 def buildmap(exp, context):
286 func, data = compileexp(exp[1], context, methods)
279 func, data = compileexp(exp[1], context, methods)
287 ctmpl = gettemplate(exp[2], context)
280 ctmpl = gettemplate(exp[2], context)
288 return (runmap, (func, data, ctmpl))
281 return (runmap, (func, data, ctmpl))
289
282
290 def runmap(context, mapping, data):
283 def runmap(context, mapping, data):
291 func, data, ctmpl = data
284 func, data, ctmpl = data
292 d = func(context, mapping, data)
285 d = func(context, mapping, data)
293 if callable(d):
286 if callable(d):
294 d = d()
287 d = d()
295
288
296 lm = mapping.copy()
289 lm = mapping.copy()
297
290
298 for i in d:
291 for i in d:
299 if isinstance(i, dict):
292 if isinstance(i, dict):
300 lm.update(i)
293 lm.update(i)
301 lm['originalnode'] = mapping.get('node')
294 lm['originalnode'] = mapping.get('node')
302 yield runtemplate(context, lm, ctmpl)
295 yield runtemplate(context, lm, ctmpl)
303 else:
296 else:
304 # v is not an iterable of dicts, this happen when 'key'
297 # v is not an iterable of dicts, this happen when 'key'
305 # has been fully expanded already and format is useless.
298 # has been fully expanded already and format is useless.
306 # If so, return the expanded value.
299 # If so, return the expanded value.
307 yield i
300 yield i
308
301
309 def buildfunc(exp, context):
302 def buildfunc(exp, context):
310 n = getsymbol(exp[1])
303 n = getsymbol(exp[1])
311 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
304 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
312 if n in funcs:
305 if n in funcs:
313 f = funcs[n]
306 f = funcs[n]
314 return (f, args)
307 return (f, args)
315 if n in context._filters:
308 if n in context._filters:
316 if len(args) != 1:
309 if len(args) != 1:
317 raise error.ParseError(_("filter %s expects one argument") % n)
310 raise error.ParseError(_("filter %s expects one argument") % n)
318 f = context._filters[n]
311 f = context._filters[n]
319 return (runfilter, (args[0], f))
312 return (runfilter, (args[0], f))
320 raise error.ParseError(_("unknown function '%s'") % n)
313 raise error.ParseError(_("unknown function '%s'") % n)
321
314
322 def date(context, mapping, args):
315 def date(context, mapping, args):
323 """:date(date[, fmt]): Format a date. See :hg:`help dates` for formatting
316 """:date(date[, fmt]): Format a date. See :hg:`help dates` for formatting
324 strings. The default is a Unix date format, including the timezone:
317 strings. The default is a Unix date format, including the timezone:
325 "Mon Sep 04 15:13:13 2006 0700"."""
318 "Mon Sep 04 15:13:13 2006 0700"."""
326 if not (1 <= len(args) <= 2):
319 if not (1 <= len(args) <= 2):
327 # i18n: "date" is a keyword
320 # i18n: "date" is a keyword
328 raise error.ParseError(_("date expects one or two arguments"))
321 raise error.ParseError(_("date expects one or two arguments"))
329
322
330 date = args[0][0](context, mapping, args[0][1])
323 date = args[0][0](context, mapping, args[0][1])
331 fmt = None
324 fmt = None
332 if len(args) == 2:
325 if len(args) == 2:
333 fmt = stringify(args[1][0](context, mapping, args[1][1]))
326 fmt = stringify(args[1][0](context, mapping, args[1][1]))
334 try:
327 try:
335 if fmt is None:
328 if fmt is None:
336 return util.datestr(date)
329 return util.datestr(date)
337 else:
330 else:
338 return util.datestr(date, fmt)
331 return util.datestr(date, fmt)
339 except (TypeError, ValueError):
332 except (TypeError, ValueError):
340 # i18n: "date" is a keyword
333 # i18n: "date" is a keyword
341 raise error.ParseError(_("date expects a date information"))
334 raise error.ParseError(_("date expects a date information"))
342
335
343 def diff(context, mapping, args):
336 def diff(context, mapping, args):
344 """:diff([includepattern [, excludepattern]]): Show a diff, optionally
337 """:diff([includepattern [, excludepattern]]): Show a diff, optionally
345 specifying files to include or exclude."""
338 specifying files to include or exclude."""
346 if len(args) > 2:
339 if len(args) > 2:
347 # i18n: "diff" is a keyword
340 # i18n: "diff" is a keyword
348 raise error.ParseError(_("diff expects one, two or no arguments"))
341 raise error.ParseError(_("diff expects one, two or no arguments"))
349
342
350 def getpatterns(i):
343 def getpatterns(i):
351 if i < len(args):
344 if i < len(args):
352 s = stringify(args[i][0](context, mapping, args[i][1])).strip()
345 s = stringify(args[i][0](context, mapping, args[i][1])).strip()
353 if s:
346 if s:
354 return [s]
347 return [s]
355 return []
348 return []
356
349
357 ctx = mapping['ctx']
350 ctx = mapping['ctx']
358 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
351 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
359
352
360 return ''.join(chunks)
353 return ''.join(chunks)
361
354
362 def fill(context, mapping, args):
355 def fill(context, mapping, args):
363 """:fill(text[, width[, initialident[, hangindent]]]): Fill many
356 """:fill(text[, width[, initialident[, hangindent]]]): Fill many
364 paragraphs with optional indentation. See the "fill" filter."""
357 paragraphs with optional indentation. See the "fill" filter."""
365 if not (1 <= len(args) <= 4):
358 if not (1 <= len(args) <= 4):
366 # i18n: "fill" is a keyword
359 # i18n: "fill" is a keyword
367 raise error.ParseError(_("fill expects one to four arguments"))
360 raise error.ParseError(_("fill expects one to four arguments"))
368
361
369 text = stringify(args[0][0](context, mapping, args[0][1]))
362 text = stringify(args[0][0](context, mapping, args[0][1]))
370 width = 76
363 width = 76
371 initindent = ''
364 initindent = ''
372 hangindent = ''
365 hangindent = ''
373 if 2 <= len(args) <= 4:
366 if 2 <= len(args) <= 4:
374 try:
367 try:
375 width = int(stringify(args[1][0](context, mapping, args[1][1])))
368 width = int(stringify(args[1][0](context, mapping, args[1][1])))
376 except ValueError:
369 except ValueError:
377 # i18n: "fill" is a keyword
370 # i18n: "fill" is a keyword
378 raise error.ParseError(_("fill expects an integer width"))
371 raise error.ParseError(_("fill expects an integer width"))
379 try:
372 try:
380 initindent = stringify(args[2][0](context, mapping, args[2][1]))
373 initindent = stringify(args[2][0](context, mapping, args[2][1]))
381 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
374 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
382 except IndexError:
375 except IndexError:
383 pass
376 pass
384
377
385 return templatefilters.fill(text, width, initindent, hangindent)
378 return templatefilters.fill(text, width, initindent, hangindent)
386
379
387 def pad(context, mapping, args):
380 def pad(context, mapping, args):
388 """:pad(text, width[, fillchar=' '[, right=False]]): Pad text with a
381 """:pad(text, width[, fillchar=' '[, right=False]]): Pad text with a
389 fill character."""
382 fill character."""
390 if not (2 <= len(args) <= 4):
383 if not (2 <= len(args) <= 4):
391 # i18n: "pad" is a keyword
384 # i18n: "pad" is a keyword
392 raise error.ParseError(_("pad() expects two to four arguments"))
385 raise error.ParseError(_("pad() expects two to four arguments"))
393
386
394 width = int(args[1][1])
387 width = int(args[1][1])
395
388
396 text = stringify(args[0][0](context, mapping, args[0][1]))
389 text = stringify(args[0][0](context, mapping, args[0][1]))
397
390
398 right = False
391 right = False
399 fillchar = ' '
392 fillchar = ' '
400 if len(args) > 2:
393 if len(args) > 2:
401 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
394 fillchar = stringify(args[2][0](context, mapping, args[2][1]))
402 if len(args) > 3:
395 if len(args) > 3:
403 right = util.parsebool(args[3][1])
396 right = util.parsebool(args[3][1])
404
397
405 if right:
398 if right:
406 return text.rjust(width, fillchar)
399 return text.rjust(width, fillchar)
407 else:
400 else:
408 return text.ljust(width, fillchar)
401 return text.ljust(width, fillchar)
409
402
410 def indent(context, mapping, args):
403 def indent(context, mapping, args):
411 """:indent(text, indentchars[, firstline]): Indents all non-empty lines
404 """:indent(text, indentchars[, firstline]): Indents all non-empty lines
412 with the characters given in the indentchars string. An optional
405 with the characters given in the indentchars string. An optional
413 third parameter will override the indent for the first line only
406 third parameter will override the indent for the first line only
414 if present."""
407 if present."""
415 if not (2 <= len(args) <= 3):
408 if not (2 <= len(args) <= 3):
416 # i18n: "indent" is a keyword
409 # i18n: "indent" is a keyword
417 raise error.ParseError(_("indent() expects two or three arguments"))
410 raise error.ParseError(_("indent() expects two or three arguments"))
418
411
419 text = stringify(args[0][0](context, mapping, args[0][1]))
412 text = stringify(args[0][0](context, mapping, args[0][1]))
420 indent = stringify(args[1][0](context, mapping, args[1][1]))
413 indent = stringify(args[1][0](context, mapping, args[1][1]))
421
414
422 if len(args) == 3:
415 if len(args) == 3:
423 firstline = stringify(args[2][0](context, mapping, args[2][1]))
416 firstline = stringify(args[2][0](context, mapping, args[2][1]))
424 else:
417 else:
425 firstline = indent
418 firstline = indent
426
419
427 # the indent function doesn't indent the first line, so we do it here
420 # the indent function doesn't indent the first line, so we do it here
428 return templatefilters.indent(firstline + text, indent)
421 return templatefilters.indent(firstline + text, indent)
429
422
430 def get(context, mapping, args):
423 def get(context, mapping, args):
431 """:get(dict, key): Get an attribute/key from an object. Some keywords
424 """:get(dict, key): Get an attribute/key from an object. Some keywords
432 are complex types. This function allows you to obtain the value of an
425 are complex types. This function allows you to obtain the value of an
433 attribute on these types."""
426 attribute on these types."""
434 if len(args) != 2:
427 if len(args) != 2:
435 # i18n: "get" is a keyword
428 # i18n: "get" is a keyword
436 raise error.ParseError(_("get() expects two arguments"))
429 raise error.ParseError(_("get() expects two arguments"))
437
430
438 dictarg = args[0][0](context, mapping, args[0][1])
431 dictarg = args[0][0](context, mapping, args[0][1])
439 if not util.safehasattr(dictarg, 'get'):
432 if not util.safehasattr(dictarg, 'get'):
440 # i18n: "get" is a keyword
433 # i18n: "get" is a keyword
441 raise error.ParseError(_("get() expects a dict as first argument"))
434 raise error.ParseError(_("get() expects a dict as first argument"))
442
435
443 key = args[1][0](context, mapping, args[1][1])
436 key = args[1][0](context, mapping, args[1][1])
444 yield dictarg.get(key)
437 yield dictarg.get(key)
445
438
446 def if_(context, mapping, args):
439 def if_(context, mapping, args):
447 """:if(expr, then[, else]): Conditionally execute based on the result of
440 """:if(expr, then[, else]): Conditionally execute based on the result of
448 an expression."""
441 an expression."""
449 if not (2 <= len(args) <= 3):
442 if not (2 <= len(args) <= 3):
450 # i18n: "if" is a keyword
443 # i18n: "if" is a keyword
451 raise error.ParseError(_("if expects two or three arguments"))
444 raise error.ParseError(_("if expects two or three arguments"))
452
445
453 test = stringify(args[0][0](context, mapping, args[0][1]))
446 test = stringify(args[0][0](context, mapping, args[0][1]))
454 if test:
447 if test:
455 yield args[1][0](context, mapping, args[1][1])
448 yield args[1][0](context, mapping, args[1][1])
456 elif len(args) == 3:
449 elif len(args) == 3:
457 yield args[2][0](context, mapping, args[2][1])
450 yield args[2][0](context, mapping, args[2][1])
458
451
459 def ifcontains(context, mapping, args):
452 def ifcontains(context, mapping, args):
460 """:ifcontains(search, thing, then[, else]): Conditionally execute based
453 """:ifcontains(search, thing, then[, else]): Conditionally execute based
461 on whether the item "search" is in "thing"."""
454 on whether the item "search" is in "thing"."""
462 if not (3 <= len(args) <= 4):
455 if not (3 <= len(args) <= 4):
463 # i18n: "ifcontains" is a keyword
456 # i18n: "ifcontains" is a keyword
464 raise error.ParseError(_("ifcontains expects three or four arguments"))
457 raise error.ParseError(_("ifcontains expects three or four arguments"))
465
458
466 item = stringify(args[0][0](context, mapping, args[0][1]))
459 item = stringify(args[0][0](context, mapping, args[0][1]))
467 items = args[1][0](context, mapping, args[1][1])
460 items = args[1][0](context, mapping, args[1][1])
468
461
469 if item in items:
462 if item in items:
470 yield args[2][0](context, mapping, args[2][1])
463 yield args[2][0](context, mapping, args[2][1])
471 elif len(args) == 4:
464 elif len(args) == 4:
472 yield args[3][0](context, mapping, args[3][1])
465 yield args[3][0](context, mapping, args[3][1])
473
466
474 def ifeq(context, mapping, args):
467 def ifeq(context, mapping, args):
475 """:ifeq(expr1, expr2, then[, else]): Conditionally execute based on
468 """:ifeq(expr1, expr2, then[, else]): Conditionally execute based on
476 whether 2 items are equivalent."""
469 whether 2 items are equivalent."""
477 if not (3 <= len(args) <= 4):
470 if not (3 <= len(args) <= 4):
478 # i18n: "ifeq" is a keyword
471 # i18n: "ifeq" is a keyword
479 raise error.ParseError(_("ifeq expects three or four arguments"))
472 raise error.ParseError(_("ifeq expects three or four arguments"))
480
473
481 test = stringify(args[0][0](context, mapping, args[0][1]))
474 test = stringify(args[0][0](context, mapping, args[0][1]))
482 match = stringify(args[1][0](context, mapping, args[1][1]))
475 match = stringify(args[1][0](context, mapping, args[1][1]))
483 if test == match:
476 if test == match:
484 yield args[2][0](context, mapping, args[2][1])
477 yield args[2][0](context, mapping, args[2][1])
485 elif len(args) == 4:
478 elif len(args) == 4:
486 yield args[3][0](context, mapping, args[3][1])
479 yield args[3][0](context, mapping, args[3][1])
487
480
488 def join(context, mapping, args):
481 def join(context, mapping, args):
489 """:join(list, sep): Join items in a list with a delimiter."""
482 """:join(list, sep): Join items in a list with a delimiter."""
490 if not (1 <= len(args) <= 2):
483 if not (1 <= len(args) <= 2):
491 # i18n: "join" is a keyword
484 # i18n: "join" is a keyword
492 raise error.ParseError(_("join expects one or two arguments"))
485 raise error.ParseError(_("join expects one or two arguments"))
493
486
494 joinset = args[0][0](context, mapping, args[0][1])
487 joinset = args[0][0](context, mapping, args[0][1])
495 if callable(joinset):
488 if callable(joinset):
496 jf = joinset.joinfmt
489 jf = joinset.joinfmt
497 joinset = [jf(x) for x in joinset()]
490 joinset = [jf(x) for x in joinset()]
498
491
499 joiner = " "
492 joiner = " "
500 if len(args) > 1:
493 if len(args) > 1:
501 joiner = stringify(args[1][0](context, mapping, args[1][1]))
494 joiner = stringify(args[1][0](context, mapping, args[1][1]))
502
495
503 first = True
496 first = True
504 for x in joinset:
497 for x in joinset:
505 if first:
498 if first:
506 first = False
499 first = False
507 else:
500 else:
508 yield joiner
501 yield joiner
509 yield x
502 yield x
510
503
511 def label(context, mapping, args):
504 def label(context, mapping, args):
512 """:label(label, expr): Apply a label to generated content. Content with
505 """:label(label, expr): Apply a label to generated content. Content with
513 a label applied can result in additional post-processing, such as
506 a label applied can result in additional post-processing, such as
514 automatic colorization."""
507 automatic colorization."""
515 if len(args) != 2:
508 if len(args) != 2:
516 # i18n: "label" is a keyword
509 # i18n: "label" is a keyword
517 raise error.ParseError(_("label expects two arguments"))
510 raise error.ParseError(_("label expects two arguments"))
518
511
519 # ignore args[0] (the label string) since this is supposed to be a a no-op
512 # ignore args[0] (the label string) since this is supposed to be a a no-op
520 yield args[1][0](context, mapping, args[1][1])
513 yield args[1][0](context, mapping, args[1][1])
521
514
522 def localdate(context, mapping, args):
515 def localdate(context, mapping, args):
523 """:localdate(date[, tz]): Converts a date to the specified timezone.
516 """:localdate(date[, tz]): Converts a date to the specified timezone.
524 The default is local date."""
517 The default is local date."""
525 if not (1 <= len(args) <= 2):
518 if not (1 <= len(args) <= 2):
526 # i18n: "localdate" is a keyword
519 # i18n: "localdate" is a keyword
527 raise error.ParseError(_("localdate expects one or two arguments"))
520 raise error.ParseError(_("localdate expects one or two arguments"))
528
521
529 date = evalfuncarg(context, mapping, args[0])
522 date = evalfuncarg(context, mapping, args[0])
530 try:
523 try:
531 date = util.parsedate(date)
524 date = util.parsedate(date)
532 except AttributeError: # not str nor date tuple
525 except AttributeError: # not str nor date tuple
533 # i18n: "localdate" is a keyword
526 # i18n: "localdate" is a keyword
534 raise error.ParseError(_("localdate expects a date information"))
527 raise error.ParseError(_("localdate expects a date information"))
535 if len(args) >= 2:
528 if len(args) >= 2:
536 tzoffset = None
529 tzoffset = None
537 tz = evalfuncarg(context, mapping, args[1])
530 tz = evalfuncarg(context, mapping, args[1])
538 if isinstance(tz, str):
531 if isinstance(tz, str):
539 tzoffset = util.parsetimezone(tz)
532 tzoffset = util.parsetimezone(tz)
540 if tzoffset is None:
533 if tzoffset is None:
541 try:
534 try:
542 tzoffset = int(tz)
535 tzoffset = int(tz)
543 except (TypeError, ValueError):
536 except (TypeError, ValueError):
544 # i18n: "localdate" is a keyword
537 # i18n: "localdate" is a keyword
545 raise error.ParseError(_("localdate expects a timezone"))
538 raise error.ParseError(_("localdate expects a timezone"))
546 else:
539 else:
547 tzoffset = util.makedate()[1]
540 tzoffset = util.makedate()[1]
548 return (date[0], tzoffset)
541 return (date[0], tzoffset)
549
542
550 def revset(context, mapping, args):
543 def revset(context, mapping, args):
551 """:revset(query[, formatargs...]): Execute a revision set query. See
544 """:revset(query[, formatargs...]): Execute a revision set query. See
552 :hg:`help revset`."""
545 :hg:`help revset`."""
553 if not len(args) > 0:
546 if not len(args) > 0:
554 # i18n: "revset" is a keyword
547 # i18n: "revset" is a keyword
555 raise error.ParseError(_("revset expects one or more arguments"))
548 raise error.ParseError(_("revset expects one or more arguments"))
556
549
557 raw = stringify(args[0][0](context, mapping, args[0][1]))
550 raw = stringify(args[0][0](context, mapping, args[0][1]))
558 ctx = mapping['ctx']
551 ctx = mapping['ctx']
559 repo = ctx.repo()
552 repo = ctx.repo()
560
553
561 def query(expr):
554 def query(expr):
562 m = revsetmod.match(repo.ui, expr)
555 m = revsetmod.match(repo.ui, expr)
563 return m(repo)
556 return m(repo)
564
557
565 if len(args) > 1:
558 if len(args) > 1:
566 formatargs = list([a[0](context, mapping, a[1]) for a in args[1:]])
559 formatargs = list([a[0](context, mapping, a[1]) for a in args[1:]])
567 revs = query(revsetmod.formatspec(raw, *formatargs))
560 revs = query(revsetmod.formatspec(raw, *formatargs))
568 revs = list([str(r) for r in revs])
561 revs = list([str(r) for r in revs])
569 else:
562 else:
570 revsetcache = mapping['cache'].setdefault("revsetcache", {})
563 revsetcache = mapping['cache'].setdefault("revsetcache", {})
571 if raw in revsetcache:
564 if raw in revsetcache:
572 revs = revsetcache[raw]
565 revs = revsetcache[raw]
573 else:
566 else:
574 revs = query(raw)
567 revs = query(raw)
575 revs = list([str(r) for r in revs])
568 revs = list([str(r) for r in revs])
576 revsetcache[raw] = revs
569 revsetcache[raw] = revs
577
570
578 return templatekw.showlist("revision", revs, **mapping)
571 return templatekw.showlist("revision", revs, **mapping)
579
572
580 def rstdoc(context, mapping, args):
573 def rstdoc(context, mapping, args):
581 """:rstdoc(text, style): Format ReStructuredText."""
574 """:rstdoc(text, style): Format ReStructuredText."""
582 if len(args) != 2:
575 if len(args) != 2:
583 # i18n: "rstdoc" is a keyword
576 # i18n: "rstdoc" is a keyword
584 raise error.ParseError(_("rstdoc expects two arguments"))
577 raise error.ParseError(_("rstdoc expects two arguments"))
585
578
586 text = stringify(args[0][0](context, mapping, args[0][1]))
579 text = stringify(args[0][0](context, mapping, args[0][1]))
587 style = stringify(args[1][0](context, mapping, args[1][1]))
580 style = stringify(args[1][0](context, mapping, args[1][1]))
588
581
589 return minirst.format(text, style=style, keep=['verbose'])
582 return minirst.format(text, style=style, keep=['verbose'])
590
583
591 def shortest(context, mapping, args):
584 def shortest(context, mapping, args):
592 """:shortest(node, minlength=4): Obtain the shortest representation of
585 """:shortest(node, minlength=4): Obtain the shortest representation of
593 a node."""
586 a node."""
594 if not (1 <= len(args) <= 2):
587 if not (1 <= len(args) <= 2):
595 # i18n: "shortest" is a keyword
588 # i18n: "shortest" is a keyword
596 raise error.ParseError(_("shortest() expects one or two arguments"))
589 raise error.ParseError(_("shortest() expects one or two arguments"))
597
590
598 node = stringify(args[0][0](context, mapping, args[0][1]))
591 node = stringify(args[0][0](context, mapping, args[0][1]))
599
592
600 minlength = 4
593 minlength = 4
601 if len(args) > 1:
594 if len(args) > 1:
602 minlength = int(args[1][1])
595 minlength = int(args[1][1])
603
596
604 cl = mapping['ctx']._repo.changelog
597 cl = mapping['ctx']._repo.changelog
605 def isvalid(test):
598 def isvalid(test):
606 try:
599 try:
607 try:
600 try:
608 cl.index.partialmatch(test)
601 cl.index.partialmatch(test)
609 except AttributeError:
602 except AttributeError:
610 # Pure mercurial doesn't support partialmatch on the index.
603 # Pure mercurial doesn't support partialmatch on the index.
611 # Fallback to the slow way.
604 # Fallback to the slow way.
612 if cl._partialmatch(test) is None:
605 if cl._partialmatch(test) is None:
613 return False
606 return False
614
607
615 try:
608 try:
616 i = int(test)
609 i = int(test)
617 # if we are a pure int, then starting with zero will not be
610 # if we are a pure int, then starting with zero will not be
618 # confused as a rev; or, obviously, if the int is larger than
611 # confused as a rev; or, obviously, if the int is larger than
619 # the value of the tip rev
612 # the value of the tip rev
620 if test[0] == '0' or i > len(cl):
613 if test[0] == '0' or i > len(cl):
621 return True
614 return True
622 return False
615 return False
623 except ValueError:
616 except ValueError:
624 return True
617 return True
625 except error.RevlogError:
618 except error.RevlogError:
626 return False
619 return False
627
620
628 shortest = node
621 shortest = node
629 startlength = max(6, minlength)
622 startlength = max(6, minlength)
630 length = startlength
623 length = startlength
631 while True:
624 while True:
632 test = node[:length]
625 test = node[:length]
633 if isvalid(test):
626 if isvalid(test):
634 shortest = test
627 shortest = test
635 if length == minlength or length > startlength:
628 if length == minlength or length > startlength:
636 return shortest
629 return shortest
637 length -= 1
630 length -= 1
638 else:
631 else:
639 length += 1
632 length += 1
640 if len(shortest) <= length:
633 if len(shortest) <= length:
641 return shortest
634 return shortest
642
635
643 def strip(context, mapping, args):
636 def strip(context, mapping, args):
644 """:strip(text[, chars]): Strip characters from a string. By default,
637 """:strip(text[, chars]): Strip characters from a string. By default,
645 strips all leading and trailing whitespace."""
638 strips all leading and trailing whitespace."""
646 if not (1 <= len(args) <= 2):
639 if not (1 <= len(args) <= 2):
647 # i18n: "strip" is a keyword
640 # i18n: "strip" is a keyword
648 raise error.ParseError(_("strip expects one or two arguments"))
641 raise error.ParseError(_("strip expects one or two arguments"))
649
642
650 text = stringify(args[0][0](context, mapping, args[0][1]))
643 text = stringify(args[0][0](context, mapping, args[0][1]))
651 if len(args) == 2:
644 if len(args) == 2:
652 chars = stringify(args[1][0](context, mapping, args[1][1]))
645 chars = stringify(args[1][0](context, mapping, args[1][1]))
653 return text.strip(chars)
646 return text.strip(chars)
654 return text.strip()
647 return text.strip()
655
648
656 def sub(context, mapping, args):
649 def sub(context, mapping, args):
657 """:sub(pattern, replacement, expression): Perform text substitution
650 """:sub(pattern, replacement, expression): Perform text substitution
658 using regular expressions."""
651 using regular expressions."""
659 if len(args) != 3:
652 if len(args) != 3:
660 # i18n: "sub" is a keyword
653 # i18n: "sub" is a keyword
661 raise error.ParseError(_("sub expects three arguments"))
654 raise error.ParseError(_("sub expects three arguments"))
662
655
663 pat = stringify(args[0][0](context, mapping, args[0][1]))
656 pat = stringify(args[0][0](context, mapping, args[0][1]))
664 rpl = stringify(args[1][0](context, mapping, args[1][1]))
657 rpl = stringify(args[1][0](context, mapping, args[1][1]))
665 src = stringify(args[2][0](context, mapping, args[2][1]))
658 src = stringify(args[2][0](context, mapping, args[2][1]))
666 try:
659 try:
667 patre = re.compile(pat)
660 patre = re.compile(pat)
668 except re.error:
661 except re.error:
669 # i18n: "sub" is a keyword
662 # i18n: "sub" is a keyword
670 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
663 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
671 try:
664 try:
672 yield patre.sub(rpl, src)
665 yield patre.sub(rpl, src)
673 except re.error:
666 except re.error:
674 # i18n: "sub" is a keyword
667 # i18n: "sub" is a keyword
675 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
668 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
676
669
677 def startswith(context, mapping, args):
670 def startswith(context, mapping, args):
678 """:startswith(pattern, text): Returns the value from the "text" argument
671 """:startswith(pattern, text): Returns the value from the "text" argument
679 if it begins with the content from the "pattern" argument."""
672 if it begins with the content from the "pattern" argument."""
680 if len(args) != 2:
673 if len(args) != 2:
681 # i18n: "startswith" is a keyword
674 # i18n: "startswith" is a keyword
682 raise error.ParseError(_("startswith expects two arguments"))
675 raise error.ParseError(_("startswith expects two arguments"))
683
676
684 patn = stringify(args[0][0](context, mapping, args[0][1]))
677 patn = stringify(args[0][0](context, mapping, args[0][1]))
685 text = stringify(args[1][0](context, mapping, args[1][1]))
678 text = stringify(args[1][0](context, mapping, args[1][1]))
686 if text.startswith(patn):
679 if text.startswith(patn):
687 return text
680 return text
688 return ''
681 return ''
689
682
690
683
691 def word(context, mapping, args):
684 def word(context, mapping, args):
692 """:word(number, text[, separator]): Return the nth word from a string."""
685 """:word(number, text[, separator]): Return the nth word from a string."""
693 if not (2 <= len(args) <= 3):
686 if not (2 <= len(args) <= 3):
694 # i18n: "word" is a keyword
687 # i18n: "word" is a keyword
695 raise error.ParseError(_("word expects two or three arguments, got %d")
688 raise error.ParseError(_("word expects two or three arguments, got %d")
696 % len(args))
689 % len(args))
697
690
698 try:
691 try:
699 num = int(stringify(args[0][0](context, mapping, args[0][1])))
692 num = int(stringify(args[0][0](context, mapping, args[0][1])))
700 except ValueError:
693 except ValueError:
701 # i18n: "word" is a keyword
694 # i18n: "word" is a keyword
702 raise error.ParseError(_("word expects an integer index"))
695 raise error.ParseError(_("word expects an integer index"))
703 text = stringify(args[1][0](context, mapping, args[1][1]))
696 text = stringify(args[1][0](context, mapping, args[1][1]))
704 if len(args) == 3:
697 if len(args) == 3:
705 splitter = stringify(args[2][0](context, mapping, args[2][1]))
698 splitter = stringify(args[2][0](context, mapping, args[2][1]))
706 else:
699 else:
707 splitter = None
700 splitter = None
708
701
709 tokens = text.split(splitter)
702 tokens = text.split(splitter)
710 if num >= len(tokens):
703 if num >= len(tokens):
711 return ''
704 return ''
712 else:
705 else:
713 return tokens[num]
706 return tokens[num]
714
707
715 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
708 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
716 exprmethods = {
709 exprmethods = {
717 "integer": lambda e, c: (runinteger, e[1]),
710 "integer": lambda e, c: (runinteger, e[1]),
718 "string": lambda e, c: (runstring, e[1]),
711 "string": lambda e, c: (runstring, e[1]),
719 "symbol": lambda e, c: (runsymbol, e[1]),
712 "symbol": lambda e, c: (runsymbol, e[1]),
720 "template": buildtemplate,
713 "template": buildtemplate,
721 "group": lambda e, c: compileexp(e[1], c, exprmethods),
714 "group": lambda e, c: compileexp(e[1], c, exprmethods),
722 # ".": buildmember,
715 # ".": buildmember,
723 "|": buildfilter,
716 "|": buildfilter,
724 "%": buildmap,
717 "%": buildmap,
725 "func": buildfunc,
718 "func": buildfunc,
726 }
719 }
727
720
728 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
721 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
729 methods = exprmethods.copy()
722 methods = exprmethods.copy()
730 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
723 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
731
724
732 funcs = {
725 funcs = {
733 "date": date,
726 "date": date,
734 "diff": diff,
727 "diff": diff,
735 "fill": fill,
728 "fill": fill,
736 "get": get,
729 "get": get,
737 "if": if_,
730 "if": if_,
738 "ifcontains": ifcontains,
731 "ifcontains": ifcontains,
739 "ifeq": ifeq,
732 "ifeq": ifeq,
740 "indent": indent,
733 "indent": indent,
741 "join": join,
734 "join": join,
742 "label": label,
735 "label": label,
743 "localdate": localdate,
736 "localdate": localdate,
744 "pad": pad,
737 "pad": pad,
745 "revset": revset,
738 "revset": revset,
746 "rstdoc": rstdoc,
739 "rstdoc": rstdoc,
747 "shortest": shortest,
740 "shortest": shortest,
748 "startswith": startswith,
741 "startswith": startswith,
749 "strip": strip,
742 "strip": strip,
750 "sub": sub,
743 "sub": sub,
751 "word": word,
744 "word": word,
752 }
745 }
753
746
754 # template engine
747 # template engine
755
748
756 stringify = templatefilters.stringify
749 stringify = templatefilters.stringify
757
750
758 def _flatten(thing):
751 def _flatten(thing):
759 '''yield a single stream from a possibly nested set of iterators'''
752 '''yield a single stream from a possibly nested set of iterators'''
760 if isinstance(thing, str):
753 if isinstance(thing, str):
761 yield thing
754 yield thing
762 elif not util.safehasattr(thing, '__iter__'):
755 elif not util.safehasattr(thing, '__iter__'):
763 if thing is not None:
756 if thing is not None:
764 yield str(thing)
757 yield str(thing)
765 else:
758 else:
766 for i in thing:
759 for i in thing:
767 if isinstance(i, str):
760 if isinstance(i, str):
768 yield i
761 yield i
769 elif not util.safehasattr(i, '__iter__'):
762 elif not util.safehasattr(i, '__iter__'):
770 if i is not None:
763 if i is not None:
771 yield str(i)
764 yield str(i)
772 elif i is not None:
765 elif i is not None:
773 for j in _flatten(i):
766 for j in _flatten(i):
774 yield j
767 yield j
775
768
776 def unquotestring(s):
769 def unquotestring(s):
777 '''unwrap quotes'''
770 '''unwrap quotes'''
778 if len(s) < 2 or s[0] != s[-1]:
771 if len(s) < 2 or s[0] != s[-1]:
779 raise SyntaxError(_('unmatched quotes'))
772 raise SyntaxError(_('unmatched quotes'))
780 return s[1:-1]
773 return s[1:-1]
781
774
782 class engine(object):
775 class engine(object):
783 '''template expansion engine.
776 '''template expansion engine.
784
777
785 template expansion works like this. a map file contains key=value
778 template expansion works like this. a map file contains key=value
786 pairs. if value is quoted, it is treated as string. otherwise, it
779 pairs. if value is quoted, it is treated as string. otherwise, it
787 is treated as name of template file.
780 is treated as name of template file.
788
781
789 templater is asked to expand a key in map. it looks up key, and
782 templater is asked to expand a key in map. it looks up key, and
790 looks for strings like this: {foo}. it expands {foo} by looking up
783 looks for strings like this: {foo}. it expands {foo} by looking up
791 foo in map, and substituting it. expansion is recursive: it stops
784 foo in map, and substituting it. expansion is recursive: it stops
792 when there is no more {foo} to replace.
785 when there is no more {foo} to replace.
793
786
794 expansion also allows formatting and filtering.
787 expansion also allows formatting and filtering.
795
788
796 format uses key to expand each item in list. syntax is
789 format uses key to expand each item in list. syntax is
797 {key%format}.
790 {key%format}.
798
791
799 filter uses function to transform value. syntax is
792 filter uses function to transform value. syntax is
800 {key|filter1|filter2|...}.'''
793 {key|filter1|filter2|...}.'''
801
794
802 def __init__(self, loader, filters={}, defaults={}):
795 def __init__(self, loader, filters={}, defaults={}):
803 self._loader = loader
796 self._loader = loader
804 self._filters = filters
797 self._filters = filters
805 self._defaults = defaults
798 self._defaults = defaults
806 self._cache = {}
799 self._cache = {}
807
800
808 def _load(self, t):
801 def _load(self, t):
809 '''load, parse, and cache a template'''
802 '''load, parse, and cache a template'''
810 if t not in self._cache:
803 if t not in self._cache:
811 self._cache[t] = compiletemplate(self._loader(t), self)
804 self._cache[t] = compiletemplate(self._loader(t), self)
812 return self._cache[t]
805 return self._cache[t]
813
806
814 def process(self, t, mapping):
807 def process(self, t, mapping):
815 '''Perform expansion. t is name of map element to expand.
808 '''Perform expansion. t is name of map element to expand.
816 mapping contains added elements for use during expansion. Is a
809 mapping contains added elements for use during expansion. Is a
817 generator.'''
810 generator.'''
818 return _flatten(runtemplate(self, mapping, self._load(t)))
811 return _flatten(runtemplate(self, mapping, self._load(t)))
819
812
820 engines = {'default': engine}
813 engines = {'default': engine}
821
814
822 def stylelist():
815 def stylelist():
823 paths = templatepaths()
816 paths = templatepaths()
824 if not paths:
817 if not paths:
825 return _('no templates found, try `hg debuginstall` for more info')
818 return _('no templates found, try `hg debuginstall` for more info')
826 dirlist = os.listdir(paths[0])
819 dirlist = os.listdir(paths[0])
827 stylelist = []
820 stylelist = []
828 for file in dirlist:
821 for file in dirlist:
829 split = file.split(".")
822 split = file.split(".")
830 if split[0] == "map-cmdline":
823 if split[0] == "map-cmdline":
831 stylelist.append(split[1])
824 stylelist.append(split[1])
832 return ", ".join(sorted(stylelist))
825 return ", ".join(sorted(stylelist))
833
826
834 class TemplateNotFound(util.Abort):
827 class TemplateNotFound(util.Abort):
835 pass
828 pass
836
829
837 class templater(object):
830 class templater(object):
838
831
839 def __init__(self, mapfile, filters={}, defaults={}, cache={},
832 def __init__(self, mapfile, filters={}, defaults={}, cache={},
840 minchunk=1024, maxchunk=65536):
833 minchunk=1024, maxchunk=65536):
841 '''set up template engine.
834 '''set up template engine.
842 mapfile is name of file to read map definitions from.
835 mapfile is name of file to read map definitions from.
843 filters is dict of functions. each transforms a value into another.
836 filters is dict of functions. each transforms a value into another.
844 defaults is dict of default map definitions.'''
837 defaults is dict of default map definitions.'''
845 self.mapfile = mapfile or 'template'
838 self.mapfile = mapfile or 'template'
846 self.cache = cache.copy()
839 self.cache = cache.copy()
847 self.map = {}
840 self.map = {}
848 if mapfile:
841 if mapfile:
849 self.base = os.path.dirname(mapfile)
842 self.base = os.path.dirname(mapfile)
850 else:
843 else:
851 self.base = ''
844 self.base = ''
852 self.filters = templatefilters.filters.copy()
845 self.filters = templatefilters.filters.copy()
853 self.filters.update(filters)
846 self.filters.update(filters)
854 self.defaults = defaults
847 self.defaults = defaults
855 self.minchunk, self.maxchunk = minchunk, maxchunk
848 self.minchunk, self.maxchunk = minchunk, maxchunk
856 self.ecache = {}
849 self.ecache = {}
857
850
858 if not mapfile:
851 if not mapfile:
859 return
852 return
860 if not os.path.exists(mapfile):
853 if not os.path.exists(mapfile):
861 raise util.Abort(_("style '%s' not found") % mapfile,
854 raise util.Abort(_("style '%s' not found") % mapfile,
862 hint=_("available styles: %s") % stylelist())
855 hint=_("available styles: %s") % stylelist())
863
856
864 conf = config.config(includepaths=templatepaths())
857 conf = config.config(includepaths=templatepaths())
865 conf.read(mapfile)
858 conf.read(mapfile)
866
859
867 for key, val in conf[''].items():
860 for key, val in conf[''].items():
868 if not val:
861 if not val:
869 raise SyntaxError(_('%s: missing value') % conf.source('', key))
862 raise SyntaxError(_('%s: missing value') % conf.source('', key))
870 if val[0] in "'\"":
863 if val[0] in "'\"":
871 try:
864 try:
872 self.cache[key] = unquotestring(val)
865 self.cache[key] = unquotestring(val)
873 except SyntaxError as inst:
866 except SyntaxError as inst:
874 raise SyntaxError('%s: %s' %
867 raise SyntaxError('%s: %s' %
875 (conf.source('', key), inst.args[0]))
868 (conf.source('', key), inst.args[0]))
876 else:
869 else:
877 val = 'default', val
870 val = 'default', val
878 if ':' in val[1]:
871 if ':' in val[1]:
879 val = val[1].split(':', 1)
872 val = val[1].split(':', 1)
880 self.map[key] = val[0], os.path.join(self.base, val[1])
873 self.map[key] = val[0], os.path.join(self.base, val[1])
881
874
882 def __contains__(self, key):
875 def __contains__(self, key):
883 return key in self.cache or key in self.map
876 return key in self.cache or key in self.map
884
877
885 def load(self, t):
878 def load(self, t):
886 '''Get the template for the given template name. Use a local cache.'''
879 '''Get the template for the given template name. Use a local cache.'''
887 if t not in self.cache:
880 if t not in self.cache:
888 try:
881 try:
889 self.cache[t] = util.readfile(self.map[t][1])
882 self.cache[t] = util.readfile(self.map[t][1])
890 except KeyError as inst:
883 except KeyError as inst:
891 raise TemplateNotFound(_('"%s" not in template map') %
884 raise TemplateNotFound(_('"%s" not in template map') %
892 inst.args[0])
885 inst.args[0])
893 except IOError as inst:
886 except IOError as inst:
894 raise IOError(inst.args[0], _('template file %s: %s') %
887 raise IOError(inst.args[0], _('template file %s: %s') %
895 (self.map[t][1], inst.args[1]))
888 (self.map[t][1], inst.args[1]))
896 return self.cache[t]
889 return self.cache[t]
897
890
898 def __call__(self, t, **mapping):
891 def __call__(self, t, **mapping):
899 ttype = t in self.map and self.map[t][0] or 'default'
892 ttype = t in self.map and self.map[t][0] or 'default'
900 if ttype not in self.ecache:
893 if ttype not in self.ecache:
901 self.ecache[ttype] = engines[ttype](self.load,
894 self.ecache[ttype] = engines[ttype](self.load,
902 self.filters, self.defaults)
895 self.filters, self.defaults)
903 proc = self.ecache[ttype]
896 proc = self.ecache[ttype]
904
897
905 stream = proc.process(t, mapping)
898 stream = proc.process(t, mapping)
906 if self.minchunk:
899 if self.minchunk:
907 stream = util.increasingchunks(stream, min=self.minchunk,
900 stream = util.increasingchunks(stream, min=self.minchunk,
908 max=self.maxchunk)
901 max=self.maxchunk)
909 return stream
902 return stream
910
903
911 def templatepaths():
904 def templatepaths():
912 '''return locations used for template files.'''
905 '''return locations used for template files.'''
913 pathsrel = ['templates']
906 pathsrel = ['templates']
914 paths = [os.path.normpath(os.path.join(util.datapath, f))
907 paths = [os.path.normpath(os.path.join(util.datapath, f))
915 for f in pathsrel]
908 for f in pathsrel]
916 return [p for p in paths if os.path.isdir(p)]
909 return [p for p in paths if os.path.isdir(p)]
917
910
918 def templatepath(name):
911 def templatepath(name):
919 '''return location of template file. returns None if not found.'''
912 '''return location of template file. returns None if not found.'''
920 for p in templatepaths():
913 for p in templatepaths():
921 f = os.path.join(p, name)
914 f = os.path.join(p, name)
922 if os.path.exists(f):
915 if os.path.exists(f):
923 return f
916 return f
924 return None
917 return None
925
918
926 def stylemap(styles, paths=None):
919 def stylemap(styles, paths=None):
927 """Return path to mapfile for a given style.
920 """Return path to mapfile for a given style.
928
921
929 Searches mapfile in the following locations:
922 Searches mapfile in the following locations:
930 1. templatepath/style/map
923 1. templatepath/style/map
931 2. templatepath/map-style
924 2. templatepath/map-style
932 3. templatepath/map
925 3. templatepath/map
933 """
926 """
934
927
935 if paths is None:
928 if paths is None:
936 paths = templatepaths()
929 paths = templatepaths()
937 elif isinstance(paths, str):
930 elif isinstance(paths, str):
938 paths = [paths]
931 paths = [paths]
939
932
940 if isinstance(styles, str):
933 if isinstance(styles, str):
941 styles = [styles]
934 styles = [styles]
942
935
943 for style in styles:
936 for style in styles:
944 # only plain name is allowed to honor template paths
937 # only plain name is allowed to honor template paths
945 if (not style
938 if (not style
946 or style in (os.curdir, os.pardir)
939 or style in (os.curdir, os.pardir)
947 or os.sep in style
940 or os.sep in style
948 or os.altsep and os.altsep in style):
941 or os.altsep and os.altsep in style):
949 continue
942 continue
950 locations = [os.path.join(style, 'map'), 'map-' + style]
943 locations = [os.path.join(style, 'map'), 'map-' + style]
951 locations.append('map')
944 locations.append('map')
952
945
953 for path in paths:
946 for path in paths:
954 for location in locations:
947 for location in locations:
955 mapfile = os.path.join(path, location)
948 mapfile = os.path.join(path, location)
956 if os.path.isfile(mapfile):
949 if os.path.isfile(mapfile):
957 return style, mapfile
950 return style, mapfile
958
951
959 raise RuntimeError("No hgweb templates found in %r" % paths)
952 raise RuntimeError("No hgweb templates found in %r" % paths)
960
953
961 # tell hggettext to extract docstrings from these functions:
954 # tell hggettext to extract docstrings from these functions:
962 i18nfunctions = funcs.values()
955 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now