""" Handlebars-light Template-Renderer fuer die compliance_legal_templates. Unterstuetzte Syntax: - {{VARIABLE_NAME}} - einfache String-Substitution - {{#IF FLAG}}...{{/IF}} - bedingter Block (truthy) - {{#IF NOT FLAG}}...{{/IF}} - negierter bedingter Block Bewusst minimal gehalten — keine Loops oder Verschachtelung tiefer Logik. Komplexere Sachen werden im Context vorberechnet. """ from __future__ import annotations import re from typing import Any # Innerste {{#IF FLAG}}...{{/IF}}-Bloecke (Content enthaelt KEIN weiteres {{#IF). # Iteratives Anwenden loest Verschachtelung von innen nach aussen sauber auf. IF_INNERMOST = re.compile( r"\{\{#IF\s+(NOT\s+)?([A-Z_][A-Z0-9_]*)\}\}" r"((?:(?!\{\{#IF).)*?)" # Content: kein weiteres {{#IF r"\{\{/IF\}\}", re.DOTALL, ) VAR_PATTERN = re.compile(r"\{\{\s*([A-Z_][A-Z0-9_]*)\s*\}\}") # Fallback: orphan IF-Tags die nach Iteration uebrig sind (z.B. unbalanced template) raus. ORPHAN_IF_TAG = re.compile( r"\{\{/IF\}\}|\{\{#IF\s+(?:NOT\s+)?[A-Z_][A-Z0-9_]*\}\}" ) def _is_truthy(val: Any) -> bool: """Pythonische Truthiness, mit Special-Case: leeres dict/list/str = False.""" if val is None: return False if isinstance(val, bool): return val if isinstance(val, (int, float)): return val != 0 if isinstance(val, str): return val.strip() != "" and val.lower() not in ("false", "0", "no", "nein") if isinstance(val, (list, dict, tuple, set)): return len(val) > 0 return True def render_template(template: str, context: dict[str, Any]) -> str: """Rendert ein Template mit dem gegebenen Kontext. Algorithmus: 1. IF-Bloecke iterativ aufloesen (max 10 Durchlaeufe, damit Nesting funktioniert) 2. Variablen substituieren Args: template: Markdown-Template mit {{VAR}} und {{#IF FLAG}}...{{/IF}} context: dict mit Variablen — Keys SCREAMING_SNAKE_CASE Returns: Gerendetes Markdown """ output = template # IF-Bloecke iterativ aufloesen — innerste zuerst, dann eine Ebene hoeher, usw. # Bis zu 20 Iterationen reichen fuer realistisches Nesting. for _ in range(20): def replace_if(match: re.Match[str]) -> str: negated = bool(match.group(1)) flag_name = match.group(2) content = match.group(3) flag_val = context.get(flag_name) condition = _is_truthy(flag_val) if negated: condition = not condition return content if condition else "" new_output = IF_INNERMOST.sub(replace_if, output) if new_output == output: break output = new_output # Falls noch orphan IF-Tags uebrig sind (z.B. unbalanced template): entfernen # damit sie nicht im Word-Output landen. output = ORPHAN_IF_TAG.sub("", output) def replace_var(match: re.Match[str]) -> str: name = match.group(1) val = context.get(name) if val is None: # Leere Platzhalter sichtbar machen fuer Debugging return f"[{name} fehlt]" if isinstance(val, bool): return "ja" if val else "nein" return str(val) output = VAR_PATTERN.sub(replace_var, output) return output def find_undefined_placeholders(template: str, context: dict[str, Any]) -> list[str]: """Listet alle Variablen-Platzhalter ohne Wert im Context.""" placeholders: set[str] = set() for match in VAR_PATTERN.finditer(template): placeholders.add(match.group(1)) for match in IF_INNERMOST.finditer(template): placeholders.add(match.group(2)) return sorted([p for p in placeholders if p not in context])