##// END OF EJS Templates
templater: introduce one-pass parsing of nested template strings...
Yuya Nishihara -
r25783:1f6878c8 default
parent child Browse files
Show More
@@ -24,6 +24,7 b' elements = {'
24 "symbol": (0, ("symbol",), None),
24 "symbol": (0, ("symbol",), None),
25 "string": (0, ("template",), None),
25 "string": (0, ("template",), None),
26 "rawstring": (0, ("rawstring",), None),
26 "rawstring": (0, ("rawstring",), None),
27 "template": (0, ("template",), None),
27 "end": (0, None, None),
28 "end": (0, None, None),
28 }
29 }
29
30
@@ -35,6 +36,11 b' def tokenize(program, start, end):'
35 pass
36 pass
36 elif c in "(,)%|": # handle simple operators
37 elif c in "(,)%|": # handle simple operators
37 yield (c, None, pos)
38 yield (c, None, pos)
39 elif c in '"\'': # handle quoted templates
40 s = pos + 1
41 data, pos = _parsetemplate(program, s, end, c)
42 yield ('template', data, s)
43 pos -= 1
38 elif (c in '"\'' or c == 'r' and
44 elif (c in '"\'' or c == 'r' and
39 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
45 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
40 if c == 'r':
46 if c == 'r':
@@ -89,7 +95,7 b' def tokenize(program, start, end):'
89 pos += 1
95 pos += 1
90 token = 'rawstring'
96 token = 'rawstring'
91 else:
97 else:
92 token = 'string'
98 token = 'template'
93 quote = program[pos:pos + 2]
99 quote = program[pos:pos + 2]
94 s = pos = pos + 2
100 s = pos = pos + 2
95 while pos < end: # find closing escaped quote
101 while pos < end: # find closing escaped quote
@@ -102,6 +108,8 b' def tokenize(program, start, end):'
102 data = program[s:pos].decode('string-escape')
108 data = program[s:pos].decode('string-escape')
103 except ValueError: # unbalanced escapes
109 except ValueError: # unbalanced escapes
104 raise error.ParseError(_("syntax error"), s)
110 raise error.ParseError(_("syntax error"), s)
111 if token == 'template':
112 data = _parsetemplate(data, 0, len(data))[0]
105 yield (token, data, s)
113 yield (token, data, s)
106 pos += 1
114 pos += 1
107 break
115 break
@@ -127,27 +135,47 b' def tokenize(program, start, end):'
127 pos += 1
135 pos += 1
128 raise error.ParseError(_("unterminated template expansion"), start)
136 raise error.ParseError(_("unterminated template expansion"), start)
129
137
130 def _parsetemplate(tmpl, start, stop):
138 def _parsetemplate(tmpl, start, stop, quote=''):
139 r"""
140 >>> _parsetemplate('foo{bar}"baz', 0, 12)
141 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
142 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
143 ([('string', 'foo'), ('symbol', 'bar')], 9)
144 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
145 ([('string', 'foo')], 4)
146 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
147 ([('string', 'foo"'), ('string', 'bar')], 9)
148 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
149 ([('string', 'foo\\\\')], 6)
150 """
131 parsed = []
151 parsed = []
152 sepchars = '{' + quote
132 pos = start
153 pos = start
133 p = parser.parser(elements)
154 p = parser.parser(elements)
134 while pos < stop:
155 while pos < stop:
135 n = tmpl.find('{', pos, stop)
156 n = min((tmpl.find(c, pos, stop) for c in sepchars),
157 key=lambda n: (n < 0, n))
136 if n < 0:
158 if n < 0:
137 parsed.append(('string', tmpl[pos:stop]))
159 parsed.append(('string', tmpl[pos:stop]))
138 pos = stop
160 pos = stop
139 break
161 break
162 c = tmpl[n]
140 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
163 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
141 if bs % 2 == 1:
164 if bs % 2 == 1:
142 # escaped (e.g. '\{', '\\\{', but not '\\{')
165 # escaped (e.g. '\{', '\\\{', but not '\\{')
143 parsed.append(('string', (tmpl[pos:n - 1] + "{")))
166 parsed.append(('string', (tmpl[pos:n - 1] + c)))
144 pos = n + 1
167 pos = n + 1
145 continue
168 continue
146 if n > pos:
169 if n > pos:
147 parsed.append(('string', tmpl[pos:n]))
170 parsed.append(('string', tmpl[pos:n]))
171 if c == quote:
172 return parsed, n + 1
148
173
149 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop))
174 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop))
150 parsed.append(parseres)
175 parsed.append(parseres)
176
177 if quote:
178 raise error.ParseError(_("unterminated string"), start)
151 return parsed, pos
179 return parsed, pos
152
180
153 def compiletemplate(tmpl, context):
181 def compiletemplate(tmpl, context):
@@ -182,7 +210,7 b' def getfilter(exp, context):'
182
210
183 def gettemplate(exp, context):
211 def gettemplate(exp, context):
184 if exp[0] == 'template':
212 if exp[0] == 'template':
185 return compiletemplate(exp[1], context)
213 return [compileexp(e, context, methods) for e in exp[1]]
186 if exp[0] == 'symbol':
214 if exp[0] == 'symbol':
187 # unlike runsymbol(), here 'symbol' is always taken as template name
215 # unlike runsymbol(), here 'symbol' is always taken as template name
188 # even if it exists in mapping. this allows us to override mapping
216 # even if it exists in mapping. this allows us to override mapping
@@ -215,7 +243,7 b' def runsymbol(context, mapping, key):'
215 return v
243 return v
216
244
217 def buildtemplate(exp, context):
245 def buildtemplate(exp, context):
218 ctmpl = compiletemplate(exp[1], context)
246 ctmpl = [compileexp(e, context, methods) for e in exp[1]]
219 if len(ctmpl) == 1:
247 if len(ctmpl) == 1:
220 return ctmpl[0] # fast path for string with no template fragment
248 return ctmpl[0] # fast path for string with no template fragment
221 return (runtemplate, ctmpl)
249 return (runtemplate, ctmpl)
@@ -2541,6 +2541,16 b' Behind the scenes, this will throw Value'
2541 abort: template filter 'datefilter' is not compatible with keyword 'author'
2541 abort: template filter 'datefilter' is not compatible with keyword 'author'
2542 [255]
2542 [255]
2543
2543
2544 Error in nested template:
2545
2546 $ hg log -T '{"date'
2547 hg: parse error at 2: unterminated string
2548 [255]
2549
2550 $ hg log -T '{"foo{date|=}"}'
2551 hg: parse error at 11: syntax error
2552 [255]
2553
2544 Thrown an error if a template function doesn't exist
2554 Thrown an error if a template function doesn't exist
2545
2555
2546 $ hg tip --template '{foo()}\n'
2556 $ hg tip --template '{foo()}\n'
@@ -2952,7 +2962,7 b' escaped single quotes and errors:'
2952 $ hg log -r 2 -T "{if(rev, '{if(rev, r\'foo\')}')}"'\n'
2962 $ hg log -r 2 -T "{if(rev, '{if(rev, r\'foo\')}')}"'\n'
2953 foo
2963 foo
2954 $ hg log -r 2 -T '{if(rev, "{if(rev, \")}")}\n'
2964 $ hg log -r 2 -T '{if(rev, "{if(rev, \")}")}\n'
2955 hg: parse error at 11: unterminated string
2965 hg: parse error at 21: unterminated string
2956 [255]
2966 [255]
2957 $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
2967 $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
2958 hg: parse error at 11: syntax error
2968 hg: parse error at 11: syntax error
@@ -3069,6 +3079,14 b' Test string escaping in nested expressio'
3069 3:\x6eo user, \x6eo domai\x6e
3079 3:\x6eo user, \x6eo domai\x6e
3070 4:\x5c\x786eew bra\x5c\x786ech
3080 4:\x5c\x786eew bra\x5c\x786ech
3071
3081
3082 Test quotes in nested expression are evaluated just like a $(command)
3083 substitution in POSIX shells:
3084
3085 $ hg log -R a -r 8 -T '{"{"{rev}:{node|short}"}"}\n'
3086 8:95c24699272e
3087 $ hg log -R a -r 8 -T '{"{"\{{rev}} \"{node|short}\""}"}\n'
3088 {8} "95c24699272e"
3089
3072 Test recursive evaluation:
3090 Test recursive evaluation:
3073
3091
3074 $ hg init r
3092 $ hg init r
@@ -26,6 +26,7 b" testmod('mercurial.revset')"
26 testmod('mercurial.store')
26 testmod('mercurial.store')
27 testmod('mercurial.subrepo')
27 testmod('mercurial.subrepo')
28 testmod('mercurial.templatefilters')
28 testmod('mercurial.templatefilters')
29 testmod('mercurial.templater')
29 testmod('mercurial.ui')
30 testmod('mercurial.ui')
30 testmod('mercurial.url')
31 testmod('mercurial.url')
31 testmod('mercurial.util')
32 testmod('mercurial.util')
General Comments 0
You need to be logged in to leave comments. Login now