customize broken links when publishing org files
1. The issue
I use org-roam to connect my notes/documents and it uses id to link them.
When I decide I want to publish a file to my blog, I use org's default publishing commands. I have some custom setup for it in my emacs dot files. However because some of the internal links link to files that are not published, for example \[[xxfj-el....][org mode]\], I always get \[BROKEN LINK xxxxxx\] in my exported posts like
org mode plot data and save to file below given that I have set org-export-with-broken-links
to 'mark
.
This is not very nice to say the least. And this is actually the best you can get with the three options for org-export-with-broken-links
, which are nil, non-nil and mark.
With nil, a broken link fails the export. non-nil simply ignores the link. And mark produces the above screenshot.
However, I really want this to be able to ignore the org id and just show the text I put in after the id, which in the above should be org mode.
2. The solution
I did a bit digging and came up with this solution. It works pretty well.
All it does is adding a new option to org-export-with-broken-links
called label
and provide its content in org-export-resolve-id-link
.
See the commit for a diff view of the changes I did in the following functions.
(defun org-export-resolve-id-link (link info) "Return headline referenced as LINK destination. INFO is a plist used as a communication channel. Return value can be the headline element matched in current parse tree or a file name. Assume LINK type is either \"id\" or \"custom-id\". Throw an error if no match is found." (let ((id (org-element-property :path link)) (contents (car (last link)))) ;; First check if id is within the current parse tree. (or (org-element-map (plist-get info :parse-tree) 'headline (lambda (headline) (when (or (equal (org-element-property :ID headline) id) (equal (org-element-property :CUSTOM_ID headline) id)) headline)) info 'first-match) ;; Otherwise, look for external files. (cdr (assoc id (plist-get info :id-alist))) (signal 'org-link-broken (list contents)))))
(defun org-export-resolve-id-link (link info) "Return headline referenced as LINK destination. INFO is a plist used as a communication channel. Return value can be the headline element matched in current parse tree or a file name. Assume LINK type is either \"id\" or \"custom-id\". Throw an error if no match is found." (let ((id (org-element-property :path link)) (contents (car (last link ))) ) ;; First check if id is within the current parse tree. (or (org-element-map (plist-get info :parse-tree) 'headline (lambda (headline) (when (or (equal (org-element-property :ID headline) id) (equal (org-element-property :CUSTOM_ID headline) id)) headline)) info 'first-match) ;; Otherwise, look for external files. (cdr (assoc id (plist-get info :id-alist))) (signal 'org-link-broken (list contents)))))
(defun org-export-data (data info) "Convert DATA into current back-end format. DATA is a parse tree, an element or an object or a secondary string. INFO is a plist holding export options. Return a string." (or (gethash data (plist-get info :exported-data)) ;; Handle broken links according to ;; `org-export-with-broken-links'. (cl-macrolet ((broken-link-handler (&rest body) `(condition-case err (progn ,@body) (org-link-broken (pcase (plist-get info :with-broken-links) (`nil (user-error "Unable to resolve link: %S" (nth 1 err))) (`mark (org-export-data (format "[BROKEN LINK: %s]" (nth 1 err)) info)) (`label (org-export-data (progn (format "%s" (nth 1 err))) info)) (_ nil)))))) (let* ((type (org-element-type data)) (parent (org-export-get-parent data)) (results (cond ;; Ignored element/object. ((memq data (plist-get info :ignore-list)) nil) ;; Raw code. ((eq type 'raw) (car (org-element-contents data))) ;; Plain text. ((eq type 'plain-text) (org-export-filter-apply-functions (plist-get info :filter-plain-text) (let ((transcoder (org-export-transcoder data info))) (if transcoder (funcall transcoder data info) data)) info)) ;; Secondary string. ((not type) (mapconcat (lambda (obj) (org-export-data obj info)) data "")) ;; Element/Object without contents or, as a special ;; case, headline with archive tag and archived trees ;; restricted to title only. ((or (not (org-element-contents data)) (and (eq type 'headline) (eq (plist-get info :with-archived-trees) 'headline) (org-element-property :archivedp data))) (let ((transcoder (org-export-transcoder data info))) (or (and (functionp transcoder) (broken-link-handler (funcall transcoder data nil info))) ;; Export snippets never return a nil value so ;; that white spaces following them are never ;; ignored. (and (eq type 'export-snippet) "")))) ;; Element/Object with contents. (t (let ((transcoder (org-export-transcoder data info))) (when transcoder (let* ((greaterp (memq type org-element-greater-elements)) (objectp (and (not greaterp) (memq type org-element-recursive-objects))) (contents (mapconcat (lambda (element) (org-export-data element info)) (org-element-contents (if (or greaterp objectp) data ;; Elements directly containing ;; objects must have their indentation ;; normalized first. (org-element-normalize-contents data ;; When normalizing first paragraph ;; of an item or ;; a footnote-definition, ignore ;; first line's indentation. (and (eq type 'paragraph) (memq (org-element-type parent) '(footnote-definition item)) (eq (car (org-element-contents parent)) data) (eq (org-element-property :pre-blank parent) 0))))) ""))) (broken-link-handler (funcall transcoder data (if (not greaterp) contents (org-element-normalize-string contents)) info))))))))) ;; Final result will be memoized before being returned. (puthash data (cond ((not results) "") ((memq type '(nil org-data plain-text raw)) results) ;; Append the same white space between elements or objects ;; as in the original buffer, and call appropriate filters. (t (org-export-filter-apply-functions (plist-get info (intern (format ":filter-%s" type))) (let ((blank (or (org-element-property :post-blank data) 0))) (if (eq (org-element-class data parent) 'object) (concat results (make-string blank ?\s)) (concat (org-element-normalize-string results) (make-string blank ?\n)))) info))) (plist-get info :exported-data))))))