> >> Also, update the commit message.
> >
> > I updated the commit message. Are manual updates supposed to go into
> > the commit message?
>
> Yes. Manual is just anther source file :)
>

Done.

> > Subject: [PATCH] lisp/ox-html: New export option for file links
> > ...
> > * lisp/ox-publish (org-publish-file-relative-name): Use
>
> lisp/ox-publish.el
>

Fixed.

> > +#+begin_example
> > +"<a href=\"https://orgmode.org/contribute\";>Contributing to Org</a>"
> > +#+end_example
>
> Maybe just
> <a href=....> without all those Elisp escapes.
>

Done.

> > --- a/lisp/ox-html.el
> > +++ b/lisp/ox-html.el
> > @@ -44,6 +44,7 @@
> >  ;;; Function Declarations
> >
> >  (declare-function htmlize-region "ext:htmlize" (beg end))
> > +(declare-function org-export-file-relative-name-maybe "ox-publish" 
> > (filename base-directory))
>
> Declare is no longer needed here.
>

Done.

> > diff --git a/lisp/ox-publish.el b/lisp/ox-publish.el
> > index d51d68f31..27c1a3086 100644
> > --- a/lisp/ox-publish.el
> > +++ b/lisp/ox-publish.el
> > @@ -45,6 +45,7 @@
> >  (require 'ox)
> >
> >  (declare-function org-at-heading-p "org" (&optional _))
> > +(declare-function org-export-file-relative-name-maybe "ox-publish" 
> > (filename base-directory))
>
> Declare is no longer needed here.
>

Done.

> --
> Ihor Radchenko // yantar92,
> Org mode maintainer,
> Learn more about Org mode at <https://orgmode.org/>.
> Support Org development at o<https://liberapay.com/org-mode>,
> or support my work at <https://liberapay.com/yantar92>
From 3fd8c5460c7ea662925bdadb57aa251df54ec072 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: New export option for file links

* lisp/ox-html.el (org-html-base-directory): New export option
for file links.
(org-html--create-file-link-path): Create file link paths for
`org-html-link'.
(org-html-link): Have `org-html--create-file-link-path' create
file link paths.

* testing/lisp/test-ox-html.el (test-ox-html-create-test-link-element):
Helper function for `org-html-link' tests.
(ox-html/test-org-html-base-directory): New tests for
`org-html-link'.
(ox-html/test-link-home-and-use-abs-url/no-base-directory):
New tests for `org-html-link'.
(ox-html/test-org-html-base-directory/with-base-directory-set):
New tests for `org-html-link'.

* lisp/ox.el (org-export-file-relative-name-maybe):
Convert filename into a relative filename if it is in a given
base-directory.

* lisp/ox-publish.el (org-publish-file-relative-name): Use
`org-export-file-relative-name-maybe` to convert filenames
into relative file names.

* doc/org-manual.org (HTML): Document the new export
option.
---
 doc/org-manual.org           |  32 ++++-
 etc/ORG-NEWS                 |  27 ++++
 lisp/ox-html.el              |  52 +++++---
 lisp/ox-publish.el           |   7 +-
 lisp/ox.el                   |  11 ++
 testing/lisp/test-ox-html.el | 236 +++++++++++++++++++++++++++++++++++
 6 files changed, 342 insertions(+), 23 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index fa0c71c30..bd05fe419 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -13570,6 +13570,13 @@ settings described in [[*Export Settings]].
   #+vindex: org-html-link-up
   The URL for the up link of exported HTML pages (~org-html-link-up~).
 
+- =HTML_BASE_DIRECTORY= ::
+
+  #+cindex: @samp{HTML_BASE_DIRECTORY}, keyword
+  #+vindex: org-html-base-directory
+  The directory of the project that you intend to
+  export (~org-html-base-directory~).
+
 - =HTML_MATHJAX= ::
 
   #+cindex: @samp{HTML_MATHJAX}, keyword
@@ -13826,8 +13833,29 @@ location, then the converted links should work without any further
 manual intervention.  However, to disable this automatic path
 translation, set ~org-html-link-org-files-as-html~ to ~nil~.  When
 disabled, the HTML export backend substitutes the ID-based links in
-the HTML output.  For more about linking files when publishing to
-a directory, see [[*Publishing links]].
+the HTML output.
+
+#+vindex: org-html-base-directory
+~org-html-base-directory~ can be used to set the directory of
+the project that you intend to export. When used in conjunction
+with ~org-html-link-home~ and ~org-html-link-use-abs-url~, file
+links to files in ~org-html-base-directory~ will have the value
+of ~org-html-link-home~ prepended to them.  For example, when
+~org-html-base-directory~ is set to =~/my/project=, ~org-html-link-home~
+is set to =https://orgmode.org= and ~org-html-link-use-abs-url~ is set to ~t~,
+~[[~/my/project/contribute][Contributing to Org]]~ will be exported as
+
+#+begin_example
+<a href="https://orgmode.org/contribute";>Contributing to Org</a>
+#+end_example
+
+When ~:base-directory~ and ~org-html-base-directory~ are both set,
+~:base-directory~ takes precedence, i.e., the value of
+~org-html-base-directory~ will be ignored.
+
+~org-html-base-directory~ is the equivalent of the ~:base-directory~
+key of ~org-publish-project-alist~. For more about linking files when
+publishing to a directory, see [[*Publishing links]].
 
 Org files can also have special directives to the HTML export
 backend.  For example, by using =#+ATTR_HTML= lines to specify new
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 2e9e8e35b..f5fe7e2e9 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -260,6 +260,33 @@ non-org-mode files, or any other place where org can not use the
 link context to create a description.  See its docstring for more
 information.
 
+*** New export option ~org-html-base-directory~
+
+This export option, nil by default, allows you to set the directory
+of the project that you intend to export.  This variable is the
+equivalent of the ~:base-directory~ key of ~org-publish-project-alist~.
+It is intended for people who use ox-html simply for exporting as
+opposed to publishing.  When used in conjunction with ~org-html-link-home~
+and ~org-html-link-use-abs-url~, file links to files in ~org-html-base-directory~
+will have the value of ~org-html-link-home~ prepended to them.  For example,
+when ~org-html-base-directory~ is set to =~/my/project=, ~org-html-link-home~
+is set to =https://orgmode.org= and ~org-html-link-use-abs-url~ is set to ~t~,
+~[[~/my/project/contribute][Contributing to Org]]~ will be exported as
+
+#+begin_example
+<a href="https://orgmode.org/contribute";>Contributing to Org</a>
+#+end_example
+
+When ~:base-directory~ and ~org-html-base-directory~ are both set,
+~:base-directory~ takes precedence, i.e., the value of
+~org-html-base-directory~ will be ignored.
+
+The option can be customized either by
+
+1. setting the global variable ~org-html-base-directory~
+2. setting the file local keyword =HTML_BASE_DIRECTORY=
+
+
 ** New functions and changes in function arguments
 
 # This also includes changes in function behavior from Elisp perspective.
diff --git a/lisp/ox-html.el b/lisp/ox-html.el
index 74fd198e2..e9ac48d20 100644
--- a/lisp/ox-html.el
+++ b/lisp/ox-html.el
@@ -122,6 +122,7 @@
     (:html-link-use-abs-url nil "html-link-use-abs-url" org-html-link-use-abs-url)
     (:html-link-home "HTML_LINK_HOME" nil org-html-link-home)
     (:html-link-up "HTML_LINK_UP" nil org-html-link-up)
+    (:html-base-directory "HTML_BASE_DIRECTORY" nil org-html-base-directory)
     (:html-mathjax "HTML_MATHJAX" nil "" space)
     (:html-equation-reference-format "HTML_EQUATION_REFERENCE_FORMAT" nil org-html-equation-reference-format t)
     (:html-postamble nil "html-postamble" org-html-postamble)
@@ -1501,6 +1502,17 @@ ignored."
   :group 'org-export-html
   :type 'string)
 
+(defcustom org-html-base-directory nil
+  "Directory of the project that you intend to export.
+This variable is the equivalent of the `:base-directory'
+key of `org-publish-project-alist'.  If `:base-directory'
+is also set, the value of this variable will be ignored."
+  :group 'org-export-html
+  :package-version '(Org . "10.0")
+  :type '(choice (const :tag "No base directory" nil)
+		 (string :tag "Base directory of the project"))
+  :safe #'string-or-null-p)
+
 ;;;; Template :: Scripts
 
 (defcustom org-html-head-include-scripts nil
@@ -3325,6 +3337,28 @@ 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,
+prepend `file' protocol to absolute file name.  INFO
+should be the export options, as a plist."
+  (let* ((html-base-directory (or (plist-get info :base-directory)
+                                  (plist-get info :html-base-directory)))
+         (maybe-file-relative-name (org-export-file-relative-name-maybe
+                              raw-path html-base-directory))
+         (home (and (plist-get info :html-link-home)
+                    (org-trim (plist-get info :html-link-home)))))
+    ;; Possibly prepend `:html-link-home' to relative file
+    ;; name.
+    (if (and home
+             (plist-get info :html-link-use-abs-url)
+             (not (file-name-absolute-p maybe-file-relative-name)))
+        (file-name-concat
+         (file-name-as-directory home)
+         maybe-file-relative-name)
+      (org-export-file-uri maybe-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 +3384,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/lisp/ox-publish.el b/lisp/ox-publish.el
index d51d68f31..5fe736b09 100644
--- a/lisp/ox-publish.el
+++ b/lisp/ox-publish.el
@@ -1206,12 +1206,7 @@ references with `org-export-get-reference'."
 INFO is the plist containing the current export state.  The
 function does not change relative file names."
   (let ((base (plist-get info :base-directory)))
-    (if (and base
-	     (file-name-absolute-p filename)
-	     (file-in-directory-p filename base))
-	(file-relative-name filename base)
-      filename)))
-
+    (org-export-file-relative-name-maybe filename base)))
 
 
 ;;; Caching functions
diff --git a/lisp/ox.el b/lisp/ox.el
index 90c853d76..bd63423bc 100644
--- a/lisp/ox.el
+++ b/lisp/ox.el
@@ -4742,6 +4742,17 @@ downloaded copy.  Otherwise, return unchanged LINK."
                  (org-element-property :raw-link link))))))
   link)
 
+(defun org-export-file-relative-name-maybe (filename base-directory)
+  "Maybe convert FILENAME to a relative filename.
+FILENAME will only be converted to a relative filename
+if it is in BASE-DIRECTORY.  Otherwise, FILENAME will simply
+be returned."
+  (if (and base-directory
+       (file-name-absolute-p filename)
+	   (file-in-directory-p filename base-directory))
+	(file-relative-name filename base-directory)
+      filename))
+
 ;;;; For References
 ;;
 ;; `org-export-get-reference' associate a unique reference for any
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index 717838105..85d038cad 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -1260,4 +1260,240 @@ entirely."
     (org-export-string-as "" 'html nil
                           '( :html-doctype "xhtml5"
                              :html-klipsify-src t)))))
+
+;;; Rendering Links
+
+(defun test-ox-html-create-test-link-element (test-link-path
+                                              &optional test-desc)
+  "Helper function for `ox-html-link' tests.
+Uses TEST-LINK-PATH and TEST-DESC to create a file link.
+That file link will then be converted into an org-element."
+  (let ((test-desc (org-link-make-string test-link-path test-desc)))
+    (org-test-with-temp-text test-desc
+      (org-element-link-parser))))
+
+(ert-deftest ox-html/test-org-html-base-directory ()
+  "Test `org-html-base-directory'."
+  ;; file is not in html-base-directory
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-desc "Install Emacs")
+           (test-dir (file-name-parent-directory buffer-file-name))
+           (test-home "https://www.example.com";)
+           (test-file-name "/en/install-emacs-on-android")
+           (test-link-element (test-ox-html-create-test-link-element
+                               test-file-name
+                               test-desc))
+           (expected-link-path (format "file://%s" test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :html-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))))
+
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-dir (file-name-parent-directory buffer-file-name))
+           (test-home "https://www.notabug.com";)
+           (test-file-name "/examples/babel.html")
+           (test-link-element (test-ox-html-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 :html-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))))
+
+  ;; file is in html-base-directory
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-desc "Contributing to Org")
+           (test-home "https://orgmode.org";)
+           (test-dir (file-name-parent-directory buffer-file-name))
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name
+                               test-desc))
+           (expected-link-path
+            (file-name-concat test-home test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :html-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))))
+
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-home "https://mywebsite.com";)
+           (test-dir (file-name-parent-directory buffer-file-name))
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name))
+           (expected-link-path
+            (file-name-concat test-home test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  expected-link-path))
+           (test-info (list :html-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)))))
+
+(ert-deftest ox-html/test-link-home-and-use-abs-url/no-base-directory ()
+  "Test `org-html-link-use-abs-url' with `org-html-link-home'."
+  (ert-with-temp-file test-link-path
+    (let* ((test-desc "Contributing to Org")
+           (test-home "https://orgmode.org";)
+           (test-link-element (test-ox-html-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 (test-ox-html-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)))))
+
+(ert-deftest ox-html/test-org-html-base-directory/with-base-directory-set ()
+  "Assert `:base-directory' has precendence over `org-html-base-directory'."
+  ;; file is in `:base-directory'
+  ;; and is not in html-base-directory
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-desc "Contributing to Org")
+           (test-home "https://orgmode.org";)
+           (test-base-directory (file-name-parent-directory
+                                 buffer-file-name))
+           (test-html-base-directory "/my/project/directory")
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name
+                               test-desc))
+           (expected-link-path
+            (file-name-concat test-home test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :base-directory test-base-directory
+                            :html-base-directory test-html-base-directory
+                            :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))))
+
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-home "https://mywebsite.com";)
+           (test-base-directory (file-name-parent-directory
+                                 buffer-file-name))
+           (test-html-base-directory "~/mywebsite")
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name))
+           (expected-link-path
+            (file-name-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-base-directory
+                            :html-base-directory test-html-base-directory
+                            :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))))
+
+  ;; file is not in `:base-directory'
+  ;; and is in html-base-directory
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-desc "Contributing to Org")
+           (test-home "https://orgmode.org";)
+           (test-base-directory "/org/base")
+           (test-html-base-directory (file-name-parent-directory
+                                   buffer-file-name))
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name
+                               test-desc))
+           (expected-link-path (format "file://%s" buffer-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :base-directory test-base-directory
+                            :html-base-directory test-html-base-directory
+                            :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))))
+
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-home "https://mywebsite.com";)
+           (test-base-directory "~/website/publish-dir")
+           (test-html-base-directory (file-name-parent-directory
+                                   buffer-file-name))
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name))
+           (expected-link-path (format "file://%s" buffer-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  expected-link-path))
+           (test-info (list :base-directory test-base-directory
+                            :html-base-directory test-html-base-directory
+                            :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