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]>
This commit is contained in:
commit
fa1c26b407
137 files changed
+43926
No files matched your search
@@ -0,0 +1,143 @@
|
||||
"""Ü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
|
||||
|
||||
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]*")
|
||||
CALLOUT = re.compile(r"^>\s*\[!(\w+)\][ \t]*(.*)$", re.MULTILINE)
|
||||
ZAUN = re.compile(r"(```.*?```|~~~.*?~~~)", re.DOTALL)
|
||||
TITELZEILE = re.compile(r"^#\s+(.+?)[ \t]*$", re.MULTILINE)
|
||||
MAX_TITEL = 255
|
||||
|
||||
|
||||
@dataclass
|
||||
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) # Einträge: ziel aus zerlege_wikilink
|
||||
aufgeloeste_verweise: dict[str, str] = field(default_factory=dict) # ziel → URL
|
||||
|
||||
|
||||
@dataclass
|
||||
class Sammler:
|
||||
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:
|
||||
teile = ZAUN.split(text)
|
||||
for stelle, teil in enumerate(teile):
|
||||
if stelle % 2 == 1: # ungerade Indizes sind die Codeblöcke
|
||||
continue
|
||||
teile[stelle] = _ersetze(teil, sammler)
|
||||
return "".join(teile)
|
||||
|
||||
|
||||
def _ersetze(text: str, sammler: Sammler) -> str:
|
||||
def bild(treffer: re.Match[str]) -> str:
|
||||
name = _ohne_tabellen_escape(treffer.group(1))
|
||||
if name not in sammler.bildnamen:
|
||||
sammler.bildnamen.append(name)
|
||||
url = sammler.bilder.get(name)
|
||||
if url:
|
||||
return f""
|
||||
sammler.hinweise.append(f"Bild nicht übertragen: {name}")
|
||||
return f"*(Bild „{name}“ wurde nicht übertragen)*"
|
||||
|
||||
def link(treffer: re.Match[str]) -> str:
|
||||
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:
|
||||
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()
|
||||
return f"> **{titel}**"
|
||||
|
||||
text = EINBETTUNG.sub(bild, text)
|
||||
text = WIKILINK.sub(link, text)
|
||||
text = CALLOUT.sub(callout, text)
|
||||
return REVIEW_TAG.sub("", text)
|
||||
|
||||
|
||||
def ohne_titelueberschrift(rumpf: str, titel: str) -> str:
|
||||
"""Entfernt die erste H1, wenn sie den Seitentitel wiederholt.
|
||||
|
||||
BookStack stellt den Seitennamen ohnehin als Überschrift dar. Bliebe die H1
|
||||
im Text stehen, stünde die Überschrift zweimal auf der Seite.
|
||||
Weicht die H1 vom Titel ab, bleibt sie unangetastet.
|
||||
"""
|
||||
treffer = TITELZEILE.search(rumpf)
|
||||
if treffer is None or treffer.group(1).strip()[:MAX_TITEL] != titel:
|
||||
return rumpf
|
||||
return rumpf[: treffer.start()] + rumpf[treffer.end() :].lstrip("\n")
|
||||
|
||||
|
||||
def konvertiere(
|
||||
rumpf: str,
|
||||
*,
|
||||
verweise: Mapping[str, str] | None = None, # Verweisindex oder schlichtes dict
|
||||
bilder: Mapping[str, str] | None = None,
|
||||
) -> Umwandlung:
|
||||
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(
|
||||
markdown=markdown,
|
||||
hinweise=sammler.hinweise,
|
||||
bildnamen=sammler.bildnamen,
|
||||
offene_verweise=sammler.offene_verweise,
|
||||
aufgeloeste_verweise=sammler.aufgeloeste_verweise,
|
||||
)
|
||||
Reference in new issue
Block a user