This patch is for a very confusing edge case that came up when I was
working on a verbatim link export option for ox-md. I was trying to see
what part of org-html-link's code needed to be extracted for `org-md-link'.
For a link like:

[[/tmp/tmp-orgtest/emacs][Emacs]]

you would expect org-html-link
to return:

<a href=\"file:///tmp/tmp-orgtest/emacs">Contributing to Org</a>

when `org-html-link-use-abs-url' is set to t,`org-html-link-home' is set to
a non-nil value and the ':base-directory` key of
`org-publish-project-alist' has not been set. Instead `org-html-link'
returns:

<a href=\"https://www.example.com/file:///en/install-emacs-on-android\";>install
Emacs</a></p>.

As it seems `org-html-link-use-abs-url' and`org-html-link-home' are mostly
intended for people who use org-publish, this likely never came up in the
wild. But as I would like to reuse these customs for verbatim link export,
this needed to be fixed.
From 4cfe916a301342de92846eef118f8f8137b7c89f Mon Sep 17 00:00:00 2001
From: ApollonDeParnasse <[email protected]>
Date: Tue, 30 Jun 2026 16:07:54 -0500
Subject: [PATCH] lisp/ox-html.el: Fix org-html-link file path creation bug

* lisp/ox-html.el (org-html-link): Use
`org-html--create-file-link-path' in order
to create the link path of a file link.
(org-html--create-file-link-path): Create the
path of a file link.

* testing/lisp/test-ox-html.el
(ox-html/test-org-html-link-use-abs-url): New
tests for `org-html-link'.
---
 lisp/ox-html.el              |  36 ++++---
 testing/lisp/test-ox-html.el | 190 +++++++++++++++++++++++++++++++++++
 2 files changed, 211 insertions(+), 15 deletions(-)

diff --git a/lisp/ox-html.el b/lisp/ox-html.el
index 74fd198e2..3aff83162 100644
--- a/lisp/ox-html.el
+++ b/lisp/ox-html.el
@@ -3325,6 +3325,24 @@ images, set it to:
 	       info nil 'link)
 	     (= link-count 1))))))
 
+(defun org-html--create-file-link-path (raw-path info)
+  "Convert RAW-PATH into a HTML file link path.
+During publishing, turn absolute file names
+belonging to base directory into relative
+file names.  Otherwise,	append `file' protocol
+to absolute file name.  INFO should be the
+export options, as a plist."
+  (let* ((file-relative-name (org-publish-file-relative-name raw-path info))
+         (home (and (plist-get info :html-link-home)
+                    (org-trim (plist-get info :html-link-home)))))
+    ;; Possibly append `:html-link-home' to relative file
+    ;; name.
+    (if (and home
+             (plist-get info :html-link-use-abs-url)
+             (not (file-name-absolute-p file-relative-name)))
+        (file-name-concat (file-name-as-directory home) file-relative-name)
+      (org-export-file-uri file-relative-name))))
+
 (defun org-html-link (link desc info)
   "Transcode a LINK object from Org to HTML.
 DESC is the description part of the link, or the empty string.
@@ -3350,22 +3368,10 @@ INFO is a plist holding contextual information.  See
 	 (path
 	  (cond
 	   ((string= "file" type)
-	    ;; During publishing, turn absolute file names belonging
-	    ;; to base directory into relative file names.  Otherwise,
-	    ;; append "file" protocol to absolute file name.
-	    (setq raw-path
-		  (org-export-file-uri
-		   (org-publish-file-relative-name raw-path info)))
-	    ;; Possibly append `:html-link-home' to relative file
-	    ;; name.
-	    (let ((home (and (plist-get info :html-link-home)
-			     (org-trim (plist-get info :html-link-home)))))
-	      (when (and home
-			 (plist-get info :html-link-use-abs-url)
-			 (not (file-name-absolute-p raw-path)))
-		(setq raw-path (concat (file-name-as-directory home) raw-path))))
 	    ;; Maybe turn ".org" into ".html".
-	    (setq raw-path (funcall link-org-files-as-html-maybe raw-path info))
+	    (setq raw-path (funcall link-org-files-as-html-maybe
+                                    (org-html--create-file-link-path raw-path info)
+                                    info))
 	    ;; Add search option, if any.  A search option can be
 	    ;; relative to a custom-id, a headline title, a name or
 	    ;; a target.
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index 717838105..1d09b2cd7 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -1260,4 +1260,194 @@ entirely."
     (org-export-string-as "" 'html nil
                           '( :html-doctype "xhtml5"
                              :html-klipsify-src t)))))
+
+
+(ert-deftest ox-html/test-org-html-link-use-abs-url ()
+  "Test `org-html-link-use-abs-url'."
+  (cl-flet* ((create-test-link-element (test-link-path
+                                        &optional test-desc)
+               (let ((test-desc (if test-desc
+                                    (format "[[file:%s][%s]]" test-link-path test-desc)
+                                  (format "[[file:%s]]" test-link-path))))
+                 (org-test-with-temp-text test-desc
+                   (org-element-link-parser)))))
+
+    ;; file is not in publishing directory
+    (ert-with-temp-directory test-dir
+      (let* ((test-home "https://www.example.com";)
+             (test-file-name "/en/install-emacs-on-android")
+             (test-link-element (create-test-link-element
+                                 test-file-name
+                                 nil))
+             (expected-link-path (format "file://%s" test-file-name))
+             (expected-link (format "<a href=\"%s\">%s</a>"
+                                    expected-link-path
+                                    expected-link-path))
+             (test-info (list :base-directory test-dir))
+             (actual-link (org-html-link
+                           test-link-element
+                           nil
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    (ert-with-temp-directory test-dir
+      (let* ((test-home "https://www.notabug.com";)
+             (test-file-name "/examples/babel.html")
+             (test-link-element (create-test-link-element test-file-name))
+             (expected-link-path (format "file://%s" test-file-name))
+             (expected-link (format "<a href=\"%s\">%s</a>"
+                                    expected-link-path
+                                    expected-link-path))
+             (test-info (list :base-directory test-dir
+                              :test-home test-home))
+             (actual-link (org-html-link
+                           test-link-element
+                           nil
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    ;; with `org-html-link-use-abs-url'
+    (ert-with-temp-directory test-dir
+      (let* ((test-desc "Install Emacs")
+             (test-home "https://www.example.com";)
+             (test-file-name "/en/install-emacs-on-android")
+             (test-link-element (create-test-link-element
+                                 test-file-name
+                                 test-desc))
+             (expected-link (format "<a href=\"file://%s\">%s</a>"
+                                    test-file-name
+                                    test-desc))
+             (test-info (list :base-directory test-dir
+                              :test-home test-home))
+             (actual-link (org-html-link
+                           test-link-element
+                           test-desc
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    (ert-with-temp-directory test-dir
+      (let* ((test-home "https://www.notabug.com";)
+             (test-file-name "/examples/babel.html")
+             (test-link-element (create-test-link-element test-file-name))
+             (expected-link-path (format "file://%s" test-file-name))
+             (expected-link (format "<a href=\"%s\">%s</a>"
+                                    expected-link-path
+                                    expected-link-path))
+             (test-info (list :base-directory test-dir
+                              :test-home test-home))
+             (actual-link (org-html-link
+                           test-link-element
+                           nil
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    ;; file is in publishing directory
+    (ert-with-temp-file test-link-path
+      (let* ((test-desc "Contributing to Org")
+             (test-home "https://orgmode.org";)
+             (test-dir (file-name-parent-directory test-link-path))
+             (test-file-name (file-name-nondirectory test-link-path))
+             (test-link-element (create-test-link-element
+                                 test-link-path
+                                 test-desc))
+             (expected-link (format "<a href=\"%s\">%s</a>"
+                                    test-file-name
+                                    test-desc))
+             (test-info (list :base-directory test-dir))
+             (actual-link (org-html-link
+                           test-link-element
+                           test-desc
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    (ert-with-temp-file test-link-path
+      (let* ((test-home "https://mywebsite.com";)
+             (test-dir (file-name-parent-directory test-link-path))
+             (test-file-name (file-name-nondirectory test-link-path))
+             (test-link-element (create-test-link-element test-link-path))
+             (expected-link-path (concat test-home "/" test-file-name))
+             (expected-link (format "<a href=\"%s\">%s</a>"
+                                    test-file-name
+                                    test-file-name))
+             (test-info (list :base-directory test-dir))
+             (actual-link (org-html-link
+                           test-link-element
+                           nil
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    ;; with `org-html-link-use-abs-url'
+    (ert-with-temp-file test-link-path
+      (let* ((test-desc "Contributing to Org")
+             (test-home "https://orgmode.org";)
+             (test-dir (file-name-parent-directory test-link-path))
+             (test-file-name (file-name-nondirectory test-link-path))
+             (test-link-element (create-test-link-element
+                                 test-link-path
+                                 test-desc))
+             (expected-link (format "<a href=\"%s/%s\">%s</a>"
+                                    test-home
+                                    test-file-name
+                                    test-desc))
+             (test-info (list :base-directory test-dir
+                              :html-link-use-abs-url t
+                              :html-link-home test-home))
+             (actual-link (org-html-link
+                           test-link-element
+                           test-desc
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    (ert-with-temp-file test-link-path
+      (let* ((test-home "https://mywebsite.com";)
+             (test-dir (file-name-parent-directory test-link-path))
+             (test-file-name (file-name-nondirectory test-link-path))
+             (test-link-element (create-test-link-element test-link-path))
+             (expected-link-path (concat test-home "/" test-file-name))
+             (expected-link (format "<a href=\"%s\">%s</a>"
+                                    expected-link-path
+                                    expected-link-path))
+             (test-info (list :base-directory test-dir
+                              :html-link-use-abs-url t
+                              :html-link-home test-home))
+             (actual-link (org-html-link
+                           test-link-element
+                           nil
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    ;; with `org-html-link-use-abs-url' and no base-directory
+    (ert-with-temp-file test-link-path
+      (let* ((test-desc "Contributing to Org")
+             (test-home "https://orgmode.org";)
+             (test-link-element (create-test-link-element
+                                 test-link-path
+                                 test-desc))
+             (expected-link-path (format  "file://%s" test-link-path))
+             (expected-link (format "<a href=\"%s\">%s</a>"
+                                    expected-link-path
+                                    test-desc))
+             (test-info (list :html-link-use-abs-url t
+                              :html-link-home test-home))
+             (actual-link (org-html-link
+                           test-link-element
+                           test-desc
+                           test-info)))
+        (should (string-equal actual-link expected-link))))
+
+    (ert-with-temp-file test-link-path
+      (let* ((test-home "https://mywebsite.com";)
+             (test-link-element (create-test-link-element test-link-path))
+             (expected-link-path (format  "file://%s" test-link-path))
+             (expected-link (format "<a href=\"%s\">%s</a>"
+                                    expected-link-path
+                                    expected-link-path))
+             (test-info (list :html-link-use-abs-url t
+                              :html-link-home test-home))
+             (actual-link (org-html-link
+                           test-link-element
+                           nil
+                           test-info)))
+        (should (string-equal actual-link expected-link))))))
+
 ;;; test-ox-html.el ends here
-- 
2.54.0

Reply via email to