On 2025-12-30 04:29, Rudolf Adamkovič wrote:
> I think this feature should be enabled by default.
>
> (Nobody wants to see raw Org markup in HTML exports by default.)

Personally I agree, though some may find some of the simpler markup
intuitive in plain text (e.g. *bold*).

> P.S. Why "title-metadata" instead of simpler "title-tag"?

This is better, thanks.

On 2026-01-03 11:06, Ihor Radchenko wrote:
> I think that we should not strip "everything else". That's certainly
> not expected. Rather we should better leave it be. Stripping is
> rather opinionated :)

Yes that’s a fair point!  They’re only excluded since I didn’t define
transcoders for all types – I considered having it derive from
‘ox-ascii’, but I think most elements are covered now.

> +1 for links.

I think I’ve done this reasonably in v2:

+ link descriptions are preferred above all else,
+ external links are exported with the ASCII backend,
+ as a fallback, put the raw link in angular brackets ‘<like this>’.

> inline src blocks can be treated same with code
> markup.
>
> export-snippets should probably be stripped.

Done in v2.

> timestamps - org-export-get-date

I ended up using ‘org-timestamp-translate’ and stripping a final blank
like in ‘org-html-timestamp’.

> I think it may be a good idea to accept optional argument - custom
> backend. Then, we will be explicit that individual backends are free
> to replace the default `org-export-string-markup-backend' with their
> own, possibly derived from default. That will be most flexible.

Good idea, I ended up having to use this in HTML anyways since it may
replace org extensions with html and/or create links relative to a
publishing root.  I’m not particularly happy with all the repetition
transcoding links, but nothing unifying jumped out at me.  The backend
is now registered as ‘strip-markup’ so that others can extend it.

So roughly, this is what’s happening so far:

| element          | markup                     | stripped              |
|------------------+----------------------------+-----------------------|
| emphasis         | *b* /i/ ~c~ =v= _u_ +s+    | b i c v u +s+         |
| latex-fragment   | $x^2$ \(y^2\)              | $x^2$ \(y^2\)         |
| inline-src-block | src_emacs_lisp{#'identity} | #'identity            |
| sub/supersript   | a^b c_d                    | a^b c_d               |
| timestamp        | [2025-12-31 Wed]           | [2025-12-31 Wed]      |
| radio-target     | <<<radio>>>                | radio                 |
| export-snippet   | @@html:text@@              | text                  |
| link             | [[id:custom-id]]           | 1.1                   |
|                  | [[#custom-id]]             | 1.1                   |
|                  | [[*Heading]]               | 1.1                   |
|                  | [[target]]                 | 1.1                   |
|                  | [[link][description]]      | description           |
|                  | https://orgmode.org        | <https://orgmode.org> |
|                  | [[news:comp.emacs]]        | <news:comp.emacs>     |
| link (HTML)      | [[./file.org]]             | <file.html>           |
|                  | [[file:./file.org]]        | <file.html>           |
|                  | [[file:./file.org.gpg]]    | <file.html>           |

AFAICT, ‘footnote-references’ and ‘diary-sexps’ are always inserted
verbatim.  Am I missing anything else that makes sense in a title?

Best,

-- 
Jacob S. Gordon
[email protected]
Please avoid sending me HTML emails and MS Office documents.
https://useplaintext.email/#etiquette
From ae91d70019d57c4cafe2b5239a3fa4e0704b3b8f Mon Sep 17 00:00:00 2001
From: "Jacob S. Gordon" <[email protected]>
Date: Tue, 6 Jan 2026 16:20:00 -0500
Subject: [PATCH v2] ox-html: Add option to strip markup from the title tag

Markup such as *bold* in the document title will make it into the
title tag when exported to HTML.  Add an option
'org-html-strip-title-tag-markup', which when non-nil forces most
markup to be removed from this tag.

* etc/ORG-NEWS (New and changed options)
(New functions and changes in function arguments): Announce changes.
* lisp/ox.el: Define a 'strip-markup' backend.
(org-export-strip-link-markup, org-export-strip-markup): Expose
transcoding functions.
* lisp/ox-html.el: Derive an 'html-strip-markup' backend from
'strip-markup' that differs on transcoding of links.
(org-html-strip-link-markup): Add link transcoder that respects
'org-html-link-org-files-as-html' and publishing settings.
(org-html-strip-title-tag-markup, org-export-define-backend): Add
option to strip markup from the title tag.
(org-html--build-meta-info): Strip markup from the title tag when the
option is set.
* testing/lisp/test-ox-html.el (ox-html/test-strip-title-tag-markup):
Add tests.

Link: https://list.orgmode.org/[email protected]/
Co-authored-by: Nicolas Goaziou <[email protected]>
---
 etc/ORG-NEWS                 | 14 ++++++
 lisp/ox-html.el              | 62 ++++++++++++++++++++++++++-
 lisp/ox.el                   | 83 ++++++++++++++++++++++++++++++++++++
 testing/lisp/test-ox-html.el | 65 ++++++++++++++++++++++++++++
 4 files changed, 222 insertions(+), 2 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index cb94c65a9..7cbe366d4 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -471,6 +471,12 @@ the suffix of =:file= is the primary determinant, and =:file-ext=
 secondary.  Header arguments =:pdf= and =:eps= are supported for
 backwards compatibility.  Default output type is still PNG.
 
+*** New option ~org-html-strip-title-tag-markup~
+
+By default, markup such as =*bold*= in the document title will make it
+into the HTML =<title>= tag.  A non-nil ~org-html-strip-title-tag-markup~
+will force this markup to be stripped.
+
 ** New functions and changes in function arguments
 
 # This also includes changes in function behavior from Elisp perspective.
@@ -597,6 +603,14 @@ accommodate even a single character of the headline, after accounting for spaces
 and the surrounding parentheses, it will omit the headline entirely and just
 show as much of the clock as fits under the limit.
 
+*** New export utility ~org-export-strip-markup~
+
+This function and an associated ~strip-markup~ backend transcodes a
+subset of Org syntax to plain text, stripping all markup.  For
+example, all pairs of emphasis markers except for strike-through are
+removed.  A transcoder ~org-export-strip-link-markup~ for links is
+also exposed.
+
 ** Removed or renamed functions and variables
 
 *** ~org-cycle-display-inline-images~ is renamed to ~org-cycle-display-link-previews~
diff --git a/lisp/ox-html.el b/lisp/ox-html.el
index e3ea14b4d..00581e195 100644
--- a/lisp/ox-html.el
+++ b/lisp/ox-html.el
@@ -157,6 +157,8 @@ (org-export-define-backend 'html
     (:html-preamble-format nil nil org-html-preamble-format)
     (:html-prefer-user-labels nil nil org-html-prefer-user-labels)
     (:html-self-link-headlines nil "html-self-link-headlines" org-html-self-link-headlines)
+    (:html-strip-title-tag-markup
+     nil nil org-html-strip-title-tag-markup)
     (:html-table-align-individual-fields
      nil nil org-html-table-align-individual-fields)
     (:html-table-caption-above nil nil org-html-table-caption-above)
@@ -743,6 +745,17 @@ (defcustom org-html-self-link-headlines nil
   :type 'boolean
   :safe #'booleanp)
 
+(defcustom org-html-strip-title-tag-markup nil
+  "When non-nil, remove markup from the title tag using the
+`html-strip-markup' backend."
+  :group 'org-export-html
+  :package-version '(Org . "9.8")
+  :type 'boolean
+  :safe #'booleanp)
+
+(org-export-define-derived-backend 'html-strip-markup 'strip-markup
+  :translate-alist '((link . org-html-strip-link-markup)))
+
 (defcustom org-html-prefer-user-labels nil
   "When non-nil use user-defined names and ID over internal ones.
 
@@ -1994,8 +2007,14 @@ (defun org-html--build-meta-entry
 (defun org-html--build-meta-info (info)
   "Return meta tags for exported document.
 INFO is a plist used as a communication channel."
-  (let* ((title (org-html-plain-text
-		 (org-element-interpret-data (plist-get info :title)) info))
+  (let* ((title
+          (if (plist-get info :html-strip-title-tag-markup)
+              (org-html-plain-text
+               (org-export-strip-markup (plist-get info :title) info
+                                        'html-strip-markup)
+               info)
+            (org-html-plain-text
+	     (org-element-interpret-data (plist-get info :title)) info)))
 	 ;; Set title to an invisible character instead of leaving it
 	 ;; empty, which is invalid.
 	 (title (if (org-string-nw-p title) title "&lrm;"))
@@ -3476,6 +3495,45 @@ (defun org-html-link (link desc info)
      (t
       (format "<i>%s</i>" desc)))))
 
+(defun org-html-strip-link-markup (link desc info)
+  "Transcoder that strips Org markup from a LINK object.
+This accounts for `org-html-link-org-files-as-html' and publishing
+settings for file links, and falls back to `org-export-strip-link-markup'
+otherwise."
+  (if (string= "file" (org-element-property :type link))
+      (let* ((link-path (org-element-property :path link))
+             (raw-path
+              ;; Convert paths relative to publishing directory.
+              (org-export-file-uri (org-publish-file-relative-name
+                                    link-path info)))
+             ;; Maybe convert ".org" -> ".html".
+             (link-org-as-html
+              (plist-get info :html-link-org-files-as-html))
+             (html-ext (plist-get info :html-extension))
+             (dot (when (> (length html-ext) 0) "."))
+             ;; Maybe append `:html-link-home' to relative file name.
+             (link-home (and (plist-get info :html-link-home)
+                             (org-trim (plist-get info :html-link-home))))
+             (append-link-home (and link-home
+                                    (plist-get info :html-link-use-abs-url)
+                                    (not (file-name-absolute-p raw-path))))
+             ;; Maybe add search option.
+             (search-option (org-element-property :search-option link)))
+        (concat "<"
+                (when append-link-home
+                  (file-name-as-directory link-home))
+                (save-match-data
+                  (if (and link-org-as-html
+                           (let ((case-fold-search t))
+                             (string-match "\\(.+\\)\\.org\\(?:\\.gpg\\)?$"
+                                           raw-path)))
+                      (concat (match-string 1 raw-path) dot html-ext)
+                    raw-path))
+                (when search-option
+                  (concat "#" (org-publish-resolve-external-link
+                               search-option link-path t))) ">"))
+    (org-export-strip-link-markup link desc info)))
+
 ;;;; Node Property
 
 (defun org-html-node-property (node-property _contents _info)
diff --git a/lisp/ox.el b/lisp/ox.el
index eb2691380..08c617657 100644
--- a/lisp/ox.el
+++ b/lisp/ox.el
@@ -6696,6 +6696,89 @@ (defun org-export-translate (s encoding info)
 	(plist-get translations :default)
 	s)))
 
+;;;; For Stripping Markup
+;;
+;; The `strip-markup' backend removes Org markup from text.  All
+;; emphasis markers are removed except for strike-through, as that
+;; could change the meaning or create ambiguous text.  LaTeX
+;; fragments, subscripts, and superscripts are inserted verbatim.  The
+;; text of inline source blocks and radio targets are used.  Links are
+;; transcoded acording to `org-export-strip-link-markup'.  Timestamps
+;; and ranges are inserted according to `org-timestamp-translate'.
+;; Other objects are not transcoded.
+;;
+;; `org-export-strip-markup' transcodes some Org elements to plain
+;; text without markup using the `strip-markup' backend.
+;;
+;; `org-export-strip-link-markup' is the transcoder for links.
+
+(org-export-define-backend 'strip-markup
+  '((bold . (lambda (_ c _) c))
+    (italic . (lambda (_ c _) c))
+    (underline . (lambda (_ c _) c))
+    (strike-through . (lambda (_ c _)
+                        (format "+%s+" c)))
+    (code . (lambda (c _ _)
+              (org-element-property :value c)))
+    (verbatim . (lambda (v _ _)
+                  (org-element-property :value v)))
+    (subscript . (lambda (_ c _) c))
+    (superscript . (lambda (_ c _) c))
+    (latex-fragment . (lambda (f _ i)
+                        (when (plist-get i :with-latex)
+                          (org-element-property :value f))))
+    (timestamp . (lambda (ts _ _)
+                   (org-timestamp-translate
+                    (org-element-put-property
+                     (org-element-copy ts t)
+                     :post-blank 0))))
+    (inline-src-block . (lambda (i _ _)
+                          (org-element-property :value i)))
+    (radio-target . (lambda (_ c _) c))
+    (link . org-export-strip-link-markup)
+    (export-snippet . ignore)))
+
+(defun org-export-strip-link-markup (link desc info)
+  "Strip markup from the LINK with DESC.
+Link descriptions are preferred, followed by the ASCII export of the
+link, with the raw link in angled brackets `<like this>' as a fallback."
+  (let ((type (org-element-property :type link)))
+    (cond
+     ;; prefer link descriptions
+     ((or (org-string-nw-p desc) (string= type "radio")) desc)
+     ((string= type "coderef")
+      (let ((ref (org-element-property :path link)))
+        (format (org-export-get-coderef-format ref desc)
+                (org-export-resolve-coderef ref info))))
+     ((member type '("custom-id" "fuzzy" "id"))
+      (let ((dest (if (string= type "fuzzy")
+                      (org-export-resolve-fuzzy-link link info)
+                    (org-export-resolve-id-link link info))))
+        (pcase (org-element-type dest)
+          ;; points nowhere
+          (`nil (format "<%s>" (org-element-property :raw-link link)))
+          (`plain-text dest)
+          (`headline
+           (if (org-export-numbered-headline-p dest info)
+               (mapconcat #'number-to-string
+                          (org-export-get-headline-number dest info)
+                          ".")
+             (org-export-data (org-element-property :title dest) info)))
+          ((and (let num (org-export-get-ordinal
+                          dest info nil
+                          #'(lambda (e _) (org-element-property :caption e))))
+                (guard num))
+           (if (atom num)
+               (number-to-string num)
+             (mapconcat #'number-to-string num ".")))
+          (_ "???"))))
+     ((org-export-custom-protocol-maybe link desc 'ascii info))
+     (t (format "<%s>" (org-element-property :raw-link link))))))
+
+(defun org-export-strip-markup (data info &optional backend)
+  "Export DATA to text using the `strip-markup' backend, or BACKEND if
+provided."
+  (org-export-data-with-backend data (or backend 'strip-markup) info))
 
 
 ;;; Asynchronous Export
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index 5a8e7df3c..a187b4c85 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -1106,5 +1106,70 @@ (ert-deftest org-html/test-toc-text ()
         (expected "\n<ul>\n<li>\n<ul>\n<li>1\n<ul>\n<li>1.1</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>2</li>\n</ul>\n"))
     (should (string= (org-html--toc-text toc-entries nil) expected))))
 
+;;; Stripping markup from the <title> tag
+
+(ert-deftest ox-html/test-strip-title-tag-markup ()
+  "When `org-html-strip-title-tag-markup' is non-nil, ensure the `<title'>
+tag is stripped of markup."
+  (dolist (case `(("*b* /i/ ~c~ =v= _u_ +s+" . "b i c v u +s+") ; emphasis
+                  ("*/bi/* +~sc~+ _+us+_" . "bi +sc+ +us+")
+                  ("+*/~+sbics+~/*+" . "++sbics++")
+                  ("a^b c_d")                                   ; subscript & superscript
+                  ("\\(x^2\\), $y^2$")                          ; latex-fragment
+                  ("[2025-12-31 Wed] [2025-12-31 Wed 23:59]")   ; timestamps & ranges
+                  ("<2025-12-31 Wed>")
+                  ("<2025-12-31 Wed>--<2026-01-01 Thu>")
+                  ("src_emacs_lisp{#'identity}" . "#'identity") ; inline-src-block
+                  ("<<<radio>>>" . "radio")                     ; radio-target
+                  ("A target\n<<<target>>>" . "A target")       ; links (internal)
+                  ("[[target]]\n1. <<target>> item" . "1")
+                  ("[[*Subheading]]\n* Heading\n** Subheading" . "1.1")
+                  (,(concat "[[#custom-id]]\n"
+                            "* Heading\n"
+                            ":PROPERTIES:\n"
+                            ":CUSTOM_ID: custom-id\n"
+                            ":END:") . "1")
+                  (,(concat "[[id:custom-id]]\n"
+                            "* Heading\n"
+                            ":PROPERTIES:\n"
+                            ":CUSTOM_ID: custom-id\n"
+                            ":END:") . "1")
+                  (,(concat "[[target]]\n"
+                            "#+NAME: target\n"
+                            "#+CAPTION: Caption\n"
+                            "#+begin_src emacs-lisp\n"
+                            "(format \"%d\" #xFF)\n"
+                            "#+end_src") . "1")
+                  (,(concat "[[(fmt)]]\n"
+                            "#+begin_src emacs-lisp -r\n"
+                            "(format \"%d\" #xFF)  (ref:fmt)\n"
+                            "#+end_src") . "1")
+                  ("[[./test.org]]" . "<./test.html>")          ; links (.org -> .html)
+                  ("[[file:./test.org]]" . "<./test.html>")
+                  ("[[file:./test.org.gpg]]" . "<./test.html>")
+                  ("https://orgmode.org"; .                      ; links (external)
+                   "<https://orgmode.org>")
+                  ("<https://orgmode.org>")
+                  ("[[https://orgmode.org]]"; . "<https://orgmode.org>")
+                  ("[[https://orgmode.org][Org website]]" . "Org website")
+                  ("[[doi:10.1000/182]]" . "<https://doi.org/10.1000/182>")
+                  ("[[news:comp.emacs]]"; . "<news:comp.emacs>")
+                  ("The @@html:HTML@@ title" .                  ; export-snippet
+                   "The title")))
+    (let* ((export-buffer "*Test HTML Export*")
+           (org-export-show-temporary-export-buffer nil)
+           (org-html-strip-title-tag-markup t)
+           (org-html-link-org-files-as-html t)
+           (org-export-with-latex t)
+           (title (car case))
+           (expected (org-html-convert-special-strings
+                      (org-html-plain-text (or (cdr case) title) nil))))
+      (org-test-with-temp-text (format "#+TITLE: %s" title)
+        (org-export-to-buffer 'html export-buffer nil nil nil nil nil)
+        (with-current-buffer export-buffer
+          (should (= 1 (how-many
+                        (format "<title>%s</title>"
+                                (regexp-quote expected))))))))))
+
 (provide 'test-ox-html)
 ;;; test-ox-html.el ends here

base-commit: 1328e518db2e5876c8768fb4c74769d5b518984a
-- 
Jacob S. Gordon
[email protected]
Please avoid sending me HTML emails and MS Office documents.
https://useplaintext.email/#etiquette

Reply via email to