Automatischer Push: 45 Datei(en) geändert

- .claude/agents/planer.md
- .claude/agents/quorum-stimme.md
- .claude/skills/bookstack-upload/SKILL.md
- .claude/skills/bookstack-upload/scripts/bookstack/archiv.py
- .claude/skills/bookstack-upload/scripts/bookstack/auswahl.py
- .claude/skills/bookstack-upload/scripts/bookstack/beschreibung.py
- .claude/skills/bookstack-upload/scripts/bookstack/bilder.py
- .claude/skills/bookstack-upload/scripts/bookstack/ergebnis.py
- .claude/skills/bookstack-upload/scripts/bookstack/hochladen.py
- .claude/skills/bookstack-upload/scripts/bookstack/konvertierung.py
- .claude/skills/bookstack-upload/scripts/bookstack/struktur.py
- .claude/skills/bookstack-upload/scripts/bookstack/verweise.py
- … und 33 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-10-01 04:00:02 +02:00
1 parent 60dee0ab68
commit deec79a6ea
45 files changed
+8645 -262

No files matched your search

@@ -1,4 +1,9 @@
"""Übersetzt Obsidian-Eigenheiten in Markdown, das BookStack versteht."""
"""Übersetzt Obsidian-Eigenheiten in Markdown, das BookStack versteht.
Ersteller: Pascal Michel
Erstellt: 2026-08-05
Doku: docs/code/konvertierung.md · Version und Änderungen: CHANGELOG.md
"""
from __future__ import annotations
@@ -6,6 +11,8 @@ import re
from collections.abc import Mapping
from dataclasses import dataclass, field
from bookstack.verweise import Verweisindex
EINBETTUNG = re.compile(r"!\[\[([^\]|]+?)(?:\|[^\]]*)?\]\]")
WIKILINK = re.compile(r"\[\[([^\]|]+?)(?:\|([^\]]*))?\]\]")
REVIEW_TAG = re.compile(r"(?<![\w#-])#review(?:-done)?(?![\w-])[ \t]*")
@@ -20,16 +27,44 @@ class Umwandlung:
markdown: str
hinweise: list[str] = field(default_factory=list)
bildnamen: list[str] = field(default_factory=list)
offene_verweise: list[str] = field(default_factory=list)
offene_verweise: list[str] = field(default_factory=list) # Einträge: ziel aus zerlege_wikilink
aufgeloeste_verweise: dict[str, str] = field(default_factory=dict) # ziel → URL
@dataclass
class Sammler:
verweise: Mapping[str, str]
verweise: Verweisindex
bilder: Mapping[str, str]
hinweise: list[str] = field(default_factory=list)
bildnamen: list[str] = field(default_factory=list)
offene_verweise: list[str] = field(default_factory=list)
aufgeloeste_verweise: dict[str, str] = field(default_factory=dict)
def _ohne_tabellen_escape(text: str) -> str:
# In Markdown-Tabellen steht [[Ziel\|Alias]]; der Backslash gehört nicht zum Ziel.
text = text.strip()
if text.endswith("\\"):
text = text[:-1]
return text.strip()
def zerlege_wikilink(ziel_roh: str, alias: str | None) -> tuple[str, str]:
"""(ziel, anzeigetext).
ziel: ziel_roh ohne Anker, ohne abschließenden Backslash, gestrippt, sonst wie geschrieben.
anzeigetext: alias gestrippt, sonst letztes Segment von ziel."""
ziel = _ohne_tabellen_escape(ziel_roh.split("#", 1)[0])
anzeigetext = (alias or "").strip() or ziel.split("/")[-1]
return ziel, anzeigetext
def ankertext(ziel_roh: str, alias: str | None) -> str | None:
"""Anzeigetext eines reinen Ankerlinks wie [[#Abschnitt]] oder [[#Abschnitt|Text]], sonst None.
Alias gestrippt, sonst der Anker ohne '#'."""
ziel = _ohne_tabellen_escape(ziel_roh)
if not ziel.startswith("#"):
return None
return (alias or "").strip() or ziel.lstrip("#").strip()
def _ersetze_ausserhalb_von_code(text: str, sammler: "Sammler") -> str:
@@ -43,7 +78,7 @@ def _ersetze_ausserhalb_von_code(text: str, sammler: "Sammler") -> str:
def _ersetze(text: str, sammler: Sammler) -> str:
def bild(treffer: re.Match[str]) -> str:
name = treffer.group(1).strip()
name = _ohne_tabellen_escape(treffer.group(1))
if name not in sammler.bildnamen:
sammler.bildnamen.append(name)
url = sammler.bilder.get(name)
@@ -53,15 +88,18 @@ def _ersetze(text: str, sammler: Sammler) -> str:
return f"*(Bild „{name}“ wurde nicht übertragen)*"
def link(treffer: re.Match[str]) -> str:
ziel, alias = treffer.group(1), treffer.group(2)
kurz = ziel.split("/")[-1].split("#")[0].strip()
text_teil = (alias or kurz).strip()
url = sammler.verweise.get(kurz.casefold())
anker = ankertext(treffer.group(1), treffer.group(2))
if anker is not None:
# Ein Anker auf die eigene Seite ist kein Verweis: Klartext, nichts in offene_verweise.
return anker
ziel, anzeigetext = zerlege_wikilink(treffer.group(1), treffer.group(2))
url = sammler.verweise.aufloesen(ziel)
if url:
return f"[{text_teil}]({url})"
if kurz not in sammler.offene_verweise:
sammler.offene_verweise.append(kurz)
return text_teil
sammler.aufgeloeste_verweise[ziel] = url
return f"[{anzeigetext}]({url})"
if ziel not in sammler.offene_verweise:
sammler.offene_verweise.append(ziel)
return anzeigetext
def callout(treffer: re.Match[str]) -> str:
titel = treffer.group(2).strip() or treffer.group(1).capitalize()
@@ -89,10 +127,11 @@ def ohne_titelueberschrift(rumpf: str, titel: str) -> str:
def konvertiere(
rumpf: str,
*,
verweise: Mapping[str, str] | None = None,
verweise: Mapping[str, str] | None = None, # Verweisindex oder schlichtes dict
bilder: Mapping[str, str] | None = None,
) -> Umwandlung:
sammler = Sammler(verweise=verweise or {}, bilder=bilder or {})
index = verweise if isinstance(verweise, Verweisindex) else Verweisindex(verweise)
sammler = Sammler(verweise=index, bilder=bilder or {})
markdown = _ersetze_ausserhalb_von_code(rumpf, sammler)
markdown = re.sub(r"\n{3,}", "\n\n", markdown).strip() + "\n"
return Umwandlung(
@@ -100,4 +139,5 @@ def konvertiere(
hinweise=sammler.hinweise,
bildnamen=sammler.bildnamen,
offene_verweise=sammler.offene_verweise,
aufgeloeste_verweise=sammler.aufgeloeste_verweise,
)