This patch will close https://list.orgmode.org/orgmode/[email protected]/.
This is mostly useful for people who export their org-documents to HTML or markdown. However, I have added an open function that will request a base URL so that users will still have the option to open these URLs.
From 78b49b2db1db62227c2c6e1a59a562e2837f7656 Mon Sep 17 00:00:00 2001 From: ApollonDeParnasse <[email protected]> Date: Fri, 22 May 2026 06:24:21 -0500 Subject: [PATCH] ol.el: New link type for relative urls Links that start with 'rel-url' followed by a colon will be treated as relative urls. When these links are exported to markdown or HTML documents, they are properly converted into relative URL links. I have added an open function that will request a base URL when called so that users will still have the option to open these URLs as they would with absolute URLs. =* lisp/ol.el= (org-link-parameters): New link type named 'rel-url' for relative urls. (org-link-open-relative-url): Function for the `:follow' parameter for 'rel-url' type links. (org-link-export-relative-url): Function for the `:export' parameter for 'rel-url' type links. =* testing/lisp/test-ol.el= (test-org-link/plain-link-re): New test cases for 'rel-url' type links. =* testing/lisp/test-org-element.el= (test-org-element-parse-relative-links-asserter): Asserter for `test-org-element/link-parser/relative-urls'. (test-org-element/link-parser/relative-urls): Assert org-element properly parses 'rel-url' type links. =* testing/lisp/test-ox-html.el= (test-ox-html-relative-links-asserter): Asserter for `ox-html/relative-links'. (ox-html/relative-links): Assert `org-link-export-relative-url' converts 'rel-url' type links into the proper HTML link format. =* testing/lisp/test-ox-md.el= (test-ox-md-relative-links-asserter): Asserter for `ox-md/relative-links'. (ox-md/relative-links): Assert `org-link-export-relative-url' converts 'rel-url' type links into the proper markdown link format. --- doc/org-manual.org | 5 +++ etc/ORG-NEWS | 12 ++++++++ lisp/ol.el | 26 +++++++++++++++- testing/lisp/test-ol.el | 18 ++++++++++- testing/lisp/test-org-element.el | 34 ++++++++++++++++++++- testing/lisp/test-ox-html.el | 49 ++++++++++++++++++++++++++++++ testing/lisp/test-ox-md.el | 52 +++++++++++++++++++++++++++++++- 7 files changed, 192 insertions(+), 4 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 90a3880cf..f5c82dc3f 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -3403,6 +3403,9 @@ Here is the full set of built-in link types: For more information, see [[info:emacs#Name Help][Name Help]] and [[info:elisp#Documentation Groups][Documentation Groups]]. +- =rel-url= :: + + Relative URL links. For =file:= and =id:= links, you can additionally specify a line number, or a text search string, separated by =::=. In Org files, you @@ -3449,6 +3452,8 @@ options: | shell | =shell:ls *.org= (synchronous), =shell:inkscape&= (asynchronous) | | elisp | =elisp:(find-file "Elisp.org")= (Elisp form to evaluate) | | | =elisp:org-agenda= (interactive Elisp command) | +| rel-url | =rel-url:/en-US/docs/Web/HTML= | + #+cindex: VM links #+cindex: Wanderlust links diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index e62a8ec46..c3e20e5b9 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -138,6 +138,18 @@ A current limitation is that export blocks and keywords are only implemented for events and todos, and not yet for calendar-wide properties. +*** ol.el now supports relative URLs. + +Links that start with 'rel-url' followed by a colon will be treated as relative urls. +When these links are exported to markdown or HTML documents, they are properly converted +into relative URL links. For example, =[[rel-url:/en-US/docs/Web/HTML][Origin-relative URL]]= +will be exported to markdown as =[Origin-relative URL](/en-US/docs/Web/HTML)=. For HTML +documents, =[[rel-url:/en-US/docs/Web/HTML][Origin-relative URL]]= will be exported as +#+begin_example +<a href=\"/en-US/docs/Web/HTML\">Origin-relative URL</a> +#+end_example + +These links can also be opened if the user provides a base URL when requested. ** New and changed options # Changes dealing with changing default values of customizations, diff --git a/lisp/ol.el b/lisp/ol.el index 73645fb97..98d4936d5 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -88,7 +88,7 @@ (declare-function org-element-contents-begin "org-element" (node)) (declare-function org-element-contents-end "org-element" (node)) (declare-function org-property-or-variable-value "org" (var &optional inherit)) - +(declare-function url-expand-file-name "url-expand" (url &optional default)) ;;; Customization @@ -2435,6 +2435,30 @@ string\"." :follow (lambda (url arg) (browse-url (concat scheme ":" url) arg)))) +;; relative urls +(defun org-link-open-relative-url (relative-url arg) + "Open RELATIVE-URL with `browse-url'. +The user will be asked for the base url. +ARG will be passed to the browser function." + (let ((base-url (read-string "base url: "))) + (browse-url (url-expand-file-name relative-url base-url) arg))) + + +(defun org-link-export-relative-url (path description backend _) + "Export a relative url type link. +PATH is the path of the link. PATH +is the DOI name. DESCRIPTION is the description of the link, +or nil. BACKEND is a symbol representing the backend used for +export." + (let ((desc (or description path))) + (pcase backend + (`html (format "<a href=\"%s\">%s</a>" path desc)) + (`md (format "[%s](%s)" desc path)) + (_ path)))) + +(org-link-set-parameters "rel-url" + :follow #'org-link-open-relative-url + :export #'org-link-export-relative-url) ;;;; "shell" link type (defun org-link--open-shell (path _) diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el index c8ec09616..d4e2ebdbe 100644 --- a/testing/lisp/test-ol.el +++ b/testing/lisp/test-ol.el @@ -778,7 +778,23 @@ See https://github.com/yantar92/org/issues/4." (equal '("http" "//foo.com/(something)?after=parens") (test-ol-parse-link-in-text - "The <point>http://foo.com/(something)?after=parens link")))) + "The <point>http://foo.com/(something)?after=parens link"))) + (should + (equal + '("rel-url" "/Learn_web_development/Howto/Web_mechanics/What_is_a_URL") + (test-ol-parse-link-in-text + "The <point>rel-url:/Learn_web_development/Howto/Web_mechanics/What_is_a_URL link"))) + (should + (equal + '("rel-url" "worg/org-contribute.html#patches") + (test-ol-parse-link-in-text + "The <point>rel-url:worg/org-contribute.html#patches"))) + (should + (equal + '("rel-url" "~bzg/org-mode-tests/tree/master/item/local/init.el") + (test-ol-parse-link-in-text + "The <point>rel-url:~bzg/org-mode-tests/tree/master/item/local/init.el")))) + ;;; Insert Links diff --git a/testing/lisp/test-org-element.el b/testing/lisp/test-org-element.el index e5ab89ac0..2f9a35996 100644 --- a/testing/lisp/test-org-element.el +++ b/testing/lisp/test-org-element.el @@ -2628,6 +2628,38 @@ e^{i\\pi}+1=0 (lambda (link) (org-element-property :type link)) nil t nil t)))))) +(defun test-org-element-parse-relative-links-asserter (text expected-values) + "Helper function for `test-org-element/link-parser/relative-urls'. +Asserts EXPECTED-VALUES matches the actual :type, :path +and :description of link parsed in TEXT. \"<point>\" +string must be at the beginning of the link to be parsed." + (org-test-with-temp-text text + (let* ((actual-element (org-element-link-parser)) + (actual-values (list (org-element-property :type (org-element-link-parser)) + (org-element-property :path (org-element-link-parser))))) + (should (equal actual-values expected-values))))) + +;; relative links +(ert-deftest test-org-element/link-parser/relative-urls () + (test-org-element-parse-relative-links-asserter + "The <point>[[rel-url:/en/install-emacs-on-android][install Emacs]] link" + '("rel-url" "/en/install-emacs-on-android")) + (test-org-element-parse-relative-links-asserter + "The <point>[[rel-url:/orgmode/87y0ijp82r.fsf@localhost/][org-meetup notes]] link" + '("rel-url" "/orgmode/87y0ijp82r.fsf@localhost/")) + (test-org-element-parse-relative-links-asserter + "The <point>[[rel-url:software/emacs/manual/html_node/elisp/Clickable-Text.html]] link" + '("rel-url" "software/emacs/manual/html_node/elisp/Clickable-Text.html")) + (test-org-element-parse-relative-links-asserter + "The <point>[[rel-url:cgit/org-mode.git/log/]] link" + '("rel-url" "cgit/org-mode.git/log/")) + ;; Works with <> links + (test-org-element-parse-relative-links-asserter + "The <point><rel-url:api/v1/path/> link" + '("rel-url" "api/v1/path/")) + (test-org-element-parse-relative-links-asserter + "The <point><rel-url:/account/history/abc.html> link" + '("rel-url" "/account/history/abc.html"))) ;;;; Macro @@ -4209,7 +4241,7 @@ DEADLINE: <2012-03-29 thu.> SCHEDULED: <2012-03-29 thu.> CLOSED: [2012-03-29 thu (:range-type daterange :type inactive :year-start 2023 :month-start 7 :day-start 10 :hour-start 17 :minute-start 30)) nil)) - + ;;; End part is nil (should ;; Expected result: "<2023-07-10 Mon>--<2023-07-10 Mon>" diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el index 11da941ab..4380c91fa 100644 --- a/testing/lisp/test-ox-html.el +++ b/testing/lisp/test-ox-html.el @@ -1130,5 +1130,54 @@ entirely." (org-html--priority 18 nil))) ) +(defun test-ox-html-relative-links-asserter (text expected-values) + "Test asserter for `org-html-link'. +TEXT should contain links. Confirm actual +text created by `org-html-link' matches +contains the values in +EXPECTED-VALUES." + (org-test-with-temp-text (format "#+options: toc:nil\n%s" text) + (let ((export-buffer "*Test HTML Export*") + (org-export-show-temporary-export-buffer nil)) + (org-export-to-buffer 'html export-buffer) + (with-current-buffer export-buffer + (mapc (lambda (expected-value) + (should (string-match-p (regexp-quote expected-value) + (buffer-substring-no-properties + (point-min) + (point-max))))) + expected-values))))) + +(ert-deftest ox-html/relative-links () + "Test `org-html-link' relative links." + (test-ox-html-relative-links-asserter + "[[rel-url:examples/babel.org][babel org file]] +[[rel-url:examples/babel.el]]" + (list "<a href=\"examples/babel.org\">babel org file</a>" + "<a href=\"examples/babel.el\">examples/babel.el</a>")) + + (test-ox-html-relative-links-asserter + "[[rel-url:/en/install-emacs-on-android][install Emacs]] +[[rel-url:software/emacs/manual/html_node/elisp/Clickable-Text.html][Clickable Text]]" + (list "<a href=\"/en/install-emacs-on-android\">install Emacs</a>" + "<a href=\"software/emacs/manual/html_node/elisp/Clickable-Text.html\">Clickable Text</a>")) + + ;; Works with <> links also + (test-ox-html-relative-links-asserter + "[[rel-url:/orgmode/87y0ijp82r.fsf@localhost/][org-meetup notes]] +<rel-url:cgit/org-mode.git/log/> +[[rel-url:../x][a hypertext anchor]]" + (list "<a href=\"/orgmode/87y0ijp82r.fsf@localhost/\">org-meetup notes</a>" + "<a href=\"cgit/org-mode.git/log/\">cgit/org-mode.git/log/</a>" + "<a href=\"../x\">a hypertext anchor</a>")) + + (test-ox-html-relative-links-asserter + "<rel-url:/en-US/docs/Web/HTML> +[[rel-url:#absolute-urls][Jump to Absolute URLs section]] +[[rel-url:?q=url+query+string][Search for url query string]]" + (list "<a href=\"/en-US/docs/Web/HTML\">/en-US/docs/Web/HTML</a>" + "<a href=\"#absolute-urls\">Jump to Absolute URLs section</a>" + "<a href=\"?q=url+query+string\">Search for url query string</a>"))) + (provide 'test-ox-html) ;;; test-ox-html.el ends here diff --git a/testing/lisp/test-ox-md.el b/testing/lisp/test-ox-md.el index e1e1e4e2d..b499f0fc6 100644 --- a/testing/lisp/test-ox-md.el +++ b/testing/lisp/test-ox-md.el @@ -35,7 +35,7 @@ *** level2 lorem ipsum ** Footnotes -[fn:1] a footnote +[fn:1] a footnote " (let ((org-md-toplevel-hlevel 4) (export-buffer "*Test MD Export*") @@ -136,6 +136,56 @@ (should (search-forward "[babel org file](examples/babel.org)")) (should (search-forward "[babel script](examples/babel.el)")))))) +(defun test-ox-md-relative-links-asserter (text expected) + "Test asserter for `org-md-link'. +TEXT should contain links. Confirm actual +text created by `org-md-link' matches +EXPECTED text." + (org-test-with-temp-text (format "#+options: toc:nil\n%s" text) + (let ((export-buffer "*Test MD Export*") + (org-export-show-temporary-export-buffer nil) + (org-md-link-org-files-as-md nil)) + (org-export-to-buffer 'md export-buffer) + (with-current-buffer export-buffer + (should (string-equal (org-trim (buffer-substring-no-properties + (point-min) (point-max))) + expected)))))) + +(ert-deftest ox-md/relative-links () + "Test `org-md-link' relative links." + (test-ox-md-relative-links-asserter + "[[rel-url:examples/babel.org][babel org file]] +[[rel-url:examples/babel.el]]" + "[babel org file](examples/babel.org) +[examples/babel.el](examples/babel.el)") + + (test-ox-md-relative-links-asserter + "[[rel-url:/en/install-emacs-on-android][install Emacs]] +[[rel-url:software/emacs/manual/html_node/elisp/Clickable-Text.html][Clickable Text]]" + "[install Emacs](/en/install-emacs-on-android) +[Clickable Text](software/emacs/manual/html_node/elisp/Clickable-Text.html)") + + (test-ox-md-relative-links-asserter + "[[rel-url:cgit/org-mode.git/log/]] +[[rel-url:?q=url+query+string][Search for url query string]]" + "[cgit/org-mode.git/log/](cgit/org-mode.git/log/) +[Search for url query string](?q=url+query+string)") + + ;; Works with <> links also + (test-ox-md-relative-links-asserter + "[[rel-url:/orgmode/87y0ijp82r.fsf@localhost/][org-meetup notes]] +<rel-url:api/v1/path/>" + "[org-meetup notes](/orgmode/87y0ijp82r.fsf@localhost/) +[api/v1/path/](api/v1/path/)") + + (test-ox-md-relative-links-asserter + "<rel-url:/account/history/abc.html> +[[rel-url:/en-US/docs/Web/HTML][Origin-relative URL]] +[[rel-url:#absolute-urls][Jump to Absolute URLs section]]" + "[/account/history/abc.html](/account/history/abc.html) +[Origin-relative URL](/en-US/docs/Web/HTML) +[Jump to Absolute URLs section](#absolute-urls)")) + (ert-deftest ox-md/headline-priority () "Test formatting of headlines with priority." (let ((export-buffer "*Test MD Export*") -- 2.54.0
