π

UOMF: Linking Headings

Show Sidebar

This is an article from a series of blog postings. Please do read my "Using Org Mode Features" (UOMF) series page for explanations on articles of this series.

In this article, I'm going to explain how I am using internal links to create links pointing to headings using unique :ID: properties.

Motivation: Why Linking?

One of the most valuable assets I do have in my Org mode is my personal knowledge base. It consists of a hierarchy like that:

I only began to outline the sub-headings of "software" so that you get an idea how my knowledge base looks like. Frequent readers of my blog already know that I dislike strict hierarchies for many good reasons. In this case (and others), I have to stick to a strict hierarchy because that's was Emacs Org mode is providing me: placing headings into a hierarchy.

To compensate this limitation, I'm using internal links all the time. For example, if I add a new heading on a special Firefox plugin that provides input for my Org mode, I have to place it either in the "software/emacs/WWW/" section or in "software/Firefox/plugins/". Either way, I tend to link this heading to the other place as well so that I might find it using navigation to both logical destinations. Sometimes, I even have to create more than one link to a heading because it would correlate with more than two potential locations. Similar to a Zettelkasten.

Why Do I Need Unique IDs?

The chapter about handling links in the Org manual mentions two methods for working with links:

Applied to this heading, this results in following link:

 [[file:public_voit.org::*Why Do I Need Unique IDs?][Why Do I Need Unique IDs?]]	  

To follow any Org mode link, you can invoke C-c C-o (org-open-at-point) or use RET or your mouse. I stick to the C-c C-o pattern since it applies to "follow" almost anything Org mode.

As you can see above, this type of internal link is based on the current Org mode file and the title of the heading. I consider both of them fragile. This link gets a broken one when I move the heading to a different file or when I rename the heading.

Therefore, I prefer methods that are using some kind of unique identifiers (IDs) for linking. Usually, these IDs are stores as special properties in the :PROPTERTY: drawers.

DISCLAIMER: I have to admit, that for historical reasons I might use internal links a bit off-standard. So please do read about my method with a grain of salt. Read more about this in the following section.

ID Versus CUSTOM_ID Properties

The Org mode manual section about internal links does refer to :CUSTOM_ID: properties for internal links. This provides you the ability to link to a heading:

 * Example heading
 :PROPERTIES:
 :CUSTOM_ID:  This-is-my-unique-ID
 :END:

 [[#This-is-my-unique-ID]] is a link to the heading above.	  

This is, where I'm using a non-standard approach compared to the manual. I began using internal links when I did not know about :CUSTOM_ID: properties. Back then, I used :ID: properties instead.

I once started the discussion about using :ID: versus :CUSTOM_ID: properties on the Org mode mailing list. My key takeaways were:

So I kept using :ID:.

One thing you have to make sure when you also want to use :ID: properties for linking: they need to be "activated" as described in the manual with (require 'org-id) or similar.

Creating Unique ID Properties

Not only in the "All Things Org Mode" video with John Wiegley you can see that UUIDs could be generated by external tools like uuidgen (see org-inline-note() on John's Org mode setup). This results in UUIDs like dd4b6da4-aabc-4c38-bdfa-e901c110d698 which obviously does fulfill the most important requirement of UUIDs: uniqueness.

However, I personally do find it helpful when I do get an idea about the link target within the link itself. I seldom use descriptions for internal links because I'm lazy. This is why I was creating my IDs manually until a couple of days ago. I switched to the :PROPERTIES: drawer, added a new line, entered :ID: typically followed by a date-stamp (easily added by a keyboard command) and a short description of the heading. For this heading, this would have looked like that:

 *** Creating Unique ID Properties
 :PROPERTIES:
 :ID: 2019-11-16-id-properties
 :END:	  

The uniqueness was introduces by pre-pending with the date-stamp: during one single day I remember that I might have created a similar heading already. It's unlikely that I may create identical IDs this way.

When a workflow like this becomes a habit, I tend not to think about it for the longest time.

Sadly.

Until recently, when I got the idea of making my life a bit easier. I overcame my aversion to elisp and came up with my-id-get-or-generate() and its supporting function my-generate-sanitized-alnum-dash-string(str):

(defun my-generate-sanitized-alnum-dash-string(str)
"Returns a string which contains only a-zA-Z0-9 with single dashes
 replacing all other characters in-between them.

 Some parts were copied and adapted from org-hugo-slug
 from https://github.com/kaushalmodi/ox-hugo (GPLv3)."
(let* (;; Remove "<FOO>..</FOO>" HTML tags if present.
       (str (replace-regexp-in-string "<\\(?1:[a-z]+\\)[^>]*>.*</\\1>" "" str))
       ;; Remove URLs if present in the string.  The ")" in the
       ;; below regexp is the closing parenthesis of a Markdown
       ;; link: [Desc](Link).
       (str (replace-regexp-in-string (concat "\\](" ffap-url-regexp "[^)]+)") "]" str))
       ;; Replace "&" with " and ", "." with " dot ", "+" with
       ;; " plus ".
       (str (replace-regexp-in-string
             "&" " and "
             (replace-regexp-in-string
              "\\." " dot "
              (replace-regexp-in-string
               "\\+" " plus " str))))
       ;; Replace German Umlauts with 7-bit ASCII.
       (str (replace-regexp-in-string "[Ä]" "Ae" str t))
       (str (replace-regexp-in-string "[Ü]" "Ue" str t))
       (str (replace-regexp-in-string "[Ö]" "Oe" str t))
       (str (replace-regexp-in-string "[ä]" "ae" str t))
       (str (replace-regexp-in-string "[ü]" "ue" str t))
       (str (replace-regexp-in-string "[ö]" "oe" str t))
       (str (replace-regexp-in-string "[ß]" "ss" str t))
       ;; Replace all characters except alphabets, numbers and
       ;; parentheses with spaces.
       (str (replace-regexp-in-string "[^[:alnum:]()]" " " str))
       ;; On emacs 24.5, multibyte punctuation characters like ":"
       ;; are considered as alphanumeric characters! Below evals to
       ;; non-nil on emacs 24.5:
       ;;   (string-match-p "[[:alnum:]]+" ":")
       ;; So replace them with space manually..
       (str (if (version< emacs-version "25.0")
                (let ((multibyte-punctuations-str ":")) ;String of multibyte punctuation chars
                  (replace-regexp-in-string (format "[%s]" multibyte-punctuations-str) " " str))
              str))
       ;; Remove leading and trailing whitespace.
       (str (replace-regexp-in-string "\\(^[[:space:]]*\\|[[:space:]]*$\\)" "" str))
       ;; Replace 2 or more spaces with a single space.
       (str (replace-regexp-in-string "[[:space:]]\\{2,\\}" " " str))
       ;; Replace parentheses with double-hyphens.
       (str (replace-regexp-in-string "\\s-*([[:space:]]*\\([^)]+?\\)[[:space:]]*)\\s-*" " -\\1- " str))
       ;; Remove any remaining parentheses character.
       (str (replace-regexp-in-string "[()]" "" str))
       ;; Replace spaces with hyphens.
       (str (replace-regexp-in-string " " "-" str))
       ;; Remove leading and trailing hyphens.
       (str (replace-regexp-in-string "\\(^[-]*\\|[-]*$\\)" "" str)))
  str)
)	  

org-hugo-slug from ox-hugo was very similar. I removed for example the lowercase-part and added the umlaut-part which still contains a minor uppercase-bug. Drop me a line if you can fix this.

And here comes the main function:

(defun my-id-get-or-generate()
"Returns the ID property if set or generates and returns a new one if not set.
 The generated ID is stripped off potential progress indicator cookies and
 sanitized to get a slug. Furthermore, it is prepended with an ISO date-stamp
 if none was found before."
    (interactive)
        (when (not (org-id-get))
            (progn
    	       (let* (
	              ;; retrieve heading string
    	              (my-heading-text (nth 4 (org-heading-components)))
		      ;; remove progress indicators like "[2/7]" or "[25%]"
		      (my-heading-text (replace-regexp-in-string "[[][0-9%/]+[]] " "" my-heading-text))
		      ;; get slug from heading text
                      (new-id (my-generate-sanitized-alnum-dash-string my-heading-text))
    	             )
                   (when (not (string-match "[12][0-9][0-9][0-9]-[01][0-9]-[0123][0-9]-.+" new-id))
                           ;; only if no ISO date-stamp is found at the beginning of the new id:
        	           (setq new-id (concat (format-time-string "%Y-%m-%d-") new-id)))
                   (org-set-property "ID" new-id)
                   )
                 )
        )
	(kill-new (org-id-get));; put ID in kill-ring
	(org-id-get);; retrieve the current ID in any case as return value
)

;(bind-key (kbd "I") #'my-id-get-or-generate my-map) ;; -> my binding	  

You can find the latest versions of the functions in my Emacs configuration.

I mapped it to my-map I which results in C-c C-, I which is relatively easy to type. This results in human-readable, unique :ID: properties like 2019-11-16-Creating-Unique-ID-Properties for this heading.

Here is a screencast showing the function in action:

You can see, that my link workflow now looks like this:

  1. Go to target heading.
    • It doesn't have to be the heading line.
  2. Get the ID by invoking my-id-get-or-generate().
    • If the heading did not have an :ID: property, it gets generated.
  3. Go to the spot where the link to the target should be created.
  4. Type id: followed by the ID via pasting from the kill-ring.

While generating the whole id: link including the ID could be easily automated, I prefer it this way. In many cases, I just need the raw ID instead of the internal link (org-depend).

Ideas for the Future

While this being a snappy workflow for now, I can think of further functionality here. For example, how about fully bi-directional ID links between source and target? This way, I would know exactly what is linking to my current heading. Those back-links could be automatically stored in special properties, for example.

What is also on my mind currently is a neat workflow for easy to use task planning using either org-depend or org-edna. Linking project tasks according their dependencies is labor-intense and error-prone. The ideas are there, the concept almost finished but my inability of finding the proper elisp functions to implement it is a hindrance. If you would like to volunteer, drop me a line. I'm convinced that this won't be much effort/lines-of-code and it will provide an easy to use workflow for planning even advanced projects.

Update: Please do read this article about org-super-links which provides back-links so that you get bi-directional links between headings. Furthermore, this feature request is a proposal for my idea on improving dependencies between headings.


Related articles that link to this one:

Comment via email (persistent) or via Disqus (ephemeral) comments below: