130 lines
5.7 KiB
Python
130 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Baut die Phase-0-Vorschau: Feature-Sicht + Einheiten → aufklappbares HTML.
|
|
|
|
Aufruf: baue-vorschau.py <features.md> <projekt-wurzel> <ausgabe.html> <einheiten.md ...>
|
|
|
|
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()
|