badb356740
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 42s
CI / detect-changes (push) Successful in 10s
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Successful in 16s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
"""
|
|
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])
|