#!/usr/bin/env python3 """Baut die Phase-0-Vorschau: Feature-Sicht + Einheiten → aufklappbares HTML. Aufruf: baue-vorschau.py Der konkrete Code je Einheit wird live aus der Quelle extrahiert (Anker: Pfad::Symbol) — Artefakte enthalten nie Code-Kopien. """ import html import json import re import sys from pathlib import Path def einheiten_parsen(pfade) -> dict: out = {} aktuelle = None modus = None for pfad in pfade: for zeile in Path(pfad).read_text(encoding="utf-8").splitlines(): if zeile.startswith("## ") and "::" in zeile: aktuelle = {"id": zeile[3:].strip(), "beschreibung": "", "input": "", "output": "", "entscheidungen": [], "kanten": [], "anker": []} out[aktuelle["id"]] = aktuelle modus = None elif aktuelle is None: continue elif zeile.startswith("beschreibung:"): aktuelle["beschreibung"] = zeile.split(":", 1)[1].strip(); modus = "beschreibung" elif zeile.startswith("input:"): aktuelle["input"] = zeile.split(":", 1)[1].strip(); modus = "input" elif zeile.startswith("output:"): aktuelle["output"] = zeile.split(":", 1)[1].strip(); modus = "output" elif zeile.startswith("anker:"): aktuelle["anker"].append(zeile.split(":", 1)[1].strip()) elif zeile.strip() in ("entscheidungen:", "fakten:"): modus = "punkte" elif zeile.strip() == "kanten:": modus = "kanten" elif modus == "punkte" and (m := re.match(r'\s+beleg:\s*"(.*)"', zeile)): pass # Belege prüft pruefe.py; die Vorschau zeigt echten Code statt Zitaten elif modus == "punkte" and zeile.startswith("- "): aktuelle["entscheidungen"].append(zeile[2:].strip()) elif modus == "punkte" and zeile.startswith(" ") and aktuelle["entscheidungen"]: aktuelle["entscheidungen"][-1] += " " + zeile.strip() elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)", zeile)): aktuelle["kanten"].append({"typ": m.group(1), "ziel": m.group(2)}) elif modus in ("beschreibung", "input", "output") and zeile.startswith(" "): aktuelle[modus] += " " + zeile.strip() return out def features_parsen(pfad) -> list: bereiche = [] bereich = feature = None for zeile in Path(pfad).read_text(encoding="utf-8").splitlines(): if zeile.startswith("# Bereich:"): bereich = {"name": zeile.split(":", 1)[1].strip(), "beschreibung": "", "features": []} bereiche.append(bereich); feature = None elif zeile.startswith("## Feature:"): titel = zeile[11:].strip() flag = "kern" if "[kern]" in titel else "rand" feature = {"titel": re.sub(r"\s*\[(kern|rand)\]", "", titel).strip(), "flag": flag, "beschreibung": "", "teilfeatures": []} bereich["features"].append(feature) elif zeile.startswith("### ") and feature is not None: feature["teilfeatures"].append({"titel": zeile[4:].strip(), "einheiten": []}) elif zeile.startswith("einheiten:") and feature and feature["teilfeatures"]: feature["teilfeatures"][-1]["einheiten"] = \ [t.strip() for t in zeile.split(":", 1)[1].split(",") if t.strip()] elif zeile.startswith("beschreibung:"): ziel = feature if feature is not None else bereich if ziel is not None: ziel["beschreibung"] = zeile.split(":", 1)[1].strip() return bereiche def code_extrahieren(wurzel: Path, eid: str) -> str: """Quellcode eines Ankers (pfad::symbol): def/class-Block per Einrückung.""" if "::" not in eid: return "" datei, symbol = eid.split("::", 1) p = wurzel / datei if not p.is_file(): return "" zeilen = p.read_text(encoding="utf-8").splitlines() start = None for i, z in enumerate(zeilen): if re.match(rf"(\s*)(?:async\s+)?(?:def|class)\s+{re.escape(symbol)}\b", z): start = i break if re.match(rf"{re.escape(symbol)}\s*[:=]", z): # Modul-Konstante start = i ende = i + 1 while ende < len(zeilen) and (zeilen[ende].startswith((" ", "\t", ")", "]", "}")) or not zeilen[ende].strip()): ende += 1 return "\n".join(zeilen[start:ende]).rstrip() if start is None: return "" einzug = len(zeilen[start]) - len(zeilen[start].lstrip()) ende = start + 1 while ende < len(zeilen): z = zeilen[ende] if z.strip() and (len(z) - len(z.lstrip())) <= einzug: break ende += 1 return "\n".join(zeilen[start:ende]).rstrip() def main(): features_pfad, wurzel, ausgabe = sys.argv[1], Path(sys.argv[2]), sys.argv[3] einheiten = einheiten_parsen(sys.argv[4:]) bereiche = features_parsen(features_pfad) code = {} for e in einheiten.values(): for eid in [e["id"], *e["anker"]]: code[e["id"]] = code.get(e["id"]) or code_extrahieren(wurzel, eid) daten = {"bereiche": bereiche, "einheiten": einheiten, "code": code} tpl = Path(__file__).with_name("vorschau-template.html").read_text(encoding="utf-8") Path(ausgabe).write_text(tpl.replace("/*DATEN*/", json.dumps(daten, ensure_ascii=False)), encoding="utf-8") n_feat = sum(len(b["features"]) for b in bereiche) print(f"{ausgabe}: {len(bereiche)} Bereiche, {n_feat} Features, {len(einheiten)} Einheiten") if __name__ == "__main__": main()