##// 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 24 "symbol": (0, ("symbol",), None),
25 25 "string": (0, ("template",), None),
26 26 "rawstring": (0, ("rawstring",), None),
27 "template": (0, ("template",), None),
27 28 "end": (0, None, None),
28 29 }
29 30
@@ -35,6 +36,11 b' def tokenize(program, start, end):'
35 36 pass
36 37 elif c in "(,)%|": # handle simple operators
37 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 44 elif (c in '"\'' or c == 'r' and
39 45 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
40 46 if c == 'r':
@@ -89,7 +95,7 b' def tokenize(program, start, end):'
89 95 pos += 1
90 96 token = 'rawstring'
91 97 else:
92 token = 'string'
98 token = 'template'
93 99 quote = program[pos:pos + 2]
94 100 s = pos = pos + 2
95 101 while pos < end: # find closing escaped quote
@@ -102,6 +108,8 b' def tokenize(program, start, end):'
102 108 data = program[s:pos].decode('string-escape')
103 109 except ValueError: # unbalanced escapes
104 110 raise error.ParseError(_("syntax error"), s)
111 if token == 'template':
112 data = _parsetemplate(data, 0, len(data))[0]
105 113 yield (token, data, s)
106 114 pos += 1
107 115 break
@@ -127,27 +135,47 b' def tokenize(program, start, end):'
127 135 pos += 1
128 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 151 parsed = []
152 sepchars = '{' + quote
132 153 pos = start
133 154 p = parser.parser(elements)
134 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 158 if n < 0:
137 159 parsed.append(('string', tmpl[pos:stop]))
138 160 pos = stop
139 161 break
162 c = tmpl[n]
140 163 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
141 164 if bs % 2 == 1:
142 165 # escaped (e.g. '\{', '\\\{', but not '\\{')
143 parsed.append(('string', (tmpl[pos:n - 1] + "{")))
166 parsed.append(('string', (tmpl[pos:n - 1] + c)))
144 167 pos = n + 1
145 168 continue
146 169 if n > pos:
147 170 parsed.append(('string', tmpl[pos:n]))
171 if c == quote:
172 return parsed, n + 1
148 173
149 174 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop))
150 175 parsed.append(parseres)
176
177 if quote:
178 raise error.ParseError(_("unterminated string"), start)
151 179 return parsed, pos
152 180
153 181 def compiletemplate(tmpl, context):
@@ -182,7 +210,7 b' def getfilter(exp, context):'
182 210
183 211 def gettemplate(exp, context):
184 212 if exp[0] == 'template':
185 return compiletemplate(exp[1], context)
213 return [compileexp(e, context, methods) for e in exp[1]]
186 214 if exp[0] == 'symbol':
187 215 # unlike runsymbol(), here 'symbol' is always taken as template name
188 216 # even if it exists in mapping. this allows us to override mapping
@@ -215,7 +243,7 b' def runsymbol(context, mapping, key):'
215 243 return v
216 244
217 245 def buildtemplate(exp, context):
218 ctmpl = compiletemplate(exp[1], context)
246 ctmpl = [compileexp(e, context, methods) for e in exp[1]]
219 247 if len(ctmpl) == 1:
220 248 return ctmpl[0] # fast path for string with no template fragment
221 249 return (runtemplate, ctmpl)
@@ -2541,6 +2541,16 b' Behind the scenes, this will throw Value'
2541 2541 abort: template filter 'datefilter' is not compatible with keyword 'author'
2542 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 2554 Thrown an error if a template function doesn't exist
2545 2555
2546 2556 $ hg tip --template '{foo()}\n'
@@ -2952,7 +2962,7 b' escaped single quotes and errors:'
2952 2962 $ hg log -r 2 -T "{if(rev, '{if(rev, r\'foo\')}')}"'\n'
2953 2963 foo
2954 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 2966 [255]
2957 2967 $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
2958 2968 hg: parse error at 11: syntax error
@@ -3069,6 +3079,14 b' Test string escaping in nested expressio'
3069 3079 3:\x6eo user, \x6eo domai\x6e
3070 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 3090 Test recursive evaluation:
3073 3091
3074 3092 $ hg init r
@@ -26,6 +26,7 b" testmod('mercurial.revset')"
26 26 testmod('mercurial.store')
27 27 testmod('mercurial.subrepo')
28 28 testmod('mercurial.templatefilters')
29 testmod('mercurial.templater')
29 30 testmod('mercurial.ui')
30 31 testmod('mercurial.url')
31 32 testmod('mercurial.util')
General Comments 0
You need to be logged in to leave comments. Login now