Skip to content

Commit 1d6c58e

Browse files
cocolatozzzeek
authored andcommitted
fix percent escape not working when not at the beginning of the line
Fixed parsing issue where attempting to render a single percent sign % using an escaped percent %% would not function correctly if the escaped percent were not the first character on a line. Note that this is a revised version of a similar change made in Mako 1.3.1 which caused unexpected parsing regressions, resulting in the release being yanked. Pull request courtesy Hai Zhu. Fixes: #323 Closes: #383 Pull-request: #383 Pull-request-sha: db93097 Change-Id: Ia13b652ccdb3cc51bb8a28aed329b4677d49e3ba
1 parent edf44dc commit 1d6c58e

File tree

6 files changed

+172
-6
lines changed

6 files changed

+172
-6
lines changed

doc/build/changelog.rst

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Changelog
2323
percent were not the first character on a line. Pull request courtesy Hai
2424
Zhu.
2525

26+
.. note:: Mako 1.3.1 was yanked from pypi and this change was reverted,
27+
replaced with a modified version for Mako 1.3.2.
28+
2629
.. changelog::
2730
:version: 1.3.0
2831
:released: Wed Nov 8 2023

doc/build/unreleased/323.rst

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.. change::
2+
:tags: bug, lexer
3+
:tickets: 323
4+
5+
Fixed parsing issue where attempting to render a single percent sign %
6+
using an escaped percent %% would not function correctly if the escaped
7+
percent were not the first character on a line. Note that this is a revised
8+
version of a similar change made in Mako 1.3.1 which caused unexpected
9+
parsing regressions, resulting in the release being yanked.
10+
Pull request courtesy Hai Zhu.

mako/lexer.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ def parse(self):
247247
continue
248248
if self.match_python_block():
249249
continue
250+
if self.match_percent():
251+
continue
250252
if self.match_text():
251253
continue
252254

@@ -352,14 +354,24 @@ def match_end(self):
352354
else:
353355
return True
354356

357+
def match_percent(self):
358+
match = self.match(r"(?<=^)(\s*)%%(%*)", re.M)
359+
if match:
360+
self.append_node(
361+
parsetree.Text, match.group(1) + "%" + match.group(2)
362+
)
363+
return True
364+
else:
365+
return False
366+
355367
def match_text(self):
356368
match = self.match(
357369
r"""
358370
(.*?) # anything, followed by:
359371
(
360-
(?<=\n)(?=[ \t]*(?=%|\#\#)) # an eval or line-based
361-
# comment preceded by a
362-
# consumed newline and whitespace
372+
(?<=\n)(?=[ \t]*(?=%|\#\#)) # an eval or line-based
373+
# comment, preceded by a
374+
# consumed newline and whitespace
363375
|
364376
(?=\${) # an expression
365377
|

mako/testing/helpers.py

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ def result_lines(result):
1919
]
2020

2121

22+
def result_raw_lines(result):
23+
return [x for x in re.split(r"\r?\n", result) if x.strip() != ""]
24+
25+
2226
def make_path(
2327
filespec: Union[Path, str],
2428
make_absolute: bool = True,

test/test_lexer.py

+90-3
Original file line numberDiff line numberDiff line change
@@ -200,16 +200,103 @@ def test_percent_escape(self):
200200
TemplateNode(
201201
{},
202202
[
203-
Text("""\n\n""", (1, 1)),
204-
Text("""% some whatever.\n\n""", (3, 2)),
205-
Text(" %% more some whatever\n", (5, 2)),
203+
Text("\n\n%", (1, 1)),
204+
Text(" some whatever.\n\n", (3, 3)),
205+
Text(" %", (5, 1)),
206+
Text(" more some whatever\n", (5, 7)),
206207
ControlLine("if", "if foo:", False, (6, 1)),
207208
ControlLine("if", "endif", True, (7, 1)),
208209
Text(" ", (8, 1)),
209210
],
210211
),
211212
)
212213

214+
def test_percent_escape2(self):
215+
template = """%% do something
216+
%%% do something
217+
if <some condition>:
218+
%%%% do something
219+
"""
220+
node = Lexer(template).parse()
221+
self._compare(
222+
node,
223+
TemplateNode(
224+
{},
225+
[
226+
Text("%", (1, 1)),
227+
Text(" do something\n", (1, 3)),
228+
Text("%%", (2, 1)),
229+
Text(" do something\nif <some condition>:\n", (2, 4)),
230+
Text(" %%%", (4, 1)),
231+
Text(" do something\n ", (4, 9)),
232+
],
233+
),
234+
)
235+
236+
def test_percent_escape_with_control_block(self):
237+
template = """
238+
% for i in [1, 2, 3]:
239+
%% do something ${i}
240+
% endfor
241+
"""
242+
node = Lexer(template).parse()
243+
self._compare(
244+
node,
245+
TemplateNode(
246+
{},
247+
[
248+
Text("\n", (1, 1)),
249+
ControlLine("for", "for i in [1, 2, 3]:", False, (2, 1)),
250+
Text(" %", (3, 1)),
251+
Text(" do something ", (3, 7)),
252+
Expression("i", [], (3, 21)),
253+
Text("\n", (3, 25)),
254+
ControlLine("for", "endfor", True, (4, 1)),
255+
],
256+
),
257+
)
258+
259+
def test_inline_percent(self):
260+
template = """
261+
%% foo
262+
bar %% baz
263+
"""
264+
node = Lexer(template).parse()
265+
self._compare(
266+
node,
267+
TemplateNode(
268+
{},
269+
[Text("\n%", (1, 1)), Text(" foo\nbar %% baz\n", (2, 3))],
270+
),
271+
)
272+
273+
def test_inline_percent_with_control_block(self):
274+
template = """
275+
% for i in [1, 2, 3]:
276+
%% foo
277+
bar %% baz
278+
% endfor
279+
"""
280+
node = Lexer(template).parse()
281+
self._compare(
282+
node,
283+
TemplateNode(
284+
{},
285+
[
286+
Text("\n", (1, 1)),
287+
ControlLine(
288+
"for", "for i in [1, 2, 3]:", False, (2, 1)
289+
),
290+
Text("%", (3, 1)),
291+
Text(" foo\nbar ", (3, 3)),
292+
Text("%", (3, 3)),
293+
Text("%", (3, 3)),
294+
Text(" baz\n", (4, 7)),
295+
ControlLine("for", "endfor", True, (5, 1)),
296+
],
297+
),
298+
)
299+
213300
def test_old_multiline_comment(self):
214301
template = """#*"""
215302
node = Lexer(template).parse()

test/test_template.py

+50
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from mako.testing.fixtures import TemplateTest
1616
from mako.testing.helpers import flatten_result
1717
from mako.testing.helpers import result_lines
18+
from mako.testing.helpers import result_raw_lines
1819

1920

2021
class ctx:
@@ -1667,3 +1668,52 @@ class FuturesTest(TemplateTest):
16671668
def test_future_import(self):
16681669
t = Template("${ x / y }", future_imports=["division"])
16691670
assert result_lines(t.render(x=12, y=5)) == ["2.4"]
1671+
1672+
1673+
class EscapeTest(TemplateTest):
1674+
def test_percent_escape(self):
1675+
t = Template(
1676+
"""%% do something
1677+
%%% do something
1678+
if <some condition>:
1679+
%%%% do something
1680+
"""
1681+
)
1682+
assert result_raw_lines(t.render()) == [
1683+
"% do something",
1684+
"%% do something",
1685+
"if <some condition>:",
1686+
" %%% do something",
1687+
]
1688+
1689+
def test_percent_escape2(self):
1690+
t = Template(
1691+
"""
1692+
% for i in [1, 2, 3]:
1693+
%% do something ${i}
1694+
% endfor
1695+
"""
1696+
)
1697+
assert result_raw_lines(t.render()) == [
1698+
" % do something 1",
1699+
" % do something 2",
1700+
" % do something 3",
1701+
]
1702+
1703+
def test_inline_percent(self):
1704+
t = Template(
1705+
"""
1706+
% for i in [1, 2, 3]:
1707+
%% foo
1708+
bar %% baz
1709+
% endfor
1710+
"""
1711+
)
1712+
assert result_raw_lines(t.render()) == [
1713+
"% foo",
1714+
"bar %% baz",
1715+
"% foo",
1716+
"bar %% baz",
1717+
"% foo",
1718+
"bar %% baz",
1719+
]

0 commit comments

Comments
 (0)