fix(founding-wizard): nested IF-Bloecke korrekt aufloesen (innermost-first)
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

This commit is contained in:
Benjamin Admin
2026-05-20 19:21:08 +02:00
parent f08eb71480
commit badb356740
2 changed files with 56 additions and 7 deletions
@@ -15,13 +15,19 @@ from __future__ import annotations
import re
from typing import Any
# Pattern fuer {{#IF FLAG}}...{{/IF}} und {{#IF NOT FLAG}}...{{/IF}}
# Greedy / non-overlapping. Inhalt darf alles enthalten ausser einem geschlossenen {{/IF}}.
IF_BLOCK = re.compile(
r"\{\{#IF\s+(NOT\s+)?([A-Z_][A-Z0-9_]*)\}\}(.*?)\{\{/IF\}\}",
# 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:
@@ -55,7 +61,9 @@ def render_template(template: str, context: dict[str, Any]) -> str:
"""
output = template
for _ in range(10): # max 10 Levels Nesting
# 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)
@@ -66,11 +74,15 @@ def render_template(template: str, context: dict[str, Any]) -> str:
condition = not condition
return content if condition else ""
new_output = IF_BLOCK.sub(replace_if, output)
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)
@@ -90,6 +102,6 @@ def find_undefined_placeholders(template: str, context: dict[str, Any]) -> list[
placeholders: set[str] = set()
for match in VAR_PATTERN.finditer(template):
placeholders.add(match.group(1))
for match in IF_BLOCK.finditer(template):
for match in IF_INNERMOST.finditer(template):
placeholders.add(match.group(2))
return sorted([p for p in placeholders if p not in context])
@@ -68,6 +68,43 @@ class TestTemplateRenderer:
result = render_template(template, {"A": True, "B": True})
assert result == "A-on+B-on"
def test_nested_outer_false_inner_true(self):
"""Bug-Regression: nested IF in outer-false darf nicht den falschen close-tag matchen."""
template = "{{#IF OUTER}}outer-{{#IF INNER}}inner{{/IF}}-end{{/IF}}AFTER"
result = render_template(template, {"OUTER": False, "INNER": True})
assert result == "AFTER"
assert "{{/IF}}" not in result
assert "outer" not in result
def test_consecutive_if_blocks(self):
"""Bug-Regression: 2 aufeinanderfolgende IF-Bloecke."""
template = "{{#IF A}}a{{/IF}}{{#IF B}}b{{/IF}}"
result = render_template(template, {"A": False, "B": True})
assert result == "b"
assert "{{" not in result
def test_orphan_if_tag_removed(self):
"""Orphan {{/IF}} aufraeumen."""
template = "Text{{/IF}}mehr"
result = render_template(template, {})
assert "{{/IF}}" not in result
def test_real_go_gf_pattern(self):
"""Realistic Pattern aus GO-GF Template."""
template = (
"{{#IF HAS_CEO_DESIGNATION}}Mit CEO {{CEO_NAME}}{{#IF HAS_SHA}} und SHA{{/IF}}.{{/IF}}"
"{{#IF NOT HAS_CEO_DESIGNATION}}Kein CEO. Eskalation nach § 6.{{/IF}}"
)
# Fall: kein CEO, kein SHA
r1 = render_template(template, {"HAS_CEO_DESIGNATION": False, "HAS_SHA": False})
assert r1 == "Kein CEO. Eskalation nach § 6."
# Fall: CEO + SHA
r2 = render_template(template, {"HAS_CEO_DESIGNATION": True, "HAS_SHA": True, "CEO_NAME": "Benjamin"})
assert r2 == "Mit CEO Benjamin und SHA."
# Fall: CEO ohne SHA
r3 = render_template(template, {"HAS_CEO_DESIGNATION": True, "HAS_SHA": False, "CEO_NAME": "Benjamin"})
assert r3 == "Mit CEO Benjamin."
def test_find_undefined_placeholders(self):
template = "{{X}} {{Y}} {{#IF Z}}.{{/IF}}"
undefined = find_undefined_placeholders(template, {"X": "1"})