From 336beb7bf7a58ffd8bd838e5ae53f00d6c983ed3 Mon Sep 17 00:00:00 2001 From: irrlicht Date: Wed, 2 Sep 2026 14:38:31 +0200 Subject: [PATCH] kakoune go to link in markdown --- kak/filetypes/markdown.kak | 9 +++ kak/scripts/hugo-link.py | 130 +++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 kak/scripts/hugo-link.py diff --git a/kak/filetypes/markdown.kak b/kak/filetypes/markdown.kak index 90dabd9..c94e033 100644 --- a/kak/filetypes/markdown.kak +++ b/kak/filetypes/markdown.kak @@ -1,6 +1,15 @@ +define-command -hidden -override md-follow-link \ +-docstring 'Folge dem Markdown-/Hugo-Link unter dem Cursor' %{ + evaluate-commands %sh{ + python3 "$kak_config/scripts/hugo-link.py" \ + "$kak_buffile" "$kak_cursor_line" "$kak_cursor_column" + } +} + define-command -hidden -override filetype-markdown %{ set-option window tabstop 2 set-option window indentwidth 2 + map window goto d ': md-follow-link' -docstring 'Link folgen' } hook global WinSetOption filetype=markdown %{ filetype-markdown } diff --git a/kak/scripts/hugo-link.py b/kak/scripts/hugo-link.py new file mode 100644 index 0000000..16f1d65 --- /dev/null +++ b/kak/scripts/hugo-link.py @@ -0,0 +1,130 @@ +#!/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()