@@ -68,6 +68,71 @@ def __init__(self, template: str, start_position: int, end_position: int):
68
68
self .content = template [start_position :end_position ]
69
69
70
70
71
+ class TemplateSyntaxError (SyntaxError ):
72
+ """Raised when a syntax error is encountered in a template."""
73
+
74
+ def __init__ (self , message : str , token : Token ):
75
+ super ().__init__ (f"{ message } \n \n " + self ._underline_token_in_template (token ))
76
+
77
+ @staticmethod
78
+ def _underline_token_in_template (
79
+ token : Token , * , lines_around : int = 4 , symbol : str = "^"
80
+ ) -> str :
81
+ """
82
+ Return ``number_of_lines`` lines before and after the token, with the token content underlined
83
+ with ``symbol`` e.g.:
84
+
85
+ ```html
86
+ [8 lines skipped]
87
+ Shopping list:
88
+ <ul>
89
+ {% for item in context["items"] %}
90
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
91
+ <li>{{ item["name"] }} - ${{ item["price"] }}</li>
92
+ {% empty %}
93
+ [5 lines skipped]
94
+ ```
95
+ """
96
+
97
+ template_before_token = token .template [: token .start_position ]
98
+ if (skipped_lines := template_before_token .count ("\n " ) - lines_around ) > 0 :
99
+ template_before_token = f"[{ skipped_lines } lines skipped]\n " + "\n " .join (
100
+ template_before_token .split ("\n " )[- (lines_around + 1 ) :]
101
+ )
102
+
103
+ template_after_token = token .template [token .end_position :]
104
+ if (skipped_lines := template_after_token .count ("\n " ) - lines_around ) > 0 :
105
+ template_after_token = (
106
+ "\n " .join (template_after_token .split ("\n " )[: (lines_around + 1 )])
107
+ + f"\n [{ skipped_lines } lines skipped]"
108
+ )
109
+
110
+ lines_before_line_with_token = template_before_token .rsplit ("\n " , 1 )[0 ]
111
+
112
+ line_with_token = (
113
+ template_before_token .rsplit ("\n " , 1 )[- 1 ]
114
+ + token .content
115
+ + template_after_token .split ("\n " )[0 ]
116
+ )
117
+
118
+ line_with_underline = (
119
+ " " * len (template_before_token .rsplit ("\n " , 1 )[- 1 ])
120
+ + symbol * len (token .content )
121
+ + " " * len (template_after_token .split ("\n " )[0 ])
122
+ )
123
+
124
+ lines_after_line_with_token = template_after_token .split ("\n " , 1 )[- 1 ]
125
+
126
+ return "\n " .join (
127
+ [
128
+ lines_before_line_with_token ,
129
+ line_with_token ,
130
+ line_with_underline ,
131
+ lines_after_line_with_token ,
132
+ ]
133
+ )
134
+
135
+
71
136
def safe_html (value : Any ) -> str :
72
137
"""
73
138
Encodes unsafe symbols in ``value`` to HTML entities and returns the string that can be safely
@@ -202,64 +267,6 @@ def _find_named_endblock(template: str, name: str):
202
267
return re .search (r"{% endblock " + name + r" %}" , template )
203
268
204
269
205
- def _underline_token_in_template (
206
- token : Token , * , lines_around : int = 5 , symbol : str = "^"
207
- ) -> str :
208
- """
209
- Return ``number_of_lines`` lines before and after the token, with the token content underlined
210
- with ``symbol`` e.g.:
211
-
212
- ```html
213
- [8 lines skipped]
214
- Shopping list:
215
- <ul>
216
- {% for item in context["items"] %}
217
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
218
- <li>{{ item["name"] }} - ${{ item["price"] }}</li>
219
- {% empty %}
220
- [5 lines skipped]
221
- ```
222
- """
223
-
224
- template_before_token = token .template [: token .start_position ]
225
- if (skipped_lines := template_before_token .count ("\n " ) - lines_around ) > 0 :
226
- template_before_token = f"[{ skipped_lines } lines skipped]\n " + "\n " .join (
227
- template_before_token .split ("\n " )[- (lines_around + 1 ) :]
228
- )
229
-
230
- template_after_token = token .template [token .end_position :]
231
- if (skipped_lines := template_after_token .count ("\n " ) - lines_around ) > 0 :
232
- template_after_token = (
233
- "\n " .join (template_after_token .split ("\n " )[: (lines_around + 1 )])
234
- + f"\n [{ skipped_lines } lines skipped]"
235
- )
236
-
237
- lines_before_line_with_token = template_before_token .rsplit ("\n " , 1 )[0 ]
238
-
239
- line_with_token = (
240
- template_before_token .rsplit ("\n " , 1 )[- 1 ]
241
- + token .content
242
- + template_after_token .split ("\n " )[0 ]
243
- )
244
-
245
- line_with_underline = (
246
- " " * len (template_before_token .rsplit ("\n " , 1 )[- 1 ])
247
- + symbol * len (token .content )
248
- + " " * len (template_after_token .split ("\n " )[0 ])
249
- )
250
-
251
- lines_after_line_with_token = template_after_token .split ("\n " , 1 )[- 1 ]
252
-
253
- return "\n " .join (
254
- [
255
- lines_before_line_with_token ,
256
- line_with_token ,
257
- line_with_underline ,
258
- lines_after_line_with_token ,
259
- ]
260
- )
261
-
262
-
263
270
def _exists_and_is_file (path : str ) -> bool :
264
271
try :
265
272
return (os .stat (path )[0 ] & 0b_11110000_00000000 ) == 0b_10000000_00000000
@@ -274,12 +281,7 @@ def _resolve_includes(template: str):
274
281
# TODO: Restrict include to specific directory
275
282
276
283
if not _exists_and_is_file (template_path ):
277
- raise OSError (
278
- f"Include template not found: { template_path } \n \n "
279
- + _underline_token_in_template (
280
- Token (template , include_match .start (), include_match .end ())
281
- )
282
- )
284
+ raise OSError (f"Template file not found: { template_path } " )
283
285
284
286
# Replace the include with the template content
285
287
with open (template_path , "rt" , encoding = "utf-8" ) as template_file :
@@ -316,15 +318,13 @@ def _resolve_includes_blocks_and_extends(template: str):
316
318
endblock_match = _find_named_endblock (template [offset :], block_name )
317
319
318
320
if endblock_match is None :
319
- raise SyntaxError (
320
- "Missing {% endblock %}:\n \n "
321
- + _underline_token_in_template (
322
- Token (
323
- template ,
324
- offset + block_match .start (),
325
- offset + block_match .end (),
326
- )
327
- )
321
+ raise TemplateSyntaxError (
322
+ "Missing {% endblock %}" ,
323
+ Token (
324
+ template ,
325
+ offset + block_match .start (),
326
+ offset + block_match .end (),
327
+ ),
328
328
)
329
329
330
330
block_content = template [
@@ -333,15 +333,13 @@ def _resolve_includes_blocks_and_extends(template: str):
333
333
334
334
# Check for unsupported nested blocks
335
335
if (nested_block_match := _find_block (block_content )) is not None :
336
- raise SyntaxError (
337
- "Nested blocks are not supported:\n \n "
338
- + _underline_token_in_template (
339
- Token (
340
- template ,
341
- offset + block_match .end () + nested_block_match .start (),
342
- offset + block_match .end () + nested_block_match .end (),
343
- )
344
- )
336
+ raise TemplateSyntaxError (
337
+ "Nested blocks are not supported" ,
338
+ Token (
339
+ template ,
340
+ offset + block_match .end () + nested_block_match .start (),
341
+ offset + block_match .end () + nested_block_match .end (),
342
+ ),
345
343
)
346
344
347
345
if block_name in block_replacements :
@@ -382,15 +380,13 @@ def _replace_blocks_with_replacements(template: str, replacements: "dict[str, st
382
380
383
381
# Check for unsupported nested blocks
384
382
if (nested_block_match := _find_block (block_content )) is not None :
385
- raise SyntaxError (
386
- "Nested blocks are not supported:\n \n "
387
- + _underline_token_in_template (
388
- Token (
389
- template ,
390
- block_match .end () + nested_block_match .start (),
391
- block_match .end () + nested_block_match .end (),
392
- )
393
- )
383
+ raise TemplateSyntaxError (
384
+ "Nested blocks are not supported" ,
385
+ Token (
386
+ template ,
387
+ block_match .end () + nested_block_match .start (),
388
+ block_match .end () + nested_block_match .end (),
389
+ ),
394
390
)
395
391
396
392
# No replacement for this block, use default content
@@ -552,23 +548,26 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too
552
548
553
549
nested_if_statements .append (token )
554
550
elif token .content .startswith (r"{% elif " ):
551
+ if not nested_if_statements :
552
+ raise TemplateSyntaxError ("Missing {% if ... %}" , token )
553
+
555
554
indentation_level -= 1
556
555
function_string += (
557
556
indent * indentation_level + f"{ token .content [3 :- 3 ]} :\n "
558
557
)
559
558
indentation_level += 1
560
559
elif token .content == r"{% else %}" :
560
+ if not nested_if_statements :
561
+ raise TemplateSyntaxError ("Missing {% if ... %}" , token )
562
+
561
563
indentation_level -= 1
562
564
function_string += indent * indentation_level + "else:\n "
563
565
indentation_level += 1
564
566
elif token .content == r"{% endif %}" :
565
- indentation_level -= 1
566
-
567
567
if not nested_if_statements :
568
- raise SyntaxError (
569
- "Missing {% if ... %}\n \n " + _underline_token_in_template (token )
570
- )
568
+ raise TemplateSyntaxError ("Missing {% if ... %}" , token )
571
569
570
+ indentation_level -= 1
572
571
nested_if_statements .pop ()
573
572
574
573
# Token is a for loop
@@ -580,24 +579,22 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too
580
579
581
580
nested_for_loops .append (token )
582
581
elif token .content == r"{% empty %}" :
582
+ if not nested_for_loops :
583
+ raise TemplateSyntaxError ("Missing {% for ... %}" , token )
584
+
583
585
indentation_level -= 1
584
586
last_forloop_iterable = (
585
587
nested_for_loops [- 1 ].content [3 :- 3 ].split (" in " , 1 )[1 ]
586
588
)
587
-
588
589
function_string += (
589
590
indent * indentation_level + f"if not { last_forloop_iterable } :\n "
590
591
)
591
592
indentation_level += 1
592
593
elif token .content == r"{% endfor %}" :
593
- indentation_level -= 1
594
-
595
594
if not nested_for_loops :
596
- raise SyntaxError (
597
- "Missing {% for ... %}\n \n "
598
- + _underline_token_in_template (token )
599
- )
595
+ raise TemplateSyntaxError ("Missing {% for ... %}" , token )
600
596
597
+ indentation_level -= 1
601
598
nested_for_loops .pop ()
602
599
603
600
# Token is a while loop
@@ -609,14 +606,10 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too
609
606
610
607
nested_while_loops .append (token )
611
608
elif token .content == r"{% endwhile %}" :
612
- indentation_level -= 1
613
-
614
609
if not nested_while_loops :
615
- raise SyntaxError (
616
- "Missing {% while ... %}\n \n "
617
- + _underline_token_in_template (token )
618
- )
610
+ raise TemplateSyntaxError ("Missing {% while ... %}" , token )
619
611
612
+ indentation_level -= 1
620
613
nested_while_loops .pop ()
621
614
622
615
# Token is a Python code
@@ -634,46 +627,31 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too
634
627
635
628
elif token .content == r"{% endautoescape %}" :
636
629
if not nested_autoescape_modes :
637
- raise SyntaxError (
638
- "Missing {% autoescape ... %}\n \n "
639
- + _underline_token_in_template (token )
640
- )
630
+ raise TemplateSyntaxError ("Missing {% autoescape ... %}" , token )
641
631
642
632
nested_autoescape_modes .pop ()
643
633
644
634
else :
645
- raise SyntaxError (
646
- f"Unknown token type: { token .content } \n \n "
647
- + _underline_token_in_template (token )
648
- )
635
+ raise TemplateSyntaxError (f"Unknown token type: { token .content } " , token )
649
636
650
637
else :
651
- raise SyntaxError (
652
- f"Unknown token type: { token .content } \n \n "
653
- + _underline_token_in_template (token )
654
- )
638
+ raise TemplateSyntaxError (f"Unknown token type: { token .content } " , token )
655
639
656
640
# Move offset to the end of the token
657
641
offset += token_match .end ()
658
642
659
643
# Checking for unclosed blocks
660
644
if len (nested_if_statements ) > 0 :
661
645
last_if_statement = nested_if_statements [- 1 ]
662
- raise SyntaxError (
663
- "Missing {% endif %}\n \n " + _underline_token_in_template (last_if_statement )
664
- )
646
+ raise TemplateSyntaxError ("Missing {% endif %}" , last_if_statement )
665
647
666
648
if len (nested_for_loops ) > 0 :
667
649
last_for_loop = nested_for_loops [- 1 ]
668
- raise SyntaxError (
669
- "Missing {% endfor %}\n \n " + _underline_token_in_template (last_for_loop )
670
- )
650
+ raise TemplateSyntaxError ("Missing {% endfor %}" , last_for_loop )
671
651
672
652
if len (nested_while_loops ) > 0 :
673
653
last_while_loop = nested_while_loops [- 1 ]
674
- raise SyntaxError (
675
- "Missing {% endwhile %}\n \n " + _underline_token_in_template (last_while_loop )
676
- )
654
+ raise TemplateSyntaxError ("Missing {% endwhile %}" , last_while_loop )
677
655
678
656
# No check for unclosed autoescape blocks, as they are optional and do not result in errors
679
657
0 commit comments