"""Ü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"(?\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"![{name}]({url})" 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, )