Files
Pascal MichelandClaude Opus 5.5 fa1c26b407 Erste Fassung: Claude-Werkzeuge aus dem Vault
.vault-tools (ohne trading-plattform-pruefung und watermarks-remover),
.claude/agents, .claude/skills, .claude/workflows, .claude/settings.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 11:27:59 +02:00

131 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Liest und schreibt die todo.md eines Vault-Projekts."""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from .abgleich import Aufgabe
CHECKBOX_MUSTER = re.compile(r"^(?P<einrueckung>[ \t]*)- \[(?P<haken>[ xX])\] (?P<rest>.*)$")
MARKER_MUSTER = re.compile(r"\s*<!--\s*plane:(?P<marker>[A-Z0-9]+-\d+)\s*-->\s*$")
ABSCHNITT_AUS_PLANE = "## Aus Plane"
@dataclass
class TodoDatei:
pfad: Path
zeilen: list[str]
frontmatter: dict[str, str]
aufgaben: list[Aufgabe]
frontmatter_zeilen: int
def _lies_frontmatter(zeilen: list[str]) -> tuple[dict[str, str], int]:
"""Gibt die flachen Schlüssel-Wert-Paare und die Anzahl belegter Zeilen zurück."""
if not zeilen or zeilen[0].strip() != "---":
return {}, 0
for index in range(1, len(zeilen)):
if zeilen[index].strip() == "---":
werte: dict[str, str] = {}
for zeile in zeilen[1:index]:
schluessel, trenner, wert = zeile.partition(":")
if trenner:
werte[schluessel.strip()] = wert.strip()
return werte, index + 1
return {}, 0
def lies(pfad: Path) -> TodoDatei:
# utf-8-sig statt utf-8: ein BOM am Dateianfang wuerde sonst als  in
# Zeile 0 stehen bleiben, das Frontmatter waere unsichtbar und der Skill
# meldete "plane_project fehlt", obwohl es dasteht. Windows-Editoren und
# PowerShell 5.1 schreiben UTF-8 regelmaessig mit BOM. Geschrieben wird
# weiterhin ohne BOM.
text = Path(pfad).read_text(encoding="utf-8-sig")
zeilen = text.split("\n")
frontmatter, belegt = _lies_frontmatter(zeilen)
aufgaben: list[Aufgabe] = []
for nummer, zeile in enumerate(zeilen):
if nummer < belegt:
continue
treffer = CHECKBOX_MUSTER.match(zeile)
if not treffer:
continue
rest = treffer.group("rest")
marker_treffer = MARKER_MUSTER.search(rest)
marker = marker_treffer.group("marker") if marker_treffer else None
text_ohne_marker = MARKER_MUSTER.sub("", rest).strip() if marker else rest.strip()
aufgaben.append(
Aufgabe(
zeilennummer=nummer,
text=text_ohne_marker,
erledigt=treffer.group("haken").lower() == "x",
marker=marker,
einrueckung=treffer.group("einrueckung"),
)
)
return TodoDatei(
pfad=Path(pfad),
zeilen=zeilen,
frontmatter=frontmatter,
aufgaben=aufgaben,
frontmatter_zeilen=belegt,
)
def plane_projekt(frontmatter: dict[str, str]) -> str | None:
wert = frontmatter.get("plane_project", "").strip()
return wert or None
def baue_zeile(einrueckung: str, text: str, erledigt: bool, marker: str | None) -> str:
haken = "x" if erledigt else " "
zeile = f"{einrueckung}- [{haken}] {text}"
if marker:
zeile = f"{zeile} <!-- plane:{marker} -->"
return zeile
def schreibe(
datei: TodoDatei,
aenderungen: dict[int, tuple[str, bool, str | None]],
neue_zeilen: list[str],
) -> str:
"""Baut den neuen Dateiinhalt. Unberührte Zeilen bleiben byteweise gleich."""
zeilen = list(datei.zeilen)
nach_nummer = {aufgabe.zeilennummer: aufgabe for aufgabe in datei.aufgaben}
for nummer, (text, erledigt, marker) in aenderungen.items():
alt = nach_nummer.get(nummer)
einrueckung = alt.einrueckung if alt else ""
zeilen[nummer] = baue_zeile(einrueckung, text, erledigt, marker)
if neue_zeilen:
zeilen = _haenge_an_aus_plane(zeilen, neue_zeilen)
return "\n".join(zeilen)
def _haenge_an_aus_plane(zeilen: list[str], neue_zeilen: list[str]) -> list[str]:
ergebnis = list(zeilen)
if ABSCHNITT_AUS_PLANE in [z.strip() for z in ergebnis]:
letzte = max(
i for i, z in enumerate(ergebnis) if z.strip() == ABSCHNITT_AUS_PLANE
)
einfuegen_ab = len(ergebnis)
for i in range(letzte + 1, len(ergebnis)):
if ergebnis[i].startswith("#"):
einfuegen_ab = i
break
while einfuegen_ab > letzte + 1 and not ergebnis[einfuegen_ab - 1].strip():
einfuegen_ab -= 1
return ergebnis[:einfuegen_ab] + neue_zeilen + ergebnis[einfuegen_ab:]
while ergebnis and not ergebnis[-1].strip():
ergebnis.pop()
return ergebnis + ["", ABSCHNITT_AUS_PLANE, ""] + neue_zeilen + [""]