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
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
1"""
2Provides string templating functionality using jinja2.
3"""
5from dataclasses import dataclass
6from functools import cache
7from html import escape
8from typing import Any
10from jinja2 import Environment, pass_context
11from jinja2.runtime import Context as JinjaContext
12from markupsafe import Markup
14_OUTPUT_HTML_CONTEXT_KEY = "_output_html"
17@dataclass(frozen=True, slots=True, kw_only=True)
18class Jinja2Template:
19 """A jinja2 template string, optionally producing HTML."""
21 source: str
22 """The jinja2 template source code."""
24 html: bool
25 """If true, the template will be treated as HTML, so placeholders will be escaped by default."""
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)
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
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 """
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)