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).

Screencast showing how I insert links

Play by play:

  1. 0:00:00 inserting a custom dotemacs link with completion
  2. 0:00:11 inserting a link to a blog post
  3. 0:00:28 selecting text in an HTML export block and adding a link to it
  4. 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:

This is part of my Emacs configuration.
View org source for this post