2929import os
3030import re
3131
32+ try :
33+ from sys import implementation
34+
35+ if implementation .name == "circuitpython" and implementation .version < (9 , 0 , 0 ):
36+ print (
37+ "Warning: adafruit_templateengine requires CircuitPython 9.0.0, as previous versions"
38+ " will have limited functionality when using block comments and non-ASCII characters."
39+ )
40+ finally :
41+ # Unimport sys to prevent accidental use
42+ del implementation
43+
3244
3345class Language : # pylint: disable=too-few-public-methods
3446 """
@@ -59,12 +71,12 @@ def safe_html(value: Any) -> str:
5971 # 1e−10
6072 """
6173
62- def replace_amp_or_semi (match : re .Match ):
74+ def _replace_amp_or_semi (match : re .Match ):
6375 return "&" if match .group (0 ) == "&" else ";"
6476
6577 return (
6678 # Replace initial & and ; together
67- re .sub (r"&|;" , replace_amp_or_semi , str (value ))
79+ re .sub (r"&|;" , _replace_amp_or_semi , str (value ))
6880 # Replace other characters
6981 .replace ('"' , """ )
7082 .replace ("_" , "_" )
@@ -152,47 +164,48 @@ def safe_markdown(value: Any) -> str:
152164 )
153165
154166
155- _PRECOMPILED_EXTENDS_PATTERN = re .compile (r"{% extends '.+?' %}|{% extends \".+?\" %}" )
156- _PRECOMPILED_BLOCK_PATTERN = re .compile (r"{% block \w+? %}" )
157- _PRECOMPILED_INCLUDE_PATTERN = re .compile (r"{% include '.+?' %}|{% include \".+?\" %}" )
158- _PRECOMPILED_HASH_COMMENT_PATTERN = re .compile (r"{# .+? #}" )
159- _PRECOMPILED_BLOCK_COMMENT_PATTERN = re .compile (
167+ _EXTENDS_PATTERN = re .compile (r"{% extends '.+?' %}|{% extends \".+?\" %}" )
168+ _BLOCK_PATTERN = re .compile (r"{% block \w+? %}" )
169+ _INCLUDE_PATTERN = re .compile (r"{% include '.+?' %}|{% include \".+?\" %}" )
170+ _HASH_COMMENT_PATTERN = re .compile (r"{# .+? #}" )
171+ _BLOCK_COMMENT_PATTERN = re .compile (
160172 r"{% comment ('.*?' |\".*?\" )?%}[\s\S]*?{% endcomment %}"
161173)
162- _PRECOMPILED_TOKEN_PATTERN = re .compile (r"{{ .+? }}|{% .+? %}" )
174+ _TOKEN_PATTERN = re .compile (r"{{ .+? }}|{% .+? %}" )
175+ _LSTRIP_BLOCK_PATTERN = re .compile (r"\n( )+$" )
163176
164177
165- def _find_next_extends (template : str ):
166- return _PRECOMPILED_EXTENDS_PATTERN .search (template )
178+ def _find_extends (template : str ):
179+ return _EXTENDS_PATTERN .search (template )
167180
168181
169- def _find_next_block (template : str ):
170- return _PRECOMPILED_BLOCK_PATTERN .search (template )
182+ def _find_block (template : str ):
183+ return _BLOCK_PATTERN .search (template )
171184
172185
173- def _find_next_include (template : str ):
174- return _PRECOMPILED_INCLUDE_PATTERN .search (template )
186+ def _find_include (template : str ):
187+ return _INCLUDE_PATTERN .search (template )
175188
176189
177190def _find_named_endblock (template : str , name : str ):
178191 return re .search (r"{% endblock " + name + r" %}" , template )
179192
180193
181- def _exists_and_is_file (path : str ):
194+ def _exists_and_is_file (path : str ) -> bool :
182195 try :
183196 return (os .stat (path )[0 ] & 0b_11110000_00000000 ) == 0b_10000000_00000000
184197 except OSError :
185198 return False
186199
187200
188201def _resolve_includes (template : str ):
189- while (include_match := _find_next_include (template )) is not None :
202+ while (include_match := _find_include (template )) is not None :
190203 template_path = include_match .group (0 )[12 :- 4 ]
191204
192205 # TODO: Restrict include to specific directory
193206
194207 if not _exists_and_is_file (template_path ):
195- raise FileNotFoundError (f"Include template not found: { template_path } " )
208+ raise OSError (f"Include template not found: { template_path } " )
196209
197210 # Replace the include with the template content
198211 with open (template_path , "rt" , encoding = "utf-8" ) as template_file :
@@ -205,15 +218,15 @@ def _resolve_includes(template: str):
205218
206219
207220def _check_for_unsupported_nested_blocks (template : str ):
208- if _find_next_block (template ) is not None :
221+ if _find_block (template ) is not None :
209222 raise ValueError ("Nested blocks are not supported" )
210223
211224
212225def _resolve_includes_blocks_and_extends (template : str ):
213226 block_replacements : "dict[str, str]" = {}
214227
215228 # Processing nested child templates
216- while (extends_match := _find_next_extends (template )) is not None :
229+ while (extends_match := _find_extends (template )) is not None :
217230 extended_template_name = extends_match .group (0 )[12 :- 4 ]
218231
219232 # Load extended template
@@ -229,20 +242,15 @@ def _resolve_includes_blocks_and_extends(template: str):
229242 template = _resolve_includes (template )
230243
231244 # Save block replacements
232- while (block_match := _find_next_block (template )) is not None :
245+ while (block_match := _find_block (template )) is not None :
233246 block_name = block_match .group (0 )[9 :- 3 ]
234247
235248 endblock_match = _find_named_endblock (template , block_name )
236249
237250 if endblock_match is None :
238251 raise ValueError (r"Missing {% endblock %} for block: " + block_name )
239252
240- # Workaround for bug in re module https://github.com/adafruit/circuitpython/issues/6860
241- block_content = template .encode ("utf-8" )[
242- block_match .end () : endblock_match .start ()
243- ].decode ("utf-8" )
244- # TODO: Uncomment when bug is fixed
245- # block_content = template[block_match.end() : endblock_match.start()]
253+ block_content = template [block_match .end () : endblock_match .start ()]
246254
247255 _check_for_unsupported_nested_blocks (block_content )
248256
@@ -267,7 +275,7 @@ def _resolve_includes_blocks_and_extends(template: str):
267275
268276def _replace_blocks_with_replacements (template : str , replacements : "dict[str, str]" ):
269277 # Replace blocks in top-level template
270- while (block_match := _find_next_block (template )) is not None :
278+ while (block_match := _find_block (template )) is not None :
271279 block_name = block_match .group (0 )[9 :- 3 ]
272280
273281 # Self-closing block tag without default content
@@ -309,34 +317,61 @@ def _replace_blocks_with_replacements(template: str, replacements: "dict[str, st
309317 return template
310318
311319
312- def _find_next_hash_comment (template : str ):
313- return _PRECOMPILED_HASH_COMMENT_PATTERN .search (template )
320+ def _find_hash_comment (template : str ):
321+ return _HASH_COMMENT_PATTERN .search (template )
322+
323+
324+ def _find_block_comment (template : str ):
325+ return _BLOCK_COMMENT_PATTERN .search (template )
326+
327+
328+ def _remove_comments (
329+ template : str ,
330+ * ,
331+ trim_blocks : bool = True ,
332+ lstrip_blocks : bool = True ,
333+ ):
334+ def _remove_matched_comment (template : str , comment_match : re .Match ):
335+ text_before_comment = template [: comment_match .start ()]
336+ text_after_comment = template [comment_match .end () :]
314337
338+ if text_before_comment :
339+ if lstrip_blocks :
340+ if _token_is_on_own_line (text_before_comment ):
341+ text_before_comment = text_before_comment .rstrip (" " )
315342
316- def _find_next_block_comment (template : str ):
317- return _PRECOMPILED_BLOCK_COMMENT_PATTERN .search (template )
343+ if text_after_comment :
344+ if trim_blocks :
345+ if text_after_comment .startswith ("\n " ):
346+ text_after_comment = text_after_comment [1 :]
318347
348+ return text_before_comment + text_after_comment
319349
320- def _remove_comments (template : str ):
321350 # Remove hash comments: {# ... #}
322- while (comment_match := _find_next_hash_comment (template )) is not None :
323- template = template [: comment_match . start ()] + template [ comment_match . end () :]
351+ while (comment_match := _find_hash_comment (template )) is not None :
352+ template = _remove_matched_comment ( template , comment_match )
324353
325354 # Remove block comments: {% comment %} ... {% endcomment %}
326- while (comment_match := _find_next_block_comment (template )) is not None :
327- template = template [: comment_match . start ()] + template [ comment_match . end () :]
355+ while (comment_match := _find_block_comment (template )) is not None :
356+ template = _remove_matched_comment ( template , comment_match )
328357
329358 return template
330359
331360
332- def _find_next_token (template : str ):
333- return _PRECOMPILED_TOKEN_PATTERN .search (template )
361+ def _find_token (template : str ):
362+ return _TOKEN_PATTERN .search (template )
363+
364+
365+ def _token_is_on_own_line (text_before_token : str ) -> bool :
366+ return _LSTRIP_BLOCK_PATTERN .search (text_before_token ) is not None
334367
335368
336369def _create_template_function ( # pylint: disable=,too-many-locals,too-many-branches,too-many-statements
337370 template : str ,
338371 language : str = Language .HTML ,
339372 * ,
373+ trim_blocks : bool = True ,
374+ lstrip_blocks : bool = True ,
340375 function_name : str = "_" ,
341376 context_name : str = "context" ,
342377 dry_run : bool = False ,
@@ -351,22 +386,34 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
351386 function_string = f"def { function_name } ({ context_name } ):\n "
352387 indent , indentation_level = " " , 1
353388
354- # Keep track of the tempalte state
389+ # Keep track of the template state
355390 forloop_iterables : "list[str]" = []
356391 autoescape_modes : "list[bool]" = ["default_on" ]
392+ last_token_was_block = False
357393
358394 # Resolve tokens
359- while (token_match := _find_next_token (template )) is not None :
395+ while (token_match := _find_token (template )) is not None :
360396 token = token_match .group (0 )
361397
362398 # Add the text before the token
363399 if text_before_token := template [: token_match .start ()]:
364- function_string += (
365- indent * indentation_level + f"yield { repr (text_before_token )} \n "
366- )
400+ if lstrip_blocks and token .startswith (r"{% " ):
401+ if _token_is_on_own_line (text_before_token ):
402+ text_before_token = text_before_token .rstrip (" " )
403+
404+ if trim_blocks :
405+ if last_token_was_block and text_before_token .startswith ("\n " ):
406+ text_before_token = text_before_token [1 :]
407+
408+ if text_before_token :
409+ function_string += (
410+ indent * indentation_level + f"yield { repr (text_before_token )} \n "
411+ )
367412
368413 # Token is an expression
369414 if token .startswith (r"{{ " ):
415+ last_token_was_block = False
416+
370417 autoescape = autoescape_modes [- 1 ] in ("on" , "default_on" )
371418
372419 # Expression should be escaped with language-specific function
@@ -383,6 +430,8 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
383430
384431 # Token is a statement
385432 elif token .startswith (r"{% " ):
433+ last_token_was_block = True
434+
386435 # Token is a some sort of if statement
387436 if token .startswith (r"{% if " ):
388437 function_string += indent * indentation_level + f"{ token [3 :- 3 ]} :\n "
@@ -449,9 +498,16 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
449498 # Continue with the rest of the template
450499 template = template [token_match .end () :]
451500
452- # Add the text after the last token (if any) and return
453- if template :
454- function_string += indent * indentation_level + f"yield { repr (template )} \n "
501+ # Add the text after the last token (if any)
502+ text_after_last_token = template
503+
504+ if text_after_last_token :
505+ if trim_blocks and text_after_last_token .startswith ("\n " ):
506+ text_after_last_token = text_after_last_token [1 :]
507+
508+ function_string += (
509+ indent * indentation_level + f"yield { repr (text_after_last_token )} \n "
510+ )
455511
456512 # If dry run, return the template function string
457513 if dry_run :
0 commit comments