#!/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 < 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"![{alt}]({path}){{width={width}cm}}" return f"![{alt}]({path}){{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()