"""Feature-Dateien: eine .md je Feature, Rohtext wird verbatim gespeichert.""" import re import parser import projekte _UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"}) # Dateisysteme lassen 255 Zeichen je Name zu. Der Titel darf länger sein — er steht # in der Datei, nicht im Namen. Ohne Deckel warf ein langer Titel einen Serverfehler. NAMENSLAENGE = 80 def _slug(titel: str) -> str: s = titel.lower().translate(_UMLAUTE) s = re.sub(r"[^a-z0-9]+", "-", s).strip("-") return s[:NAMENSLAENGE].strip("-") or "feature" def _eintrag(datei) -> dict: roh = datei.read_text() return {"id": datei.stem, "roh": roh, "sektionen": parser.zerlege(roh)} def _datei(projekt: str, id: str): datei = projekte.wurzel(projekt) / f"{id}.md" if not datei.is_file(): raise FileNotFoundError(f"Feature „{id}“ nicht gefunden") return datei def liste(projekt: str, nur_stand: bool = False) -> list[dict]: """Die Karten. Standardmäßig die, an denen gerade gearbeitet wird. `nur_stand` liest stattdessen den Projektordner — die Übersicht soll sagen, was das Projekt heute kann, nicht was jemand gerade plant. """ ordner = projekte.stand(projekt) if nur_stand else projekte.wurzel(projekt) dateien = ordner.glob("*.md") eintraege = [_eintrag(d) for d in sorted(dateien)] return sorted(eintraege, key=lambda e: e["sektionen"]["titel"].lower()) def pruefbar(projekt: str) -> list[dict]: """Alles außer der Vision. Die hält fest, wie es gedacht ist — sie ist die Vorlage, nicht der Prüfling, und keine Prüfung schlägt Änderungen daran vor.""" return [f for f in liste(projekt) if f["sektionen"]["art"] != parser.VISION] def _frei(ordner, slug: str): """Der erste freie Dateiname zum Slug; gleiche Namen bekommen eine Nummer.""" datei = ordner / f"{slug}.md" n = 2 while datei.exists(): datei = ordner / f"{slug}-{n}.md" n += 1 return datei def anlegen(projekt: str, roh: str) -> dict: titel = parser.zerlege(roh)["titel"] if not titel: raise ValueError("Titel fehlt") datei = _frei(projekte.wurzel(projekt), _slug(titel)) datei.write_text(roh) return _eintrag(datei) def _folge_titel(datei, titel: str): """Benennt die Datei um, wenn der Slug nicht mehr zum Titel passt.""" slug = _slug(titel) if re.fullmatch(re.escape(slug) + r"(-\d+)?", datei.stem): return datei # passt schon (inkl. Kollisions-Suffix) ziel = _frei(datei.parent, slug) datei.rename(ziel) return ziel def speichern(projekt: str, id: str, roh: str) -> dict: datei = _datei(projekt, id) titel = parser.zerlege(roh)["titel"] if not titel: raise ValueError("Titel fehlt") datei.write_text(roh) return _eintrag(_folge_titel(datei, titel)) def loeschen(projekt: str, id: str): _datei(projekt, id).unlink()