##// END OF EJS Templates
templater: parse template string to tree by templater class...
Yuya Nishihara -
r38373:e637dc0b default
parent child Browse files
Show More
@@ -1,134 +1,134
1 # Copyright 2009, Alexander Solovyov <piranha@piranha.org.ua>
1 # Copyright 2009, Alexander Solovyov <piranha@piranha.org.ua>
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 """extend schemes with shortcuts to repository swarms
6 """extend schemes with shortcuts to repository swarms
7
7
8 This extension allows you to specify shortcuts for parent URLs with a
8 This extension allows you to specify shortcuts for parent URLs with a
9 lot of repositories to act like a scheme, for example::
9 lot of repositories to act like a scheme, for example::
10
10
11 [schemes]
11 [schemes]
12 py = http://code.python.org/hg/
12 py = http://code.python.org/hg/
13
13
14 After that you can use it like::
14 After that you can use it like::
15
15
16 hg clone py://trunk/
16 hg clone py://trunk/
17
17
18 Additionally there is support for some more complex schemas, for
18 Additionally there is support for some more complex schemas, for
19 example used by Google Code::
19 example used by Google Code::
20
20
21 [schemes]
21 [schemes]
22 gcode = http://{1}.googlecode.com/hg/
22 gcode = http://{1}.googlecode.com/hg/
23
23
24 The syntax is taken from Mercurial templates, and you have unlimited
24 The syntax is taken from Mercurial templates, and you have unlimited
25 number of variables, starting with ``{1}`` and continuing with
25 number of variables, starting with ``{1}`` and continuing with
26 ``{2}``, ``{3}`` and so on. This variables will receive parts of URL
26 ``{2}``, ``{3}`` and so on. This variables will receive parts of URL
27 supplied, split by ``/``. Anything not specified as ``{part}`` will be
27 supplied, split by ``/``. Anything not specified as ``{part}`` will be
28 just appended to an URL.
28 just appended to an URL.
29
29
30 For convenience, the extension adds these schemes by default::
30 For convenience, the extension adds these schemes by default::
31
31
32 [schemes]
32 [schemes]
33 py = http://hg.python.org/
33 py = http://hg.python.org/
34 bb = https://bitbucket.org/
34 bb = https://bitbucket.org/
35 bb+ssh = ssh://hg@bitbucket.org/
35 bb+ssh = ssh://hg@bitbucket.org/
36 gcode = https://{1}.googlecode.com/hg/
36 gcode = https://{1}.googlecode.com/hg/
37 kiln = https://{1}.kilnhg.com/Repo/
37 kiln = https://{1}.kilnhg.com/Repo/
38
38
39 You can override a predefined scheme by defining a new scheme with the
39 You can override a predefined scheme by defining a new scheme with the
40 same name.
40 same name.
41 """
41 """
42 from __future__ import absolute_import
42 from __future__ import absolute_import
43
43
44 import os
44 import os
45 import re
45 import re
46
46
47 from mercurial.i18n import _
47 from mercurial.i18n import _
48 from mercurial import (
48 from mercurial import (
49 error,
49 error,
50 extensions,
50 extensions,
51 hg,
51 hg,
52 pycompat,
52 pycompat,
53 registrar,
53 registrar,
54 templater,
54 templater,
55 util,
55 util,
56 )
56 )
57
57
58 cmdtable = {}
58 cmdtable = {}
59 command = registrar.command(cmdtable)
59 command = registrar.command(cmdtable)
60 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
60 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
61 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
61 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
62 # be specifying the version(s) of Mercurial they are tested with, or
62 # be specifying the version(s) of Mercurial they are tested with, or
63 # leave the attribute unspecified.
63 # leave the attribute unspecified.
64 testedwith = 'ships-with-hg-core'
64 testedwith = 'ships-with-hg-core'
65
65
66 _partre = re.compile(br'\{(\d+)\}')
66 _partre = re.compile(br'\{(\d+)\}')
67
67
68 class ShortRepository(object):
68 class ShortRepository(object):
69 def __init__(self, url, scheme, templater):
69 def __init__(self, url, scheme, templater):
70 self.scheme = scheme
70 self.scheme = scheme
71 self.templater = templater
71 self.templater = templater
72 self.url = url
72 self.url = url
73 try:
73 try:
74 self.parts = max(map(int, _partre.findall(self.url)))
74 self.parts = max(map(int, _partre.findall(self.url)))
75 except ValueError:
75 except ValueError:
76 self.parts = 0
76 self.parts = 0
77
77
78 def __repr__(self):
78 def __repr__(self):
79 return '<ShortRepository: %s>' % self.scheme
79 return '<ShortRepository: %s>' % self.scheme
80
80
81 def instance(self, ui, url, create, intents=None):
81 def instance(self, ui, url, create, intents=None):
82 url = self.resolve(url)
82 url = self.resolve(url)
83 return hg._peerlookup(url).instance(ui, url, create, intents=intents)
83 return hg._peerlookup(url).instance(ui, url, create, intents=intents)
84
84
85 def resolve(self, url):
85 def resolve(self, url):
86 # Should this use the util.url class, or is manual parsing better?
86 # Should this use the util.url class, or is manual parsing better?
87 try:
87 try:
88 url = url.split('://', 1)[1]
88 url = url.split('://', 1)[1]
89 except IndexError:
89 except IndexError:
90 raise error.Abort(_("no '://' in scheme url '%s'") % url)
90 raise error.Abort(_("no '://' in scheme url '%s'") % url)
91 parts = url.split('/', self.parts)
91 parts = url.split('/', self.parts)
92 if len(parts) > self.parts:
92 if len(parts) > self.parts:
93 tail = parts[-1]
93 tail = parts[-1]
94 parts = parts[:-1]
94 parts = parts[:-1]
95 else:
95 else:
96 tail = ''
96 tail = ''
97 context = dict(('%d' % (i + 1), v) for i, v in enumerate(parts))
97 context = dict(('%d' % (i + 1), v) for i, v in enumerate(parts))
98 return ''.join(self.templater.process(self.url, context)) + tail
98 return ''.join(self.templater.process(self.url, context)) + tail
99
99
100 def hasdriveletter(orig, path):
100 def hasdriveletter(orig, path):
101 if path:
101 if path:
102 for scheme in schemes:
102 for scheme in schemes:
103 if path.startswith(scheme + ':'):
103 if path.startswith(scheme + ':'):
104 return False
104 return False
105 return orig(path)
105 return orig(path)
106
106
107 schemes = {
107 schemes = {
108 'py': 'http://hg.python.org/',
108 'py': 'http://hg.python.org/',
109 'bb': 'https://bitbucket.org/',
109 'bb': 'https://bitbucket.org/',
110 'bb+ssh': 'ssh://hg@bitbucket.org/',
110 'bb+ssh': 'ssh://hg@bitbucket.org/',
111 'gcode': 'https://{1}.googlecode.com/hg/',
111 'gcode': 'https://{1}.googlecode.com/hg/',
112 'kiln': 'https://{1}.kilnhg.com/Repo/'
112 'kiln': 'https://{1}.kilnhg.com/Repo/'
113 }
113 }
114
114
115 def extsetup(ui):
115 def extsetup(ui):
116 schemes.update(dict(ui.configitems('schemes')))
116 schemes.update(dict(ui.configitems('schemes')))
117 t = templater.engine(lambda x: x)
117 t = templater.engine(templater.parse)
118 for scheme, url in schemes.items():
118 for scheme, url in schemes.items():
119 if (pycompat.iswindows and len(scheme) == 1 and scheme.isalpha()
119 if (pycompat.iswindows and len(scheme) == 1 and scheme.isalpha()
120 and os.path.exists('%s:\\' % scheme)):
120 and os.path.exists('%s:\\' % scheme)):
121 raise error.Abort(_('custom scheme %s:// conflicts with drive '
121 raise error.Abort(_('custom scheme %s:// conflicts with drive '
122 'letter %s:\\\n') % (scheme, scheme.upper()))
122 'letter %s:\\\n') % (scheme, scheme.upper()))
123 hg.schemes[scheme] = ShortRepository(url, scheme, t)
123 hg.schemes[scheme] = ShortRepository(url, scheme, t)
124
124
125 extensions.wrapfunction(util, 'hasdriveletter', hasdriveletter)
125 extensions.wrapfunction(util, 'hasdriveletter', hasdriveletter)
126
126
127 @command('debugexpandscheme', norepo=True)
127 @command('debugexpandscheme', norepo=True)
128 def expandscheme(ui, url, **opts):
128 def expandscheme(ui, url, **opts):
129 """given a repo path, provide the scheme-expanded path
129 """given a repo path, provide the scheme-expanded path
130 """
130 """
131 repo = hg._peerlookup(url)
131 repo = hg._peerlookup(url)
132 if isinstance(repo, ShortRepository):
132 if isinstance(repo, ShortRepository):
133 url = repo.resolve(url)
133 url = repo.resolve(url)
134 ui.write(url + '\n')
134 ui.write(url + '\n')
@@ -1,910 +1,911
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 """Slightly complicated template engine for commands and hgweb
8 """Slightly complicated template engine for commands and hgweb
9
9
10 This module provides low-level interface to the template engine. See the
10 This module provides low-level interface to the template engine. See the
11 formatter and cmdutil modules if you are looking for high-level functions
11 formatter and cmdutil modules if you are looking for high-level functions
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13
13
14 Internal Data Types
14 Internal Data Types
15 -------------------
15 -------------------
16
16
17 Template keywords and functions take a dictionary of current symbols and
17 Template keywords and functions take a dictionary of current symbols and
18 resources (a "mapping") and return result. Inputs and outputs must be one
18 resources (a "mapping") and return result. Inputs and outputs must be one
19 of the following data types:
19 of the following data types:
20
20
21 bytes
21 bytes
22 a byte string, which is generally a human-readable text in local encoding.
22 a byte string, which is generally a human-readable text in local encoding.
23
23
24 generator
24 generator
25 a lazily-evaluated byte string, which is a possibly nested generator of
25 a lazily-evaluated byte string, which is a possibly nested generator of
26 values of any printable types, and will be folded by ``stringify()``
26 values of any printable types, and will be folded by ``stringify()``
27 or ``flatten()``.
27 or ``flatten()``.
28
28
29 None
29 None
30 sometimes represents an empty value, which can be stringified to ''.
30 sometimes represents an empty value, which can be stringified to ''.
31
31
32 True, False, int, float
32 True, False, int, float
33 can be stringified as such.
33 can be stringified as such.
34
34
35 wrappedbytes, wrappedvalue
35 wrappedbytes, wrappedvalue
36 a wrapper for the above printable types.
36 a wrapper for the above printable types.
37
37
38 date
38 date
39 represents a (unixtime, offset) tuple.
39 represents a (unixtime, offset) tuple.
40
40
41 hybrid
41 hybrid
42 represents a list/dict of printable values, which can also be converted
42 represents a list/dict of printable values, which can also be converted
43 to mappings by % operator.
43 to mappings by % operator.
44
44
45 hybriditem
45 hybriditem
46 represents a scalar printable value, also supports % operator.
46 represents a scalar printable value, also supports % operator.
47
47
48 mappinggenerator, mappinglist
48 mappinggenerator, mappinglist
49 represents mappings (i.e. a list of dicts), which may have default
49 represents mappings (i.e. a list of dicts), which may have default
50 output format.
50 output format.
51
51
52 mappedgenerator
52 mappedgenerator
53 a lazily-evaluated list of byte strings, which is e.g. a result of %
53 a lazily-evaluated list of byte strings, which is e.g. a result of %
54 operation.
54 operation.
55 """
55 """
56
56
57 from __future__ import absolute_import, print_function
57 from __future__ import absolute_import, print_function
58
58
59 import abc
59 import abc
60 import os
60 import os
61
61
62 from .i18n import _
62 from .i18n import _
63 from . import (
63 from . import (
64 config,
64 config,
65 encoding,
65 encoding,
66 error,
66 error,
67 parser,
67 parser,
68 pycompat,
68 pycompat,
69 templatefilters,
69 templatefilters,
70 templatefuncs,
70 templatefuncs,
71 templateutil,
71 templateutil,
72 util,
72 util,
73 )
73 )
74 from .utils import (
74 from .utils import (
75 stringutil,
75 stringutil,
76 )
76 )
77
77
78 # template parsing
78 # template parsing
79
79
80 elements = {
80 elements = {
81 # token-type: binding-strength, primary, prefix, infix, suffix
81 # token-type: binding-strength, primary, prefix, infix, suffix
82 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
82 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
83 ".": (18, None, None, (".", 18), None),
83 ".": (18, None, None, (".", 18), None),
84 "%": (15, None, None, ("%", 15), None),
84 "%": (15, None, None, ("%", 15), None),
85 "|": (15, None, None, ("|", 15), None),
85 "|": (15, None, None, ("|", 15), None),
86 "*": (5, None, None, ("*", 5), None),
86 "*": (5, None, None, ("*", 5), None),
87 "/": (5, None, None, ("/", 5), None),
87 "/": (5, None, None, ("/", 5), None),
88 "+": (4, None, None, ("+", 4), None),
88 "+": (4, None, None, ("+", 4), None),
89 "-": (4, None, ("negate", 19), ("-", 4), None),
89 "-": (4, None, ("negate", 19), ("-", 4), None),
90 "=": (3, None, None, ("keyvalue", 3), None),
90 "=": (3, None, None, ("keyvalue", 3), None),
91 ",": (2, None, None, ("list", 2), None),
91 ",": (2, None, None, ("list", 2), None),
92 ")": (0, None, None, None, None),
92 ")": (0, None, None, None, None),
93 "integer": (0, "integer", None, None, None),
93 "integer": (0, "integer", None, None, None),
94 "symbol": (0, "symbol", None, None, None),
94 "symbol": (0, "symbol", None, None, None),
95 "string": (0, "string", None, None, None),
95 "string": (0, "string", None, None, None),
96 "template": (0, "template", None, None, None),
96 "template": (0, "template", None, None, None),
97 "end": (0, None, None, None, None),
97 "end": (0, None, None, None, None),
98 }
98 }
99
99
100 def tokenize(program, start, end, term=None):
100 def tokenize(program, start, end, term=None):
101 """Parse a template expression into a stream of tokens, which must end
101 """Parse a template expression into a stream of tokens, which must end
102 with term if specified"""
102 with term if specified"""
103 pos = start
103 pos = start
104 program = pycompat.bytestr(program)
104 program = pycompat.bytestr(program)
105 while pos < end:
105 while pos < end:
106 c = program[pos]
106 c = program[pos]
107 if c.isspace(): # skip inter-token whitespace
107 if c.isspace(): # skip inter-token whitespace
108 pass
108 pass
109 elif c in "(=,).%|+-*/": # handle simple operators
109 elif c in "(=,).%|+-*/": # handle simple operators
110 yield (c, None, pos)
110 yield (c, None, pos)
111 elif c in '"\'': # handle quoted templates
111 elif c in '"\'': # handle quoted templates
112 s = pos + 1
112 s = pos + 1
113 data, pos = _parsetemplate(program, s, end, c)
113 data, pos = _parsetemplate(program, s, end, c)
114 yield ('template', data, s)
114 yield ('template', data, s)
115 pos -= 1
115 pos -= 1
116 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
116 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
117 # handle quoted strings
117 # handle quoted strings
118 c = program[pos + 1]
118 c = program[pos + 1]
119 s = pos = pos + 2
119 s = pos = pos + 2
120 while pos < end: # find closing quote
120 while pos < end: # find closing quote
121 d = program[pos]
121 d = program[pos]
122 if d == '\\': # skip over escaped characters
122 if d == '\\': # skip over escaped characters
123 pos += 2
123 pos += 2
124 continue
124 continue
125 if d == c:
125 if d == c:
126 yield ('string', program[s:pos], s)
126 yield ('string', program[s:pos], s)
127 break
127 break
128 pos += 1
128 pos += 1
129 else:
129 else:
130 raise error.ParseError(_("unterminated string"), s)
130 raise error.ParseError(_("unterminated string"), s)
131 elif c.isdigit():
131 elif c.isdigit():
132 s = pos
132 s = pos
133 while pos < end:
133 while pos < end:
134 d = program[pos]
134 d = program[pos]
135 if not d.isdigit():
135 if not d.isdigit():
136 break
136 break
137 pos += 1
137 pos += 1
138 yield ('integer', program[s:pos], s)
138 yield ('integer', program[s:pos], s)
139 pos -= 1
139 pos -= 1
140 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
140 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
141 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
141 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
142 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
142 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
143 # where some of nested templates were preprocessed as strings and
143 # where some of nested templates were preprocessed as strings and
144 # then compiled. therefore, \"...\" was allowed. (issue4733)
144 # then compiled. therefore, \"...\" was allowed. (issue4733)
145 #
145 #
146 # processing flow of _evalifliteral() at 5ab28a2e9962:
146 # processing flow of _evalifliteral() at 5ab28a2e9962:
147 # outer template string -> stringify() -> compiletemplate()
147 # outer template string -> stringify() -> compiletemplate()
148 # ------------------------ ------------ ------------------
148 # ------------------------ ------------ ------------------
149 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
149 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
150 # ~~~~~~~~
150 # ~~~~~~~~
151 # escaped quoted string
151 # escaped quoted string
152 if c == 'r':
152 if c == 'r':
153 pos += 1
153 pos += 1
154 token = 'string'
154 token = 'string'
155 else:
155 else:
156 token = 'template'
156 token = 'template'
157 quote = program[pos:pos + 2]
157 quote = program[pos:pos + 2]
158 s = pos = pos + 2
158 s = pos = pos + 2
159 while pos < end: # find closing escaped quote
159 while pos < end: # find closing escaped quote
160 if program.startswith('\\\\\\', pos, end):
160 if program.startswith('\\\\\\', pos, end):
161 pos += 4 # skip over double escaped characters
161 pos += 4 # skip over double escaped characters
162 continue
162 continue
163 if program.startswith(quote, pos, end):
163 if program.startswith(quote, pos, end):
164 # interpret as if it were a part of an outer string
164 # interpret as if it were a part of an outer string
165 data = parser.unescapestr(program[s:pos])
165 data = parser.unescapestr(program[s:pos])
166 if token == 'template':
166 if token == 'template':
167 data = _parsetemplate(data, 0, len(data))[0]
167 data = _parsetemplate(data, 0, len(data))[0]
168 yield (token, data, s)
168 yield (token, data, s)
169 pos += 1
169 pos += 1
170 break
170 break
171 pos += 1
171 pos += 1
172 else:
172 else:
173 raise error.ParseError(_("unterminated string"), s)
173 raise error.ParseError(_("unterminated string"), s)
174 elif c.isalnum() or c in '_':
174 elif c.isalnum() or c in '_':
175 s = pos
175 s = pos
176 pos += 1
176 pos += 1
177 while pos < end: # find end of symbol
177 while pos < end: # find end of symbol
178 d = program[pos]
178 d = program[pos]
179 if not (d.isalnum() or d == "_"):
179 if not (d.isalnum() or d == "_"):
180 break
180 break
181 pos += 1
181 pos += 1
182 sym = program[s:pos]
182 sym = program[s:pos]
183 yield ('symbol', sym, s)
183 yield ('symbol', sym, s)
184 pos -= 1
184 pos -= 1
185 elif c == term:
185 elif c == term:
186 yield ('end', None, pos)
186 yield ('end', None, pos)
187 return
187 return
188 else:
188 else:
189 raise error.ParseError(_("syntax error"), pos)
189 raise error.ParseError(_("syntax error"), pos)
190 pos += 1
190 pos += 1
191 if term:
191 if term:
192 raise error.ParseError(_("unterminated template expansion"), start)
192 raise error.ParseError(_("unterminated template expansion"), start)
193 yield ('end', None, pos)
193 yield ('end', None, pos)
194
194
195 def _parsetemplate(tmpl, start, stop, quote=''):
195 def _parsetemplate(tmpl, start, stop, quote=''):
196 r"""
196 r"""
197 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
197 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
198 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
198 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
199 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
199 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
200 ([('string', 'foo'), ('symbol', 'bar')], 9)
200 ([('string', 'foo'), ('symbol', 'bar')], 9)
201 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
201 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
202 ([('string', 'foo')], 4)
202 ([('string', 'foo')], 4)
203 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
203 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
204 ([('string', 'foo"'), ('string', 'bar')], 9)
204 ([('string', 'foo"'), ('string', 'bar')], 9)
205 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
205 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
206 ([('string', 'foo\\')], 6)
206 ([('string', 'foo\\')], 6)
207 """
207 """
208 parsed = []
208 parsed = []
209 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
209 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
210 if typ == 'string':
210 if typ == 'string':
211 parsed.append((typ, val))
211 parsed.append((typ, val))
212 elif typ == 'template':
212 elif typ == 'template':
213 parsed.append(val)
213 parsed.append(val)
214 elif typ == 'end':
214 elif typ == 'end':
215 return parsed, pos
215 return parsed, pos
216 else:
216 else:
217 raise error.ProgrammingError('unexpected type: %s' % typ)
217 raise error.ProgrammingError('unexpected type: %s' % typ)
218 raise error.ProgrammingError('unterminated scanning of template')
218 raise error.ProgrammingError('unterminated scanning of template')
219
219
220 def scantemplate(tmpl, raw=False):
220 def scantemplate(tmpl, raw=False):
221 r"""Scan (type, start, end) positions of outermost elements in template
221 r"""Scan (type, start, end) positions of outermost elements in template
222
222
223 If raw=True, a backslash is not taken as an escape character just like
223 If raw=True, a backslash is not taken as an escape character just like
224 r'' string in Python. Note that this is different from r'' literal in
224 r'' string in Python. Note that this is different from r'' literal in
225 template in that no template fragment can appear in r'', e.g. r'{foo}'
225 template in that no template fragment can appear in r'', e.g. r'{foo}'
226 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
226 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
227 'foo'.
227 'foo'.
228
228
229 >>> list(scantemplate(b'foo{bar}"baz'))
229 >>> list(scantemplate(b'foo{bar}"baz'))
230 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
230 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
231 >>> list(scantemplate(b'outer{"inner"}outer'))
231 >>> list(scantemplate(b'outer{"inner"}outer'))
232 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
232 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
233 >>> list(scantemplate(b'foo\\{escaped}'))
233 >>> list(scantemplate(b'foo\\{escaped}'))
234 [('string', 0, 5), ('string', 5, 13)]
234 [('string', 0, 5), ('string', 5, 13)]
235 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
235 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
236 [('string', 0, 4), ('template', 4, 13)]
236 [('string', 0, 4), ('template', 4, 13)]
237 """
237 """
238 last = None
238 last = None
239 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
239 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
240 if last:
240 if last:
241 yield last + (pos,)
241 yield last + (pos,)
242 if typ == 'end':
242 if typ == 'end':
243 return
243 return
244 else:
244 else:
245 last = (typ, pos)
245 last = (typ, pos)
246 raise error.ProgrammingError('unterminated scanning of template')
246 raise error.ProgrammingError('unterminated scanning of template')
247
247
248 def _scantemplate(tmpl, start, stop, quote='', raw=False):
248 def _scantemplate(tmpl, start, stop, quote='', raw=False):
249 """Parse template string into chunks of strings and template expressions"""
249 """Parse template string into chunks of strings and template expressions"""
250 sepchars = '{' + quote
250 sepchars = '{' + quote
251 unescape = [parser.unescapestr, pycompat.identity][raw]
251 unescape = [parser.unescapestr, pycompat.identity][raw]
252 pos = start
252 pos = start
253 p = parser.parser(elements)
253 p = parser.parser(elements)
254 try:
254 try:
255 while pos < stop:
255 while pos < stop:
256 n = min((tmpl.find(c, pos, stop)
256 n = min((tmpl.find(c, pos, stop)
257 for c in pycompat.bytestr(sepchars)),
257 for c in pycompat.bytestr(sepchars)),
258 key=lambda n: (n < 0, n))
258 key=lambda n: (n < 0, n))
259 if n < 0:
259 if n < 0:
260 yield ('string', unescape(tmpl[pos:stop]), pos)
260 yield ('string', unescape(tmpl[pos:stop]), pos)
261 pos = stop
261 pos = stop
262 break
262 break
263 c = tmpl[n:n + 1]
263 c = tmpl[n:n + 1]
264 bs = 0 # count leading backslashes
264 bs = 0 # count leading backslashes
265 if not raw:
265 if not raw:
266 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
266 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
267 if bs % 2 == 1:
267 if bs % 2 == 1:
268 # escaped (e.g. '\{', '\\\{', but not '\\{')
268 # escaped (e.g. '\{', '\\\{', but not '\\{')
269 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
269 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
270 pos = n + 1
270 pos = n + 1
271 continue
271 continue
272 if n > pos:
272 if n > pos:
273 yield ('string', unescape(tmpl[pos:n]), pos)
273 yield ('string', unescape(tmpl[pos:n]), pos)
274 if c == quote:
274 if c == quote:
275 yield ('end', None, n + 1)
275 yield ('end', None, n + 1)
276 return
276 return
277
277
278 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
278 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
279 if not tmpl.startswith('}', pos):
279 if not tmpl.startswith('}', pos):
280 raise error.ParseError(_("invalid token"), pos)
280 raise error.ParseError(_("invalid token"), pos)
281 yield ('template', parseres, n)
281 yield ('template', parseres, n)
282 pos += 1
282 pos += 1
283
283
284 if quote:
284 if quote:
285 raise error.ParseError(_("unterminated string"), start)
285 raise error.ParseError(_("unterminated string"), start)
286 except error.ParseError as inst:
286 except error.ParseError as inst:
287 if len(inst.args) > 1: # has location
287 if len(inst.args) > 1: # has location
288 loc = inst.args[1]
288 loc = inst.args[1]
289 # Offset the caret location by the number of newlines before the
289 # Offset the caret location by the number of newlines before the
290 # location of the error, since we will replace one-char newlines
290 # location of the error, since we will replace one-char newlines
291 # with the two-char literal r'\n'.
291 # with the two-char literal r'\n'.
292 offset = tmpl[:loc].count('\n')
292 offset = tmpl[:loc].count('\n')
293 tmpl = tmpl.replace('\n', br'\n')
293 tmpl = tmpl.replace('\n', br'\n')
294 # We want the caret to point to the place in the template that
294 # We want the caret to point to the place in the template that
295 # failed to parse, but in a hint we get a open paren at the
295 # failed to parse, but in a hint we get a open paren at the
296 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
296 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
297 # to line up the caret with the location of the error.
297 # to line up the caret with the location of the error.
298 inst.hint = (tmpl + '\n'
298 inst.hint = (tmpl + '\n'
299 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
299 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
300 raise
300 raise
301 yield ('end', None, pos)
301 yield ('end', None, pos)
302
302
303 def _unnesttemplatelist(tree):
303 def _unnesttemplatelist(tree):
304 """Expand list of templates to node tuple
304 """Expand list of templates to node tuple
305
305
306 >>> def f(tree):
306 >>> def f(tree):
307 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
307 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
308 >>> f((b'template', []))
308 >>> f((b'template', []))
309 (string '')
309 (string '')
310 >>> f((b'template', [(b'string', b'foo')]))
310 >>> f((b'template', [(b'string', b'foo')]))
311 (string 'foo')
311 (string 'foo')
312 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
312 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
313 (template
313 (template
314 (string 'foo')
314 (string 'foo')
315 (symbol 'rev'))
315 (symbol 'rev'))
316 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
316 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
317 (template
317 (template
318 (symbol 'rev'))
318 (symbol 'rev'))
319 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
319 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
320 (string 'foo')
320 (string 'foo')
321 """
321 """
322 if not isinstance(tree, tuple):
322 if not isinstance(tree, tuple):
323 return tree
323 return tree
324 op = tree[0]
324 op = tree[0]
325 if op != 'template':
325 if op != 'template':
326 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
326 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
327
327
328 assert len(tree) == 2
328 assert len(tree) == 2
329 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
329 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
330 if not xs:
330 if not xs:
331 return ('string', '') # empty template ""
331 return ('string', '') # empty template ""
332 elif len(xs) == 1 and xs[0][0] == 'string':
332 elif len(xs) == 1 and xs[0][0] == 'string':
333 return xs[0] # fast path for string with no template fragment "x"
333 return xs[0] # fast path for string with no template fragment "x"
334 else:
334 else:
335 return (op,) + xs
335 return (op,) + xs
336
336
337 def parse(tmpl):
337 def parse(tmpl):
338 """Parse template string into tree"""
338 """Parse template string into tree"""
339 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
339 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
340 assert pos == len(tmpl), 'unquoted template should be consumed'
340 assert pos == len(tmpl), 'unquoted template should be consumed'
341 return _unnesttemplatelist(('template', parsed))
341 return _unnesttemplatelist(('template', parsed))
342
342
343 def _parseexpr(expr):
343 def _parseexpr(expr):
344 """Parse a template expression into tree
344 """Parse a template expression into tree
345
345
346 >>> _parseexpr(b'"foo"')
346 >>> _parseexpr(b'"foo"')
347 ('string', 'foo')
347 ('string', 'foo')
348 >>> _parseexpr(b'foo(bar)')
348 >>> _parseexpr(b'foo(bar)')
349 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
349 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
350 >>> _parseexpr(b'foo(')
350 >>> _parseexpr(b'foo(')
351 Traceback (most recent call last):
351 Traceback (most recent call last):
352 ...
352 ...
353 ParseError: ('not a prefix: end', 4)
353 ParseError: ('not a prefix: end', 4)
354 >>> _parseexpr(b'"foo" "bar"')
354 >>> _parseexpr(b'"foo" "bar"')
355 Traceback (most recent call last):
355 Traceback (most recent call last):
356 ...
356 ...
357 ParseError: ('invalid token', 7)
357 ParseError: ('invalid token', 7)
358 """
358 """
359 p = parser.parser(elements)
359 p = parser.parser(elements)
360 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
360 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
361 if pos != len(expr):
361 if pos != len(expr):
362 raise error.ParseError(_('invalid token'), pos)
362 raise error.ParseError(_('invalid token'), pos)
363 return _unnesttemplatelist(tree)
363 return _unnesttemplatelist(tree)
364
364
365 def prettyformat(tree):
365 def prettyformat(tree):
366 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
366 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
367
367
368 def compileexp(exp, context, curmethods):
368 def compileexp(exp, context, curmethods):
369 """Compile parsed template tree to (func, data) pair"""
369 """Compile parsed template tree to (func, data) pair"""
370 if not exp:
370 if not exp:
371 raise error.ParseError(_("missing argument"))
371 raise error.ParseError(_("missing argument"))
372 t = exp[0]
372 t = exp[0]
373 if t in curmethods:
373 if t in curmethods:
374 return curmethods[t](exp, context)
374 return curmethods[t](exp, context)
375 raise error.ParseError(_("unknown method '%s'") % t)
375 raise error.ParseError(_("unknown method '%s'") % t)
376
376
377 # template evaluation
377 # template evaluation
378
378
379 def getsymbol(exp):
379 def getsymbol(exp):
380 if exp[0] == 'symbol':
380 if exp[0] == 'symbol':
381 return exp[1]
381 return exp[1]
382 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
382 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
383
383
384 def getlist(x):
384 def getlist(x):
385 if not x:
385 if not x:
386 return []
386 return []
387 if x[0] == 'list':
387 if x[0] == 'list':
388 return getlist(x[1]) + [x[2]]
388 return getlist(x[1]) + [x[2]]
389 return [x]
389 return [x]
390
390
391 def gettemplate(exp, context):
391 def gettemplate(exp, context):
392 """Compile given template tree or load named template from map file;
392 """Compile given template tree or load named template from map file;
393 returns (func, data) pair"""
393 returns (func, data) pair"""
394 if exp[0] in ('template', 'string'):
394 if exp[0] in ('template', 'string'):
395 return compileexp(exp, context, methods)
395 return compileexp(exp, context, methods)
396 if exp[0] == 'symbol':
396 if exp[0] == 'symbol':
397 # unlike runsymbol(), here 'symbol' is always taken as template name
397 # unlike runsymbol(), here 'symbol' is always taken as template name
398 # even if it exists in mapping. this allows us to override mapping
398 # even if it exists in mapping. this allows us to override mapping
399 # by web templates, e.g. 'changelogtag' is redefined in map file.
399 # by web templates, e.g. 'changelogtag' is redefined in map file.
400 return context._load(exp[1])
400 return context._load(exp[1])
401 raise error.ParseError(_("expected template specifier"))
401 raise error.ParseError(_("expected template specifier"))
402
402
403 def _runrecursivesymbol(context, mapping, key):
403 def _runrecursivesymbol(context, mapping, key):
404 raise error.Abort(_("recursive reference '%s' in template") % key)
404 raise error.Abort(_("recursive reference '%s' in template") % key)
405
405
406 def buildtemplate(exp, context):
406 def buildtemplate(exp, context):
407 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
407 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
408 return (templateutil.runtemplate, ctmpl)
408 return (templateutil.runtemplate, ctmpl)
409
409
410 def buildfilter(exp, context):
410 def buildfilter(exp, context):
411 n = getsymbol(exp[2])
411 n = getsymbol(exp[2])
412 if n in context._filters:
412 if n in context._filters:
413 filt = context._filters[n]
413 filt = context._filters[n]
414 arg = compileexp(exp[1], context, methods)
414 arg = compileexp(exp[1], context, methods)
415 return (templateutil.runfilter, (arg, filt))
415 return (templateutil.runfilter, (arg, filt))
416 if n in context._funcs:
416 if n in context._funcs:
417 f = context._funcs[n]
417 f = context._funcs[n]
418 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
418 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
419 return (f, args)
419 return (f, args)
420 raise error.ParseError(_("unknown function '%s'") % n)
420 raise error.ParseError(_("unknown function '%s'") % n)
421
421
422 def buildmap(exp, context):
422 def buildmap(exp, context):
423 darg = compileexp(exp[1], context, methods)
423 darg = compileexp(exp[1], context, methods)
424 targ = gettemplate(exp[2], context)
424 targ = gettemplate(exp[2], context)
425 return (templateutil.runmap, (darg, targ))
425 return (templateutil.runmap, (darg, targ))
426
426
427 def buildmember(exp, context):
427 def buildmember(exp, context):
428 darg = compileexp(exp[1], context, methods)
428 darg = compileexp(exp[1], context, methods)
429 memb = getsymbol(exp[2])
429 memb = getsymbol(exp[2])
430 return (templateutil.runmember, (darg, memb))
430 return (templateutil.runmember, (darg, memb))
431
431
432 def buildnegate(exp, context):
432 def buildnegate(exp, context):
433 arg = compileexp(exp[1], context, exprmethods)
433 arg = compileexp(exp[1], context, exprmethods)
434 return (templateutil.runnegate, arg)
434 return (templateutil.runnegate, arg)
435
435
436 def buildarithmetic(exp, context, func):
436 def buildarithmetic(exp, context, func):
437 left = compileexp(exp[1], context, exprmethods)
437 left = compileexp(exp[1], context, exprmethods)
438 right = compileexp(exp[2], context, exprmethods)
438 right = compileexp(exp[2], context, exprmethods)
439 return (templateutil.runarithmetic, (func, left, right))
439 return (templateutil.runarithmetic, (func, left, right))
440
440
441 def buildfunc(exp, context):
441 def buildfunc(exp, context):
442 n = getsymbol(exp[1])
442 n = getsymbol(exp[1])
443 if n in context._funcs:
443 if n in context._funcs:
444 f = context._funcs[n]
444 f = context._funcs[n]
445 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
445 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
446 return (f, args)
446 return (f, args)
447 if n in context._filters:
447 if n in context._filters:
448 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
448 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
449 if len(args) != 1:
449 if len(args) != 1:
450 raise error.ParseError(_("filter %s expects one argument") % n)
450 raise error.ParseError(_("filter %s expects one argument") % n)
451 f = context._filters[n]
451 f = context._filters[n]
452 return (templateutil.runfilter, (args[0], f))
452 return (templateutil.runfilter, (args[0], f))
453 raise error.ParseError(_("unknown function '%s'") % n)
453 raise error.ParseError(_("unknown function '%s'") % n)
454
454
455 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
455 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
456 """Compile parsed tree of function arguments into list or dict of
456 """Compile parsed tree of function arguments into list or dict of
457 (func, data) pairs
457 (func, data) pairs
458
458
459 >>> context = engine(lambda t: (templateutil.runsymbol, t))
459 >>> context = engine(lambda t: (templateutil.runsymbol, t))
460 >>> def fargs(expr, argspec):
460 >>> def fargs(expr, argspec):
461 ... x = _parseexpr(expr)
461 ... x = _parseexpr(expr)
462 ... n = getsymbol(x[1])
462 ... n = getsymbol(x[1])
463 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
463 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
464 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
464 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
465 ['l', 'k']
465 ['l', 'k']
466 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
466 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
467 >>> list(args.keys()), list(args[b'opts'].keys())
467 >>> list(args.keys()), list(args[b'opts'].keys())
468 (['opts'], ['opts', 'k'])
468 (['opts'], ['opts', 'k'])
469 """
469 """
470 def compiledict(xs):
470 def compiledict(xs):
471 return util.sortdict((k, compileexp(x, context, curmethods))
471 return util.sortdict((k, compileexp(x, context, curmethods))
472 for k, x in xs.iteritems())
472 for k, x in xs.iteritems())
473 def compilelist(xs):
473 def compilelist(xs):
474 return [compileexp(x, context, curmethods) for x in xs]
474 return [compileexp(x, context, curmethods) for x in xs]
475
475
476 if not argspec:
476 if not argspec:
477 # filter or function with no argspec: return list of positional args
477 # filter or function with no argspec: return list of positional args
478 return compilelist(getlist(exp))
478 return compilelist(getlist(exp))
479
479
480 # function with argspec: return dict of named args
480 # function with argspec: return dict of named args
481 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
481 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
482 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
482 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
483 keyvaluenode='keyvalue', keynode='symbol')
483 keyvaluenode='keyvalue', keynode='symbol')
484 compargs = util.sortdict()
484 compargs = util.sortdict()
485 if varkey:
485 if varkey:
486 compargs[varkey] = compilelist(treeargs.pop(varkey))
486 compargs[varkey] = compilelist(treeargs.pop(varkey))
487 if optkey:
487 if optkey:
488 compargs[optkey] = compiledict(treeargs.pop(optkey))
488 compargs[optkey] = compiledict(treeargs.pop(optkey))
489 compargs.update(compiledict(treeargs))
489 compargs.update(compiledict(treeargs))
490 return compargs
490 return compargs
491
491
492 def buildkeyvaluepair(exp, content):
492 def buildkeyvaluepair(exp, content):
493 raise error.ParseError(_("can't use a key-value pair in this context"))
493 raise error.ParseError(_("can't use a key-value pair in this context"))
494
494
495 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
495 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
496 exprmethods = {
496 exprmethods = {
497 "integer": lambda e, c: (templateutil.runinteger, e[1]),
497 "integer": lambda e, c: (templateutil.runinteger, e[1]),
498 "string": lambda e, c: (templateutil.runstring, e[1]),
498 "string": lambda e, c: (templateutil.runstring, e[1]),
499 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
499 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
500 "template": buildtemplate,
500 "template": buildtemplate,
501 "group": lambda e, c: compileexp(e[1], c, exprmethods),
501 "group": lambda e, c: compileexp(e[1], c, exprmethods),
502 ".": buildmember,
502 ".": buildmember,
503 "|": buildfilter,
503 "|": buildfilter,
504 "%": buildmap,
504 "%": buildmap,
505 "func": buildfunc,
505 "func": buildfunc,
506 "keyvalue": buildkeyvaluepair,
506 "keyvalue": buildkeyvaluepair,
507 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
507 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
508 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
508 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
509 "negate": buildnegate,
509 "negate": buildnegate,
510 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
510 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
511 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
511 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
512 }
512 }
513
513
514 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
514 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
515 methods = exprmethods.copy()
515 methods = exprmethods.copy()
516 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
516 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
517
517
518 class _aliasrules(parser.basealiasrules):
518 class _aliasrules(parser.basealiasrules):
519 """Parsing and expansion rule set of template aliases"""
519 """Parsing and expansion rule set of template aliases"""
520 _section = _('template alias')
520 _section = _('template alias')
521 _parse = staticmethod(_parseexpr)
521 _parse = staticmethod(_parseexpr)
522
522
523 @staticmethod
523 @staticmethod
524 def _trygetfunc(tree):
524 def _trygetfunc(tree):
525 """Return (name, args) if tree is func(...) or ...|filter; otherwise
525 """Return (name, args) if tree is func(...) or ...|filter; otherwise
526 None"""
526 None"""
527 if tree[0] == 'func' and tree[1][0] == 'symbol':
527 if tree[0] == 'func' and tree[1][0] == 'symbol':
528 return tree[1][1], getlist(tree[2])
528 return tree[1][1], getlist(tree[2])
529 if tree[0] == '|' and tree[2][0] == 'symbol':
529 if tree[0] == '|' and tree[2][0] == 'symbol':
530 return tree[2][1], [tree[1]]
530 return tree[2][1], [tree[1]]
531
531
532 def expandaliases(tree, aliases):
532 def expandaliases(tree, aliases):
533 """Return new tree of aliases are expanded"""
533 """Return new tree of aliases are expanded"""
534 aliasmap = _aliasrules.buildmap(aliases)
534 aliasmap = _aliasrules.buildmap(aliases)
535 return _aliasrules.expand(aliasmap, tree)
535 return _aliasrules.expand(aliasmap, tree)
536
536
537 # template engine
537 # template engine
538
538
539 def unquotestring(s):
539 def unquotestring(s):
540 '''unwrap quotes if any; otherwise returns unmodified string'''
540 '''unwrap quotes if any; otherwise returns unmodified string'''
541 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
541 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
542 return s
542 return s
543 return s[1:-1]
543 return s[1:-1]
544
544
545 class resourcemapper(object):
545 class resourcemapper(object):
546 """Mapper of internal template resources"""
546 """Mapper of internal template resources"""
547
547
548 __metaclass__ = abc.ABCMeta
548 __metaclass__ = abc.ABCMeta
549
549
550 @abc.abstractmethod
550 @abc.abstractmethod
551 def availablekeys(self, context, mapping):
551 def availablekeys(self, context, mapping):
552 """Return a set of available resource keys based on the given mapping"""
552 """Return a set of available resource keys based on the given mapping"""
553
553
554 @abc.abstractmethod
554 @abc.abstractmethod
555 def knownkeys(self):
555 def knownkeys(self):
556 """Return a set of supported resource keys"""
556 """Return a set of supported resource keys"""
557
557
558 @abc.abstractmethod
558 @abc.abstractmethod
559 def lookup(self, context, mapping, key):
559 def lookup(self, context, mapping, key):
560 """Return a resource for the key if available; otherwise None"""
560 """Return a resource for the key if available; otherwise None"""
561
561
562 @abc.abstractmethod
562 @abc.abstractmethod
563 def populatemap(self, context, origmapping, newmapping):
563 def populatemap(self, context, origmapping, newmapping):
564 """Return a dict of additional mapping items which should be paired
564 """Return a dict of additional mapping items which should be paired
565 with the given new mapping"""
565 with the given new mapping"""
566
566
567 class nullresourcemapper(resourcemapper):
567 class nullresourcemapper(resourcemapper):
568 def availablekeys(self, context, mapping):
568 def availablekeys(self, context, mapping):
569 return set()
569 return set()
570
570
571 def knownkeys(self):
571 def knownkeys(self):
572 return set()
572 return set()
573
573
574 def lookup(self, context, mapping, key):
574 def lookup(self, context, mapping, key):
575 return None
575 return None
576
576
577 def populatemap(self, context, origmapping, newmapping):
577 def populatemap(self, context, origmapping, newmapping):
578 return {}
578 return {}
579
579
580 class engine(object):
580 class engine(object):
581 '''template expansion engine.
581 '''template expansion engine.
582
582
583 template expansion works like this. a map file contains key=value
583 template expansion works like this. a map file contains key=value
584 pairs. if value is quoted, it is treated as string. otherwise, it
584 pairs. if value is quoted, it is treated as string. otherwise, it
585 is treated as name of template file.
585 is treated as name of template file.
586
586
587 templater is asked to expand a key in map. it looks up key, and
587 templater is asked to expand a key in map. it looks up key, and
588 looks for strings like this: {foo}. it expands {foo} by looking up
588 looks for strings like this: {foo}. it expands {foo} by looking up
589 foo in map, and substituting it. expansion is recursive: it stops
589 foo in map, and substituting it. expansion is recursive: it stops
590 when there is no more {foo} to replace.
590 when there is no more {foo} to replace.
591
591
592 expansion also allows formatting and filtering.
592 expansion also allows formatting and filtering.
593
593
594 format uses key to expand each item in list. syntax is
594 format uses key to expand each item in list. syntax is
595 {key%format}.
595 {key%format}.
596
596
597 filter uses function to transform value. syntax is
597 filter uses function to transform value. syntax is
598 {key|filter1|filter2|...}.'''
598 {key|filter1|filter2|...}.'''
599
599
600 def __init__(self, loader, filters=None, defaults=None, resources=None,
600 def __init__(self, loader, filters=None, defaults=None, resources=None):
601 aliases=()):
602 self._loader = loader
601 self._loader = loader
603 if filters is None:
602 if filters is None:
604 filters = {}
603 filters = {}
605 self._filters = filters
604 self._filters = filters
606 self._funcs = templatefuncs.funcs # make this a parameter if needed
605 self._funcs = templatefuncs.funcs # make this a parameter if needed
607 if defaults is None:
606 if defaults is None:
608 defaults = {}
607 defaults = {}
609 if resources is None:
608 if resources is None:
610 resources = nullresourcemapper()
609 resources = nullresourcemapper()
611 self._defaults = defaults
610 self._defaults = defaults
612 self._resources = resources
611 self._resources = resources
613 self._aliasmap = _aliasrules.buildmap(aliases)
614 self._cache = {} # key: (func, data)
612 self._cache = {} # key: (func, data)
615 self._tmplcache = {} # literal template: (func, data)
613 self._tmplcache = {} # literal template: (func, data)
616
614
617 def overlaymap(self, origmapping, newmapping):
615 def overlaymap(self, origmapping, newmapping):
618 """Create combined mapping from the original mapping and partial
616 """Create combined mapping from the original mapping and partial
619 mapping to override the original"""
617 mapping to override the original"""
620 # do not copy symbols which overrides the defaults depending on
618 # do not copy symbols which overrides the defaults depending on
621 # new resources, so the defaults will be re-evaluated (issue5612)
619 # new resources, so the defaults will be re-evaluated (issue5612)
622 knownres = self._resources.knownkeys()
620 knownres = self._resources.knownkeys()
623 newres = self._resources.availablekeys(self, newmapping)
621 newres = self._resources.availablekeys(self, newmapping)
624 mapping = {k: v for k, v in origmapping.iteritems()
622 mapping = {k: v for k, v in origmapping.iteritems()
625 if (k in knownres # not a symbol per self.symbol()
623 if (k in knownres # not a symbol per self.symbol()
626 or newres.isdisjoint(self._defaultrequires(k)))}
624 or newres.isdisjoint(self._defaultrequires(k)))}
627 mapping.update(newmapping)
625 mapping.update(newmapping)
628 mapping.update(
626 mapping.update(
629 self._resources.populatemap(self, origmapping, newmapping))
627 self._resources.populatemap(self, origmapping, newmapping))
630 return mapping
628 return mapping
631
629
632 def _defaultrequires(self, key):
630 def _defaultrequires(self, key):
633 """Resource keys required by the specified default symbol function"""
631 """Resource keys required by the specified default symbol function"""
634 v = self._defaults.get(key)
632 v = self._defaults.get(key)
635 if v is None or not callable(v):
633 if v is None or not callable(v):
636 return ()
634 return ()
637 return getattr(v, '_requires', ())
635 return getattr(v, '_requires', ())
638
636
639 def symbol(self, mapping, key):
637 def symbol(self, mapping, key):
640 """Resolve symbol to value or function; None if nothing found"""
638 """Resolve symbol to value or function; None if nothing found"""
641 v = None
639 v = None
642 if key not in self._resources.knownkeys():
640 if key not in self._resources.knownkeys():
643 v = mapping.get(key)
641 v = mapping.get(key)
644 if v is None:
642 if v is None:
645 v = self._defaults.get(key)
643 v = self._defaults.get(key)
646 return v
644 return v
647
645
648 def availableresourcekeys(self, mapping):
646 def availableresourcekeys(self, mapping):
649 """Return a set of available resource keys based on the given mapping"""
647 """Return a set of available resource keys based on the given mapping"""
650 return self._resources.availablekeys(self, mapping)
648 return self._resources.availablekeys(self, mapping)
651
649
652 def knownresourcekeys(self):
650 def knownresourcekeys(self):
653 """Return a set of supported resource keys"""
651 """Return a set of supported resource keys"""
654 return self._resources.knownkeys()
652 return self._resources.knownkeys()
655
653
656 def resource(self, mapping, key):
654 def resource(self, mapping, key):
657 """Return internal data (e.g. cache) used for keyword/function
655 """Return internal data (e.g. cache) used for keyword/function
658 evaluation"""
656 evaluation"""
659 v = self._resources.lookup(self, mapping, key)
657 v = self._resources.lookup(self, mapping, key)
660 if v is None:
658 if v is None:
661 raise templateutil.ResourceUnavailable(
659 raise templateutil.ResourceUnavailable(
662 _('template resource not available: %s') % key)
660 _('template resource not available: %s') % key)
663 return v
661 return v
664
662
665 def _load(self, t):
663 def _load(self, t):
666 '''load, parse, and cache a template'''
664 '''load, parse, and cache a template'''
667 if t not in self._cache:
665 if t not in self._cache:
668 x = parse(self._loader(t))
666 x = self._loader(t)
669 if self._aliasmap:
670 x = _aliasrules.expand(self._aliasmap, x)
671 # put poison to cut recursion while compiling 't'
667 # put poison to cut recursion while compiling 't'
672 self._cache[t] = (_runrecursivesymbol, t)
668 self._cache[t] = (_runrecursivesymbol, t)
673 try:
669 try:
674 self._cache[t] = compileexp(x, self, methods)
670 self._cache[t] = compileexp(x, self, methods)
675 except: # re-raises
671 except: # re-raises
676 del self._cache[t]
672 del self._cache[t]
677 raise
673 raise
678 return self._cache[t]
674 return self._cache[t]
679
675
680 def _parse(self, tmpl):
676 def _parse(self, tmpl):
681 """Parse and cache a literal template"""
677 """Parse and cache a literal template"""
682 if tmpl not in self._tmplcache:
678 if tmpl not in self._tmplcache:
683 x = parse(tmpl)
679 x = parse(tmpl)
684 self._tmplcache[tmpl] = compileexp(x, self, methods)
680 self._tmplcache[tmpl] = compileexp(x, self, methods)
685 return self._tmplcache[tmpl]
681 return self._tmplcache[tmpl]
686
682
687 def preload(self, t):
683 def preload(self, t):
688 """Load, parse, and cache the specified template if available"""
684 """Load, parse, and cache the specified template if available"""
689 try:
685 try:
690 self._load(t)
686 self._load(t)
691 return True
687 return True
692 except templateutil.TemplateNotFound:
688 except templateutil.TemplateNotFound:
693 return False
689 return False
694
690
695 def process(self, t, mapping):
691 def process(self, t, mapping):
696 '''Perform expansion. t is name of map element to expand.
692 '''Perform expansion. t is name of map element to expand.
697 mapping contains added elements for use during expansion. Is a
693 mapping contains added elements for use during expansion. Is a
698 generator.'''
694 generator.'''
699 func, data = self._load(t)
695 func, data = self._load(t)
700 return self._expand(func, data, mapping)
696 return self._expand(func, data, mapping)
701
697
702 def expand(self, tmpl, mapping):
698 def expand(self, tmpl, mapping):
703 """Perform expansion over a literal template
699 """Perform expansion over a literal template
704
700
705 No user aliases will be expanded since this is supposed to be called
701 No user aliases will be expanded since this is supposed to be called
706 with an internal template string.
702 with an internal template string.
707 """
703 """
708 func, data = self._parse(tmpl)
704 func, data = self._parse(tmpl)
709 return self._expand(func, data, mapping)
705 return self._expand(func, data, mapping)
710
706
711 def _expand(self, func, data, mapping):
707 def _expand(self, func, data, mapping):
712 # populate additional items only if they don't exist in the given
708 # populate additional items only if they don't exist in the given
713 # mapping. this is slightly different from overlaymap() because the
709 # mapping. this is slightly different from overlaymap() because the
714 # initial 'revcache' may contain pre-computed items.
710 # initial 'revcache' may contain pre-computed items.
715 extramapping = self._resources.populatemap(self, {}, mapping)
711 extramapping = self._resources.populatemap(self, {}, mapping)
716 if extramapping:
712 if extramapping:
717 extramapping.update(mapping)
713 extramapping.update(mapping)
718 mapping = extramapping
714 mapping = extramapping
719 return templateutil.flatten(self, mapping, func(self, mapping, data))
715 return templateutil.flatten(self, mapping, func(self, mapping, data))
720
716
721 def stylelist():
717 def stylelist():
722 paths = templatepaths()
718 paths = templatepaths()
723 if not paths:
719 if not paths:
724 return _('no templates found, try `hg debuginstall` for more info')
720 return _('no templates found, try `hg debuginstall` for more info')
725 dirlist = os.listdir(paths[0])
721 dirlist = os.listdir(paths[0])
726 stylelist = []
722 stylelist = []
727 for file in dirlist:
723 for file in dirlist:
728 split = file.split(".")
724 split = file.split(".")
729 if split[-1] in ('orig', 'rej'):
725 if split[-1] in ('orig', 'rej'):
730 continue
726 continue
731 if split[0] == "map-cmdline":
727 if split[0] == "map-cmdline":
732 stylelist.append(split[1])
728 stylelist.append(split[1])
733 return ", ".join(sorted(stylelist))
729 return ", ".join(sorted(stylelist))
734
730
735 def _readmapfile(mapfile):
731 def _readmapfile(mapfile):
736 """Load template elements from the given map file"""
732 """Load template elements from the given map file"""
737 if not os.path.exists(mapfile):
733 if not os.path.exists(mapfile):
738 raise error.Abort(_("style '%s' not found") % mapfile,
734 raise error.Abort(_("style '%s' not found") % mapfile,
739 hint=_("available styles: %s") % stylelist())
735 hint=_("available styles: %s") % stylelist())
740
736
741 base = os.path.dirname(mapfile)
737 base = os.path.dirname(mapfile)
742 conf = config.config(includepaths=templatepaths())
738 conf = config.config(includepaths=templatepaths())
743 conf.read(mapfile, remap={'': 'templates'})
739 conf.read(mapfile, remap={'': 'templates'})
744
740
745 cache = {}
741 cache = {}
746 tmap = {}
742 tmap = {}
747 aliases = []
743 aliases = []
748
744
749 val = conf.get('templates', '__base__')
745 val = conf.get('templates', '__base__')
750 if val and val[0] not in "'\"":
746 if val and val[0] not in "'\"":
751 # treat as a pointer to a base class for this style
747 # treat as a pointer to a base class for this style
752 path = util.normpath(os.path.join(base, val))
748 path = util.normpath(os.path.join(base, val))
753
749
754 # fallback check in template paths
750 # fallback check in template paths
755 if not os.path.exists(path):
751 if not os.path.exists(path):
756 for p in templatepaths():
752 for p in templatepaths():
757 p2 = util.normpath(os.path.join(p, val))
753 p2 = util.normpath(os.path.join(p, val))
758 if os.path.isfile(p2):
754 if os.path.isfile(p2):
759 path = p2
755 path = p2
760 break
756 break
761 p3 = util.normpath(os.path.join(p2, "map"))
757 p3 = util.normpath(os.path.join(p2, "map"))
762 if os.path.isfile(p3):
758 if os.path.isfile(p3):
763 path = p3
759 path = p3
764 break
760 break
765
761
766 cache, tmap, aliases = _readmapfile(path)
762 cache, tmap, aliases = _readmapfile(path)
767
763
768 for key, val in conf['templates'].items():
764 for key, val in conf['templates'].items():
769 if not val:
765 if not val:
770 raise error.ParseError(_('missing value'),
766 raise error.ParseError(_('missing value'),
771 conf.source('templates', key))
767 conf.source('templates', key))
772 if val[0] in "'\"":
768 if val[0] in "'\"":
773 if val[0] != val[-1]:
769 if val[0] != val[-1]:
774 raise error.ParseError(_('unmatched quotes'),
770 raise error.ParseError(_('unmatched quotes'),
775 conf.source('templates', key))
771 conf.source('templates', key))
776 cache[key] = unquotestring(val)
772 cache[key] = unquotestring(val)
777 elif key != '__base__':
773 elif key != '__base__':
778 tmap[key] = os.path.join(base, val)
774 tmap[key] = os.path.join(base, val)
779 aliases.extend(conf['templatealias'].items())
775 aliases.extend(conf['templatealias'].items())
780 return cache, tmap, aliases
776 return cache, tmap, aliases
781
777
782 class templater(object):
778 class templater(object):
783
779
784 def __init__(self, filters=None, defaults=None, resources=None,
780 def __init__(self, filters=None, defaults=None, resources=None,
785 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
781 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
786 """Create template engine optionally with preloaded template fragments
782 """Create template engine optionally with preloaded template fragments
787
783
788 - ``filters``: a dict of functions to transform a value into another.
784 - ``filters``: a dict of functions to transform a value into another.
789 - ``defaults``: a dict of symbol values/functions; may be overridden
785 - ``defaults``: a dict of symbol values/functions; may be overridden
790 by a ``mapping`` dict.
786 by a ``mapping`` dict.
791 - ``resources``: a resourcemapper object to look up internal data
787 - ``resources``: a resourcemapper object to look up internal data
792 (e.g. cache), inaccessible from user template.
788 (e.g. cache), inaccessible from user template.
793 - ``cache``: a dict of preloaded template fragments.
789 - ``cache``: a dict of preloaded template fragments.
794 - ``aliases``: a list of alias (name, replacement) pairs.
790 - ``aliases``: a list of alias (name, replacement) pairs.
795
791
796 self.cache may be updated later to register additional template
792 self.cache may be updated later to register additional template
797 fragments.
793 fragments.
798 """
794 """
799 if filters is None:
795 if filters is None:
800 filters = {}
796 filters = {}
801 if defaults is None:
797 if defaults is None:
802 defaults = {}
798 defaults = {}
803 if cache is None:
799 if cache is None:
804 cache = {}
800 cache = {}
805 self.cache = cache.copy()
801 self.cache = cache.copy()
806 self._map = {}
802 self._map = {}
807 self._filters = templatefilters.filters.copy()
803 self._filters = templatefilters.filters.copy()
808 self._filters.update(filters)
804 self._filters.update(filters)
809 self.defaults = defaults
805 self.defaults = defaults
810 self._resources = resources
806 self._resources = resources
811 self._aliases = aliases
807 self._aliasmap = _aliasrules.buildmap(aliases)
812 self._minchunk, self._maxchunk = minchunk, maxchunk
808 self._minchunk, self._maxchunk = minchunk, maxchunk
813
809
814 @classmethod
810 @classmethod
815 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
811 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
816 cache=None, minchunk=1024, maxchunk=65536):
812 cache=None, minchunk=1024, maxchunk=65536):
817 """Create templater from the specified map file"""
813 """Create templater from the specified map file"""
818 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
814 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
819 cache, tmap, aliases = _readmapfile(mapfile)
815 cache, tmap, aliases = _readmapfile(mapfile)
820 t.cache.update(cache)
816 t.cache.update(cache)
821 t._map = tmap
817 t._map = tmap
822 t._aliases = aliases
818 t._aliasmap = _aliasrules.buildmap(aliases)
823 return t
819 return t
824
820
825 def __contains__(self, key):
821 def __contains__(self, key):
826 return key in self.cache or key in self._map
822 return key in self.cache or key in self._map
827
823
828 def load(self, t):
824 def load(self, t):
829 '''Get the template for the given template name. Use a local cache.'''
825 """Get parsed tree for the given template name. Use a local cache."""
830 if t not in self.cache:
826 if t not in self.cache:
831 try:
827 try:
832 self.cache[t] = util.readfile(self._map[t])
828 self.cache[t] = util.readfile(self._map[t])
833 except KeyError as inst:
829 except KeyError as inst:
834 raise templateutil.TemplateNotFound(
830 raise templateutil.TemplateNotFound(
835 _('"%s" not in template map') % inst.args[0])
831 _('"%s" not in template map') % inst.args[0])
836 except IOError as inst:
832 except IOError as inst:
837 reason = (_('template file %s: %s')
833 reason = (_('template file %s: %s')
838 % (self._map[t],
834 % (self._map[t],
839 stringutil.forcebytestr(inst.args[1])))
835 stringutil.forcebytestr(inst.args[1])))
840 raise IOError(inst.args[0], encoding.strfromlocal(reason))
836 raise IOError(inst.args[0], encoding.strfromlocal(reason))
841 return self.cache[t]
837 return self._parse(self.cache[t])
838
839 def _parse(self, tmpl):
840 x = parse(tmpl)
841 if self._aliasmap:
842 x = _aliasrules.expand(self._aliasmap, x)
843 return x
842
844
843 def renderdefault(self, mapping):
845 def renderdefault(self, mapping):
844 """Render the default unnamed template and return result as string"""
846 """Render the default unnamed template and return result as string"""
845 return self.render('', mapping)
847 return self.render('', mapping)
846
848
847 def render(self, t, mapping):
849 def render(self, t, mapping):
848 """Render the specified named template and return result as string"""
850 """Render the specified named template and return result as string"""
849 return b''.join(self.generate(t, mapping))
851 return b''.join(self.generate(t, mapping))
850
852
851 def generate(self, t, mapping):
853 def generate(self, t, mapping):
852 """Return a generator that renders the specified named template and
854 """Return a generator that renders the specified named template and
853 yields chunks"""
855 yields chunks"""
854 proc = engine(self.load, self._filters, self.defaults, self._resources,
856 proc = engine(self.load, self._filters, self.defaults, self._resources)
855 self._aliases)
856 stream = proc.process(t, mapping)
857 stream = proc.process(t, mapping)
857 if self._minchunk:
858 if self._minchunk:
858 stream = util.increasingchunks(stream, min=self._minchunk,
859 stream = util.increasingchunks(stream, min=self._minchunk,
859 max=self._maxchunk)
860 max=self._maxchunk)
860 return stream
861 return stream
861
862
862 def templatepaths():
863 def templatepaths():
863 '''return locations used for template files.'''
864 '''return locations used for template files.'''
864 pathsrel = ['templates']
865 pathsrel = ['templates']
865 paths = [os.path.normpath(os.path.join(util.datapath, f))
866 paths = [os.path.normpath(os.path.join(util.datapath, f))
866 for f in pathsrel]
867 for f in pathsrel]
867 return [p for p in paths if os.path.isdir(p)]
868 return [p for p in paths if os.path.isdir(p)]
868
869
869 def templatepath(name):
870 def templatepath(name):
870 '''return location of template file. returns None if not found.'''
871 '''return location of template file. returns None if not found.'''
871 for p in templatepaths():
872 for p in templatepaths():
872 f = os.path.join(p, name)
873 f = os.path.join(p, name)
873 if os.path.exists(f):
874 if os.path.exists(f):
874 return f
875 return f
875 return None
876 return None
876
877
877 def stylemap(styles, paths=None):
878 def stylemap(styles, paths=None):
878 """Return path to mapfile for a given style.
879 """Return path to mapfile for a given style.
879
880
880 Searches mapfile in the following locations:
881 Searches mapfile in the following locations:
881 1. templatepath/style/map
882 1. templatepath/style/map
882 2. templatepath/map-style
883 2. templatepath/map-style
883 3. templatepath/map
884 3. templatepath/map
884 """
885 """
885
886
886 if paths is None:
887 if paths is None:
887 paths = templatepaths()
888 paths = templatepaths()
888 elif isinstance(paths, bytes):
889 elif isinstance(paths, bytes):
889 paths = [paths]
890 paths = [paths]
890
891
891 if isinstance(styles, bytes):
892 if isinstance(styles, bytes):
892 styles = [styles]
893 styles = [styles]
893
894
894 for style in styles:
895 for style in styles:
895 # only plain name is allowed to honor template paths
896 # only plain name is allowed to honor template paths
896 if (not style
897 if (not style
897 or style in (pycompat.oscurdir, pycompat.ospardir)
898 or style in (pycompat.oscurdir, pycompat.ospardir)
898 or pycompat.ossep in style
899 or pycompat.ossep in style
899 or pycompat.osaltsep and pycompat.osaltsep in style):
900 or pycompat.osaltsep and pycompat.osaltsep in style):
900 continue
901 continue
901 locations = [os.path.join(style, 'map'), 'map-' + style]
902 locations = [os.path.join(style, 'map'), 'map-' + style]
902 locations.append('map')
903 locations.append('map')
903
904
904 for path in paths:
905 for path in paths:
905 for location in locations:
906 for location in locations:
906 mapfile = os.path.join(path, location)
907 mapfile = os.path.join(path, location)
907 if os.path.isfile(mapfile):
908 if os.path.isfile(mapfile):
908 return style, mapfile
909 return style, mapfile
909
910
910 raise RuntimeError("No hgweb templates found in %r" % paths)
911 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now