@@ -219,7 +219,7 @@ def _resolve_includes(template: str):
219
219
220
220
def _check_for_unsupported_nested_blocks (template : str ):
221
221
if _find_block (template ) is not None :
222
- raise ValueError ("Nested blocks are not supported" )
222
+ raise SyntaxError ("Nested blocks are not supported" )
223
223
224
224
225
225
def _resolve_includes_blocks_and_extends (template : str ):
@@ -248,7 +248,7 @@ def _resolve_includes_blocks_and_extends(template: str):
248
248
endblock_match = _find_named_endblock (template , block_name )
249
249
250
250
if endblock_match is None :
251
- raise ValueError ( r "Missing {% endblock %} for block: " + block_name )
251
+ raise SyntaxError ( "Missing {% endblock %} for block: " + block_name )
252
252
253
253
block_content = template [block_match .end () : endblock_match .start ()]
254
254
@@ -366,13 +366,13 @@ def _token_is_on_own_line(text_before_token: str) -> bool:
366
366
return _LSTRIP_BLOCK_PATTERN .search (text_before_token ) is not None
367
367
368
368
369
- def _create_template_function ( # pylint: disable=,too-many-locals,too-many-branches,too-many-statements
369
+ def _create_template_rendering_function ( # pylint: disable=,too-many-locals,too-many-branches,too-many-statements
370
370
template : str ,
371
371
language : str = Language .HTML ,
372
372
* ,
373
373
trim_blocks : bool = True ,
374
374
lstrip_blocks : bool = True ,
375
- function_name : str = "_ " ,
375
+ function_name : str = "__template_rendering_function " ,
376
376
context_name : str = "context" ,
377
377
dry_run : bool = False ,
378
378
) -> "Generator[str] | str" :
@@ -387,8 +387,10 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
387
387
indent , indentation_level = " " , 1
388
388
389
389
# Keep track of the template state
390
- forloop_iterables : "list[str]" = []
391
- autoescape_modes : "list[bool]" = ["default_on" ]
390
+ nested_if_statements : "list[str]" = []
391
+ nested_for_loops : "list[str]" = []
392
+ nested_while_loops : "list[str]" = []
393
+ nested_autoescape_modes : "list[str]" = []
392
394
last_token_was_block = False
393
395
394
396
# Resolve tokens
@@ -405,16 +407,21 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
405
407
if last_token_was_block and text_before_token .startswith ("\n " ):
406
408
text_before_token = text_before_token [1 :]
407
409
408
- if text_before_token :
409
- function_string += (
410
- indent * indentation_level + f"yield { repr (text_before_token )} \n "
411
- )
410
+ if text_before_token :
411
+ function_string += (
412
+ indent * indentation_level + f"yield { repr (text_before_token )} \n "
413
+ )
414
+ else :
415
+ function_string += indent * indentation_level + "pass\n "
412
416
413
417
# Token is an expression
414
418
if token .startswith (r"{{ " ):
415
419
last_token_was_block = False
416
420
417
- autoescape = autoescape_modes [- 1 ] in ("on" , "default_on" )
421
+ if nested_autoescape_modes :
422
+ autoescape = nested_autoescape_modes [- 1 ][14 :- 3 ] == "on"
423
+ else :
424
+ autoescape = True
418
425
419
426
# Expression should be escaped with language-specific function
420
427
if autoescape :
@@ -436,6 +443,8 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
436
443
if token .startswith (r"{% if " ):
437
444
function_string += indent * indentation_level + f"{ token [3 :- 3 ]} :\n "
438
445
indentation_level += 1
446
+
447
+ nested_if_statements .append (token )
439
448
elif token .startswith (r"{% elif " ):
440
449
indentation_level -= 1
441
450
function_string += indent * indentation_level + f"{ token [3 :- 3 ]} :\n "
@@ -447,30 +456,49 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
447
456
elif token == r"{% endif %}" :
448
457
indentation_level -= 1
449
458
459
+ if not nested_if_statements :
460
+ raise SyntaxError ("Missing {% if ... %} block for {% endif %}" )
461
+
462
+ nested_if_statements .pop ()
463
+
450
464
# Token is a for loop
451
465
elif token .startswith (r"{% for " ):
452
466
function_string += indent * indentation_level + f"{ token [3 :- 3 ]} :\n "
453
467
indentation_level += 1
454
468
455
- forloop_iterables .append (token [ 3 : - 3 ]. split ( " in " , 1 )[ 1 ] )
469
+ nested_for_loops .append (token )
456
470
elif token == r"{% empty %}" :
457
471
indentation_level -= 1
472
+ last_forloop_iterable = nested_for_loops [- 1 ][3 :- 3 ].split (" in " , 1 )[1 ]
458
473
459
474
function_string += (
460
- indent * indentation_level + f"if not { forloop_iterables [ - 1 ] } :\n "
475
+ indent * indentation_level + f"if not { last_forloop_iterable } :\n "
461
476
)
462
477
indentation_level += 1
463
478
elif token == r"{% endfor %}" :
464
479
indentation_level -= 1
465
- forloop_iterables .pop ()
480
+
481
+ if not nested_for_loops :
482
+ raise SyntaxError ("Missing {% for ... %} block for {% endfor %}" )
483
+
484
+ nested_for_loops .pop ()
466
485
467
486
# Token is a while loop
468
487
elif token .startswith (r"{% while " ):
469
488
function_string += indent * indentation_level + f"{ token [3 :- 3 ]} :\n "
470
489
indentation_level += 1
490
+
491
+ nested_while_loops .append (token )
471
492
elif token == r"{% endwhile %}" :
472
493
indentation_level -= 1
473
494
495
+ if not nested_while_loops :
496
+ raise SyntaxError (
497
+ "Missing {% while ... %} block for {% endwhile %}"
498
+ )
499
+
500
+ nested_while_loops .pop ()
501
+
474
502
# Token is a Python code
475
503
elif token .startswith (r"{% exec " ):
476
504
expression = token [8 :- 3 ]
@@ -481,23 +509,41 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
481
509
mode = token [14 :- 3 ]
482
510
if mode not in ("on" , "off" ):
483
511
raise ValueError (f"Unknown autoescape mode: { mode } " )
484
- autoescape_modes .append (mode )
512
+
513
+ nested_autoescape_modes .append (token )
514
+
485
515
elif token == r"{% endautoescape %}" :
486
- if autoescape_modes == ["default_on" ]:
487
- raise ValueError ("No autoescape mode to end" )
488
- autoescape_modes .pop ()
516
+ if not nested_autoescape_modes :
517
+ raise SyntaxError (
518
+ "Missing {% autoescape ... %} block for {% endautoescape %}"
519
+ )
520
+
521
+ nested_autoescape_modes .pop ()
489
522
490
523
else :
491
- raise ValueError (
492
- f"Unknown token type: { token } at { token_match .start ()} "
493
- )
524
+ raise SyntaxError (f"Unknown token type: { token } " )
494
525
495
526
else :
496
- raise ValueError (f"Unknown token type: { token } at { token_match . start () } " )
527
+ raise SyntaxError (f"Unknown token type: { token } " )
497
528
498
529
# Continue with the rest of the template
499
530
template = template [token_match .end () :]
500
531
532
+ # Checking for unclosed blocks
533
+ if len (nested_if_statements ) > 0 :
534
+ last_if_statement = nested_if_statements [- 1 ]
535
+ raise SyntaxError ("Missing {% endif %} for " + last_if_statement )
536
+
537
+ if len (nested_for_loops ) > 0 :
538
+ last_for_loop = nested_for_loops [- 1 ]
539
+ raise SyntaxError ("Missing {% endfor %} for " + last_for_loop )
540
+
541
+ if len (nested_while_loops ) > 0 :
542
+ last_while_loop = nested_while_loops [- 1 ]
543
+ raise SyntaxError ("Missing {% endwhile %} for " + last_while_loop )
544
+
545
+ # No check for unclosed autoescape blocks, as they are optional and do not result in errors
546
+
501
547
# Add the text after the last token (if any)
502
548
text_after_last_token = template
503
549
@@ -557,7 +603,9 @@ def __init__(self, template_string: str, *, language: str = Language.HTML) -> No
557
603
:param str template_string: String containing the template to be rendered
558
604
:param str language: Language for autoescaping. Defaults to HTML
559
605
"""
560
- self ._template_function = _create_template_function (template_string , language )
606
+ self ._template_function = _create_template_rendering_function (
607
+ template_string , language
608
+ )
561
609
562
610
def render_iter (
563
611
self , context : dict = None , * , chunk_size : int = None
0 commit comments