Compare commits

..
1 Commits
Author SHA1 Message Date
irrlicht 336beb7bf7 kakoune go to link in markdown 2026-09-02 14:38:31 +02:00
2 changed files with 139 additions and 0 deletions
+9
View File
@@ -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 %{ define-command -hidden -override filetype-markdown %{
set-option window tabstop 2 set-option window tabstop 2
set-option window indentwidth 2 set-option window indentwidth 2
map window goto d '<esc>: md-follow-link<ret>' -docstring 'Link folgen'
} }
hook global WinSetOption filetype=markdown %{ filetype-markdown } hook global WinSetOption filetype=markdown %{ filetype-markdown }
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Folge dem Markdown-/Hugo-internen Link unter dem Cursor.
Aufruf: hugo-link.py <buffile> <cursor_line> <cursor_column>
Gibt auf stdout genau ein Kakoune-Kommando aus:
* edit -existing '<pfad>' -> 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](<ziel>)
LINK_RE = re.compile(r'\[[^\]]*\]\(\s*<?([^)>\s]+)>?(?:\s+"[^"]*")?\s*\)')
# <https://...> 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()