116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Vorverarbeitung von Obsidian-Markdown zu sauberem Pandoc-Markdown.
|
|
|
|
Behandelt:
|
|
- Obsidian-Wikilinks: [[Ziel]] [[Ziel|Anzeige]] [[bild.jpg|BreitePx]]
|
|
- Emoji -> drucktaugliche Glyphen bzw. Text (dekorative Emojis werden entfernt)
|
|
- ==Markierung== -> **fett** (drucktauglicher Ersatz)
|
|
- %% Kommentare %% -> entfernt
|
|
- Variation-Selektoren (U+FE0F/U+FE0E) entfernen
|
|
|
|
Aufruf:
|
|
obsidian_preprocess.py <VAULT_ROOT> < eingabe.md > ausgabe.md
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
IMG_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg", ".avif"}
|
|
|
|
# Emoji -> Ersatz (leerer String = entfernen)
|
|
EMOJI = {
|
|
"\U0001F449": "\u2192", # 👉 ->
|
|
"\U0001F7E1": "\u25CF", # 🟡 ●
|
|
"\u26A0": "\u26A0", # ⚠ (wird über DejaVu-Fallback gesetzt)
|
|
"\u2600": "\u2600", # ☀ (DejaVu-Fallback)
|
|
"\U0001F338": "", # 🌸
|
|
"\U0001F342": "", # 🍂
|
|
"\u2744": "", # ❄
|
|
"\U0001F324": "", # 🌤
|
|
"\U0001F5FA": "", # 🗺
|
|
"\U0001F3D4": "", # 🏔
|
|
"\U0001F5E3": "", # 🗣
|
|
"\U0001F48E": "", # 💎
|
|
"\U0001F3B2": "", # 🎲
|
|
"\u26A1": "", # ⚡
|
|
"\U0001F4C8": "", # 📈
|
|
"\U0001F4C9": "", # 📉
|
|
"\U0001F504": "", # 🔄
|
|
}
|
|
VS = {"\uFE0F", "\uFE0E"}
|
|
|
|
SKIP_DIRS = {".git", ".obsidian", ".trash", "node_modules"}
|
|
|
|
|
|
def scan_images(root: str) -> dict:
|
|
"""basename(lower) -> absoluter Pfad (nur Nicht-.trash-Pfade)."""
|
|
mapping = {}
|
|
|
|
def walk():
|
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
for fn in filenames:
|
|
if os.path.splitext(fn)[1].lower() in IMG_EXT:
|
|
# Absoluter Pfad, damit Pandoc das Bild unabhängig vom
|
|
# aktuellen Arbeitsverzeichnis findet.
|
|
full = os.path.abspath(os.path.join(dirpath, fn))
|
|
key = fn.lower()
|
|
mapping.setdefault(key, full)
|
|
walk()
|
|
return mapping
|
|
|
|
|
|
def px_to_cm(px: float) -> float:
|
|
return round(px / 96.0 * 2.54, 2)
|
|
|
|
|
|
def resolve_wikilink(image_map: dict, inner: str) -> str:
|
|
if "|" in inner:
|
|
target, disp = inner.split("|", 1)
|
|
else:
|
|
target, disp = inner, None
|
|
target = target.strip()
|
|
ext = os.path.splitext(target.lower())[1]
|
|
if ext in IMG_EXT:
|
|
path = image_map.get(target.lower(), target)
|
|
alt = os.path.basename(target)
|
|
if disp is not None and disp.strip().isdigit():
|
|
width = px_to_cm(int(disp.strip()))
|
|
return f"{{width={width}cm}}"
|
|
return f"{{width=100%}}"
|
|
# Notiz-Link -> Klartext
|
|
return (disp.strip() if disp is not None else target)
|
|
|
|
|
|
def process(text: str, image_map: dict) -> str:
|
|
# 1. Kommentare
|
|
text = re.sub(r"%%.*?%%", "", text, flags=re.DOTALL)
|
|
# 2. Wikilinks
|
|
text = re.sub(r"\[\[([^\]]+)\]\]", lambda m: resolve_wikilink(image_map, m.group(1)), text)
|
|
# 3. Variation-Selektoren
|
|
for vs in VS:
|
|
text = text.replace(vs, "")
|
|
# 4. Emoji
|
|
for k, v in EMOJI.items():
|
|
text = text.replace(k, v)
|
|
# 5. ==mark== -> **fett**
|
|
text = re.sub(r"==([^=\n]+)==", r"**\1**", text)
|
|
# 6. Leerraum glätten (nach Emoji-Entfernung), aber Tabellen/Code unberührt
|
|
out = []
|
|
for line in text.split("\n"):
|
|
stripped = line.lstrip()
|
|
if not stripped.startswith("|") and not stripped.startswith("```"):
|
|
line = re.sub(r"[ \t]{2,}", " ", line)
|
|
out.append(line)
|
|
return "\n".join(out)
|
|
|
|
|
|
def main():
|
|
root = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
image_map = scan_images(root)
|
|
sys.stdout.write(process(sys.stdin.read(), image_map))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|