Adding Org Mode link awesomeness elsewhere: my-org-insert-link-dwim
| emacs, org
I love so many things about Org Mode's links. I can use C-c C-l
(org-insert-link
) to insert a link. If I've selected some text, C-c C-l
turns the text into the link's description. I can define my own custom link types with interactive completion, default descriptions, and export formats. This is so nice, I want it in all the different places I write links in:
- Markdown, like on the EmacsConf wiki; then I don't have to remember Markdown's syntax for links
- mastodon.el toots
- Oddmuse, like on EmacsWiki
- HTML/Web mode
- Org Mode HTML export blocks
Some considerations inspired by Emacs DWIM: do what ✨I✨ mean, which I used as a starting point:
- I want Emacs to use the URL from the clipboard, but only if I call it with
C-u
so I don't get surprised. - If I haven't already selected some text, I want to use the page title or the custom link description as a default description.
- I want to be able to use my custom link types for completion, but I want it to insert the external web links if I'm putting the link into a non-Org Mode buffer (or in a source or export block that isn't Org Mode). For example, let's say I select
dotemacs:my-org-insert-link-dwim
with completion. In Org Mode, it should use that as the link target so that I can follow the link to my config and have it exported as an HTML link. In Markdown, it should be inserted as[Adding Org Mode niceties elsewhere: my-org-insert-link-dwim](https://sachachua.com/dotemacs#my-org-insert-link-dwim)
.
Play by play:
- 0:00:00 inserting a custom dotemacs link with completion
- 0:00:11 inserting a link to a blog post
- 0:00:28 selecting text in an HTML export block and adding a link to it
- 0:00:48 adding a bookmark link as a plain text link in a Python src block
Here's the my-org-insert-link-dwim
function, using my-org-link-as-url
from Copy web link and my-org-set-link-target-with-search
from Using web searches and bookmarks to quickly link placeholders in Org Mode:
(defun my-org-insert-link-dwim (&optional use-clipboard)
"Like `org-insert-link' but with personal dwim preferences."
(interactive (list (equal current-prefix-arg '(4))))
(let* ((point-in-link (and (derived-mode-p 'org-mode) (org-in-regexp org-link-any-re 1)))
(clipboard-url (and use-clipboard
(when (string-match-p "^http" (current-kill 0))
(current-kill 0))))
(point-in-html-block (and (derived-mode-p 'org-mode)
(let ((elem (org-element-context)))
(and (eq (org-element-type elem) 'export-block)
(string= (org-element-property :type elem) "HTML")))))
(point-in-src-or-export-block
(and (derived-mode-p 'org-mode)
(let ((elem (org-element-context)))
(and (member (org-element-type elem) '(src-block export-block))
(not (string= (org-element-property :type elem) "Org"))))))
(url (cond
((and use-clipboard (string-match-p "^http" (current-kill 0)))
(current-kill 0))
((my-org-in-bracketed-text-link-p) nil)
((not point-in-link) (my-org-read-link))))
(region-content (when (region-active-p)
(buffer-substring-no-properties (region-beginning)
(region-end))))
(title (or region-content
(when (or (string-match (regexp-quote "*new toot*") (buffer-name))
(derived-mode-p '(markdown-mode web-mode oddmuse-mode))
point-in-html-block
point-in-src-or-export-block
(not (and (derived-mode-p 'org-mode)
point-in-link)))
(read-string "Title: "
(or (my-org-link-default-description url nil)
(my-page-title clipboard-url)))))))
;; resolve the links; see my-org-link-as-url in https://sachachua.com/dotemacs#web-link
(unless (and (derived-mode-p 'org-mode)
(not (or point-in-html-block point-in-src-or-export-block)))
(setq url (my-org-link-as-url url)))
(when (region-active-p) (delete-region (region-beginning) (region-end)))
(cond
((or (string-match (regexp-quote "*new toot*") (buffer-name))
(derived-mode-p 'markdown-mode))
(insert (format "[%s](%s)" title url)))
((or (derived-mode-p '(web-mode html-mode)) point-in-html-block)
(insert (format "<a href=\"%s\">%s</a>" url title)))
((derived-mode-p 'oddmuse-mode)
(insert (format "[%s %s]" url title)))
((or point-in-src-or-export-block
(not (derived-mode-p 'org-mode)))
(insert title " " url))
((and region-content url (not point-in-link))
(insert (org-link-make-string url region-content)))
((and url (not point-in-link))
(insert (org-link-make-string
url
(or title
(read-string "Title: "
(or (my-org-link-default-description url nil)
(my-page-title clipboard-url)))))))
;; bracketed [[plain text]]; see Using web searches and bookmarks to quickly link placeholders in Org Mode https://sachachua.com/dotemacs#completion-consult-consult-omni-using-web-searches-and-bookmarks-to-quickly-link-placeholders-in-org-mode
((my-org-set-link-target-with-search))
;; In Org Mode, edit the link
((call-interactively 'org-insert-link)))))
Consistent keybindings mean less thinking.
(dolist (group '((org . org-mode-map)
(markdown-mode . markdown-mode-map)
(web-mode . web-mode-map)
(oddmuse-mode . oddmuse-mode-map)
(text-mode . text-mode-map)
(html-mode . html-mode-map)))
(with-eval-after-load (car group)
(keymap-set (symbol-value (cdr group)) "C-c C-l" #'my-org-insert-link-dwim)))
All right, let's dig into the details. This code gets the page title so that we can use it as the link's description. I like to simplify some page titles. For example, when I link to Reddit or HN discussions, I just want to use "Reddit" or "HN".
(defun my-page-title (url)
"Get the page title for URL. Simplify some titles."
(condition-case nil
(pcase link
((rx "reddit.com") "Reddit")
((rx "news.ycombinator.com") "HN")
((rx "lobste.rs") "lobste.rs")
(_
(with-current-buffer (url-retrieve-synchronously url)
(string-trim
(replace-regexp-in-string
"[ \n]+" " "
(replace-regexp-in-string
"\\(^Github - \\|:: Sacha Chua\\)" ""
(or
(dom-text (car
(dom-by-tag (libxml-parse-html-region
(point-min)
(point-max))
'title)))
"")))))))
(error nil)))
Let's use that as the default for https:
links too.
(defun my-org-link-https-insert-description (link desc)
"Default to the page title."
(unless desc (my-page-title link)))
(with-eval-after-load 'org
(org-link-set-parameters "https" :insert-description #'my-org-link-https-insert-description))
I want to get the default description for a link,
even if it uses a custom link type. I extracted
this code from org-insert-link
.
(defun my-org-link-default-description (link desc)
"Return the default description for an Org Mode LINK.
This uses :insert-description if defined."
(let* ((abbrevs org-link-abbrev-alist-local)
(all-prefixes (append (mapcar #'car abbrevs)
(mapcar #'car org-link-abbrev-alist)
(org-link-types)))
(type
(cond
((and all-prefixes
(string-match (rx-to-string `(: string-start (submatch (or ,@all-prefixes)) ":")) link))
(match-string 1 link))
((file-name-absolute-p link) "file")
((string-match "\\`\\.\\.?/" link) "file"))))
(when (org-link-get-parameter type :insert-description)
(let ((def (org-link-get-parameter type :insert-description)))
(condition-case nil
(cond
((stringp def) def)
((functionp def)
(funcall def link desc)))
(error
nil))))))
Now I want an Emacs Lisp function that interactively reads a link with completion, but doesn't actually insert it. I extracted this logic from org-read-link
.
my-org-read-link, extracted from org-read-link
(defun my-org-read-link ()
"Act like `org-insert-link'. Return link."
(let* ((wcf (current-window-configuration))
(origbuf (current-buffer))
(abbrevs org-link-abbrev-alist-local)
(all-prefixes (append (mapcar #'car abbrevs)
(mapcar #'car org-link-abbrev-alist)
(org-link-types)))
link)
(unwind-protect
;; Fake a link history, containing the stored links.
(let ((org-link--history
(append (mapcar #'car org-stored-links)
org-link--insert-history)))
(setq link
(org-completing-read
(org-format-prompt "Insert link" (caar org-stored-links))
(append
(mapcar (lambda (x) (concat x ":")) all-prefixes)
(mapcar #'car org-stored-links)
;; Allow description completion. Avoid "nil" option
;; in the case of `completing-read-default' when
;; some links have no description.
(delq nil (mapcar 'cadr org-stored-links)))
nil nil nil
'org-link--history
(caar org-stored-links)))
(unless (org-string-nw-p link) (user-error "No link selected"))
(dolist (l org-stored-links)
(when (equal link (cadr l))
(setq link (car l))))
(when (or (member link all-prefixes)
(and (equal ":" (substring link -1))
(member (substring link 0 -1) all-prefixes)
(setq link (substring link 0 -1))))
(setq link (with-current-buffer origbuf
(org-link--try-special-completion link)))))
(when-let* ((window (get-buffer-window "*Org Links*" t)))
(quit-window 'kill window))
(set-window-configuration wcf)
(when (get-buffer "*Org Links*")
(kill-buffer "*Org Links*")))
link))
So now the my-org-insert-link-dwim
function can read a link with completion (unless I'm getting it from the clipboard), get the default description from the link (using custom links' :insert-description
or the webpage's title), and either wrap the link around the region or insert it in whatever syntax makes sense.
On a related note, you might also enjoy:
- Generating Descriptions of org-insert-link // Take on Rules
- Adding More Link Description Defaults // Take on Rules