init
This commit is contained in:
129
phase0/baue-vorschau.py
Normal file
129
phase0/baue-vorschau.py
Normal file
@@ -0,0 +1,129 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user