Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur, was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen, das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor. Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale Bezeichnung ersetzt; die Regeln selbst sind unverändert. Co-Authored-By: Claude Opus 5 <[email protected]>
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
"""Übersetzt Obsidian-Eigenheiten in Markdown, das BookStack versteht."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass, field
|
|
|
|
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)
|
|
|
|
|
|
@dataclass
|
|
class Sammler:
|
|
verweise: Mapping[str, str]
|
|
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)
|
|
|
|
|
|
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 = treffer.group(1).strip()
|
|
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:
|
|
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())
|
|
if url:
|
|
return f"[{text_teil}]({url})"
|
|
if kurz not in sammler.offene_verweise:
|
|
sammler.offene_verweise.append(kurz)
|
|
return text_teil
|
|
|
|
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,
|
|
bilder: Mapping[str, str] | None = None,
|
|
) -> Umwandlung:
|
|
sammler = Sammler(verweise=verweise or {}, 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,
|
|
)
|