Source code for httk.web.templating.jinja2_engine

from pathlib import Path

from jinja2 import Environment, FileSystemLoader
from markupsafe import Markup

from .base import TemplateRenderInput

[docs] TEMPLATE_SUFFIXES: tuple[str, ...] = ( ".html.j2", ".jinja", ".j2", ".html", ".httkweb.html", )
[docs] class JinjaTemplateEngine: def __init__( self, template_dir: Path, *, template_suffixes: tuple[str, ...] = TEMPLATE_SUFFIXES, ) -> None:
[docs] self.template_dir = template_dir
[docs] self.template_suffixes = template_suffixes
self._environment = Environment( loader=FileSystemLoader(str(template_dir)), autoescape=True, trim_blocks=True, lstrip_blocks=True, )
[docs] def render(self, render_input: TemplateRenderInput) -> str: template_key = self._resolve_template(render_input.template_name) base_key = self._resolve_template(render_input.base_template_name) context = dict(render_input.context) content = render_input.content_html if template_key is not None: template = self._environment.get_template(template_key) context["content"] = Markup(content) content = template.render(**context) if base_key is not None: base_template = self._environment.get_template(base_key) context["content"] = Markup(content) content = base_template.render(**context) return content
[docs] def render_fragment(self, *, template_name: str, context: dict[str, object]) -> str | None: template_key = self._resolve_fragment_template(template_name) if template_key is None: return None template = self._environment.get_template(template_key) return template.render(**context)
def _resolve_template(self, name: str | None) -> str | None: if name is None: return None candidate = name.strip() if not candidate: return None path_candidate = Path(candidate) if not self._is_safe_relative_path(path_candidate): return None direct = self.template_dir / candidate if direct.exists() and direct.is_file(): return candidate if path_candidate.suffix: return None for suffix in self.template_suffixes: with_suffix = self.template_dir / f"{candidate}{suffix}" if with_suffix.exists() and with_suffix.is_file(): return f"{candidate}{suffix}" return None def _is_safe_relative_path(self, path_candidate: Path) -> bool: if path_candidate.is_absolute(): return False base_dir = self.template_dir.resolve(strict=False) resolved_candidate = (self.template_dir / path_candidate).resolve(strict=False) try: resolved_candidate.relative_to(base_dir) except ValueError: return False return True def _resolve_fragment_template(self, name: str) -> str | None: """ Resolve function-fragment templates with extra legacy compatibility. In old httkweb metadata, function templates often used bare names (e.g. `hello_world_result`) and were resolved against the active template engine extension. Here we keep that behavior by probing a few additional suffix variants. """ template_key = self._resolve_template(name) if template_key is not None: return template_key candidate = name.strip() if not candidate: return None path_candidate = Path(candidate) if not self._is_safe_relative_path(path_candidate): return None # If metadata already carries an HTML-like suffix (e.g. "fragment.html"), # also probe common Jinja variants that may be present in v2 projects. # This mirrors old usage where function metadata often referenced ".html" # while active template files could be ".html.j2" / ".jinja" / ".j2". if candidate.endswith(".httkweb.html"): stem = candidate[: -len(".httkweb.html")] probes = [f"{candidate}.j2", f"{candidate}.jinja", f"{stem}.html.j2", f"{stem}.jinja", f"{stem}.j2"] for probe in probes: template_key = self._resolve_template(probe) if template_key is not None: return template_key return None if candidate.endswith(".html"): stem = candidate[: -len(".html")] probes = [f"{candidate}.j2", f"{candidate}.jinja", f"{stem}.html.j2", f"{stem}.jinja", f"{stem}.j2"] for probe in probes: template_key = self._resolve_template(probe) if template_key is not None: return template_key return None # For bare names, probe the same suffix families as standard template # resolution plus legacy html-style names used by old function metadata. if not path_candidate.suffix: for suffix in self.template_suffixes: template_key = self._resolve_template(f"{candidate}{suffix}") if template_key is not None: return template_key return None