Coverage for app/backend/src/couchers/templating.py: 100%

31 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 09:29 +0000

1""" 

2Provides string templating functionality using jinja2. 

3""" 

4 

5from dataclasses import dataclass 

6from functools import cache 

7from html import escape 

8from typing import Any 

9 

10from jinja2 import Environment, pass_context 

11from jinja2.runtime import Context as JinjaContext 

12from markupsafe import Markup 

13 

14_OUTPUT_HTML_CONTEXT_KEY = "_output_html" 

15 

16 

17@dataclass(frozen=True, slots=True, kw_only=True) 

18class Jinja2Template: 

19 """A jinja2 template string, optionally producing HTML.""" 

20 

21 source: str 

22 """The jinja2 template source code.""" 

23 

24 html: bool 

25 """If true, the template will be treated as HTML, so placeholders will be escaped by default.""" 

26 

27 def render(self, args: dict[str, Any]) -> str: 

28 args = {**args, _OUTPUT_HTML_CONTEXT_KEY: self.html} 

29 return _get_jinja_env().from_string(self.source).render(args) 

30 

31 

32@cache 

33def _get_jinja_env() -> Environment: 

34 env = Environment(trim_blocks=True) 

35 env.autoescape = False # We do escaping in _finalize 

36 env.finalize = _finalize 

37 return env 

38 

39 

40@pass_context 

41def _finalize(jinja_context: JinjaContext, value: Any) -> str: 

42 """ 

43 Converts a value into a string for interpolation into the template, 

44 ensuring that only safe markup is preserved if the output is html. 

45 """ 

46 

47 output_html: bool = jinja_context[_OUTPUT_HTML_CONTEXT_KEY] 

48 match value: 

49 case Markup(): 

50 return str(value) 

51 case _: 

52 if output_html: 

53 # Plaintext rendered in HTML context: escape markup and preserve newlines. 

54 return escape(str(value)).replace("\n", "<br>") 

55 else: 

56 return str(value)