#!/usr/bin/env python3 """Folge dem Markdown-/Hugo-internen Link unter dem Cursor. Aufruf: hugo-link.py Gibt auf stdout genau ein Kakoune-Kommando aus: * edit -existing '' -> Zieldatei oeffnen * nop %sh{ xdg-open ... } -> externe URL im Browser * echo -markup '{Error}..' -> nichts gefunden / Fehler """ import os import re import shlex import sys def kak_str(s): """String fuer Kakoune in einfachen Anfuehrungszeichen quoten.""" return "'" + s.replace("'", "''") + "'" def emit_and_exit(cmd): print(cmd) sys.exit(0) def fail(msg): emit_and_exit("echo -markup " + kak_str("{Error}" + msg)) CONFIG_NAMES = ( "hugo.toml", "hugo.yaml", "hugo.yml", "hugo.json", "config.toml", "config.yaml", "config.yml", "config.json", ) # [text](ziel "optionaler Titel") bzw. [text]() LINK_RE = re.compile(r'\[[^\]]*\]\(\s*\s]+)>?(?:\s+"[^"]*")?\s*\)') # Autolink AUTOLINK_RE = re.compile(r'<([a-zA-Z][\w+.-]*:[^>\s]+)>') def find_site_root(start): d = start while True: if any(os.path.exists(os.path.join(d, n)) for n in CONFIG_NAMES): return d if os.path.isdir(os.path.join(d, "content")): return d parent = os.path.dirname(d) if parent == d: return None d = parent def target_under_cursor(line, idx): for regex in (LINK_RE, AUTOLINK_RE): for m in regex.finditer(line): if m.start() <= idx < m.end(): return m.group(1) return None def open_external(target): emit_and_exit("nop %%sh{ setsid xdg-open %s >/dev/null 2>&1 & }" % shlex.quote(target)) def main(): if len(sys.argv) != 4: fail("interner Aufruffehler") buffile, line_s, col_s = sys.argv[1], sys.argv[2], sys.argv[3] line_no, col = int(line_s), int(col_s) try: with open(buffile, encoding="utf-8") as fh: lines = fh.read().split("\n") except OSError as exc: fail("kann Buffer nicht lesen: %s" % exc) if not 1 <= line_no <= len(lines): fail("Cursor ausserhalb des Buffers") line = lines[line_no - 1] target = target_under_cursor(line, col - 1) if target is None: fail("kein Link unter dem Cursor") # Fragment (#...) und Query (?...) abtrennen target = re.split(r'[#?]', target, maxsplit=1)[0] if not target: fail("Link zeigt nur auf einen Anker") if (re.match(r'^[a-zA-Z][a-zA-Z0-9+.-]*://', target) or target.startswith("//") or target.startswith("mailto:")): open_external(target) buf_dir = os.path.dirname(os.path.abspath(buffile)) if target.startswith("/"): root = find_site_root(buf_dir) if root is None: fail("kein Hugo-Root (hugo.toml / config.*) oberhalb gefunden") content_dir = os.path.join(root, "content") if not os.path.isdir(content_dir): content_dir = root base = os.path.normpath( os.path.join(content_dir, target.lstrip("/"))) else: base = os.path.normpath(os.path.join(buf_dir, target)) if base.endswith(".md"): candidates = [base] else: candidates = [ base + ".md", os.path.join(base, "_index.md"), os.path.join(base, "index.md"), base, # z.B. direkt verlinktes Bild / statische Datei ] for cand in candidates: if os.path.isfile(cand): emit_and_exit("edit -existing " + kak_str(cand)) fail("Ziel nicht gefunden: " + " | ".join(candidates)) if __name__ == "__main__": main()