Hey, folks.  I've been writing more latex in org recently, and I
stumbled over the bug named in the subject line.  Here's a test case
you can evaluate in a scratch buffer in emacs -Q:

```
(with-temp-buffer
  (org-mode)
  (insert "* \\phi wraps
:PROPERTIES:
:ALT_TITLE: \\psi should wrap
:END:")
  (org-export-as 'latex nil nil t))
;; say `eval-print-last-sexp' (C-j)
;;
;; expected:
;; "\\section[\\(\\psi\\) should wrap]{\\(\\phi\\) wraps}
;; \\label{...}
;; "
;;
;; actual:
;; "\\section[\\psi should wrap]{\\(\\phi\\) wraps}
;; \\label{...}
;; "
```

When this happens in a real export I get a warning and my latex
compiler (pdflatex) guesses, badly, where to insert dollar signs.

I'd like to fix this.  I dug into the issue and the root cause seems
kind of gnarly.  When `org-latex-headline' pulls the alt title it
correctly parses any entities / latex fragments in the property value,
but it doesn't wrap them into latex-math-block objects because
secondary string parsing doesn't give :filter-parse-tree functions a
chance to run.  I couldn't see any way to use the existing machinery
to do the math-block wrapping, so I just added an extra step in o-l-h.

Patch attached (including a new test).

Note that in developing this patch I worked off of today's main branch
(commit 2815a6).  Therefore this patch includes an extra diff hunk to
accommodate 35d12c, even though that commit isn't included in any of
the release builds.  If you'd prefer a patch that applies cleanly on
top of the latest release, I can resend without that hunk.

Thanks,

Trevor

Emacs  : GNU Emacs 30.2 (build 1, x86_64-pc-linux-gnu, GTK+ Version
3.24.50, cairo version 1.18.4)
Package: Org mode version 9.7.35 (9.7.35-7fbe36 @
/home/trevor/.emacs.d/elpa/org-9.7.35/)
From 0166acc92e3589e2c28d58d9b976523fe71f66fe Mon Sep 17 00:00:00 2001
From: Trevor Murphy <[email protected]>
Date: Sun, 9 Nov 2025 14:40:17 -0800
Subject: [PATCH] Handle math mode latex in headline alternative titles.

* lisp/ox-latex.el (org-latex-headline): Parse and regexp-quote
entities and latex fragments when computing opt-title.
* testing/lisp/test-ox-latex.el (test-ox-latex/math-in-alt-title): New
test.
---
 lisp/ox-latex.el              | 73 ++++++++++++++++++++++-------------
 testing/lisp/test-ox-latex.el | 12 ++++++
 2 files changed, 58 insertions(+), 27 deletions(-)

diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index 1efc9bb93..7b79420a8 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -2418,32 +2418,48 @@ org-latex-headline
 	;; This is a standard headline.  Export it as a section.  Add
 	;; an alternative heading when possible, and when this is not
 	;; identical to the usual heading.
-	(let ((opt-title
-	       (funcall (plist-get info :latex-format-headline-function)
-			todo todo-type priority
-			(org-export-data-with-backend
-                         ;; Returns alternative title when provided or
-                         ;; title itself.
-			 (org-export-get-alt-title headline info)
-			 org-latex--section-backend info)
-			(and (eq (plist-get info :with-tags) t) tags)
-			info))
-	      ;; Maybe end local TOC (see `org-latex-keyword').
-	      (contents
-	       (concat
-		contents
-		(let ((case-fold-search t)
-		      (section
-		       (let ((first (car (org-element-contents headline))))
-			 (and (org-element-type-p first 'section) first))))
-		  (org-element-map section 'keyword
-		    (lambda (k)
-		      (and (equal (org-element-property :key k) "TOC")
-			   (let ((v (org-element-property :value k)))
-			     (and (string-match-p "\\<headlines\\>" v)
-				  (string-match-p "\\<local\\>" v)
-				  (format "\\stopcontents[level-%d]" level)))))
-		    info t)))))
+	(let* ((alt-title
+		;; Returns alternative title when provided or title
+		;; itself.
+		(org-export-get-alt-title headline info))
+	       (wrapped-alt
+		;; If alt-title contains entities / latex fragments
+		;; then they do not come wrapped in a
+		;; latex-math-block.  Before we can wrap them, gotta
+		;; break the parent->headline pointer relationship.
+		;; Wrapping subroutines recognize independent
+		;; secondary strings by having parent->container
+		;; relationship instead.  -wrap-latex-math-block only
+		;; acts on entities / latex fragments, so this step
+		;; will not impact plain text alternative titles.
+		(progn
+		  (dolist (elt alt-title)
+		    (org-element-put-property elt :parent alt-title))
+		  (org-latex--wrap-latex-math-block alt-title info)))
+	       (opt-title
+		(funcall (plist-get info :latex-format-headline-function)
+			 todo todo-type priority
+			 (org-export-data-with-backend
+			  wrapped-alt
+			  org-latex--section-backend info)
+			 (and (eq (plist-get info :with-tags) t) tags)
+			 info))
+	       ;; Maybe end local TOC (see `org-latex-keyword').
+	       (contents
+	        (concat
+		 contents
+		 (let ((case-fold-search t)
+		       (section
+		        (let ((first (car (org-element-contents headline))))
+			  (and (org-element-type-p first 'section) first))))
+		   (org-element-map section 'keyword
+		     (lambda (k)
+		       (and (equal (org-element-property :key k) "TOC")
+			    (let ((v (org-element-property :value k)))
+			      (and (string-match-p "\\<headlines\\>" v)
+				   (string-match-p "\\<local\\>" v)
+				   (format "\\stopcontents[level-%d]" level)))))
+		     info t)))))
           ;; When do we need to explicitly specify a heading for TOC?
           ;; 1. On numbered section with footnotes in title or alt_title
           ;; 2. On an unnumbered section if :UNNUMBERED: allows it regardless of footnotes
@@ -2505,7 +2521,10 @@ org-latex-headline
 		        ;; since square brackets are not supported in
 		        ;; optional arguments.
                         ((un-bracketed-alt (replace-regexp-in-string
-                                            "\\[" "(" (replace-regexp-in-string "\\]" ")" opt-title)))
+                                            "\\[" "("
+                                            (replace-regexp-in-string
+                                             "\\]" ")"
+                                             (regexp-quote opt-title))))
                          (replacement-re (concat "\\1[" un-bracketed-alt "]")))
                       (setq new-format (replace-match replacement-re nil nil section-fmt 1))))
                   (format new-format
diff --git a/testing/lisp/test-ox-latex.el b/testing/lisp/test-ox-latex.el
index 04164767c..35088deee 100644
--- a/testing/lisp/test-ox-latex.el
+++ b/testing/lisp/test-ox-latex.el
@@ -272,5 +272,17 @@ test-ox-latex/new-toc-as-org
       (should (search-forward "}
 \\addcontentsline{toc}{section}{Section 3}")))))
 
+(ert-deftest test-ox-latex/math-in-alt-title ()
+  "Test math wrapping in ALT_TITLE properties."
+  (org-test-with-exported-text
+      'latex
+      "* \\phi wraps
+:PROPERTIES:
+:ALT_TITLE: \\psi wraps too
+:END:"
+    (goto-char (point-min))
+    (should (search-forward
+             "\\section[\\(\\psi\\) wraps too]{\\(\\phi\\) wraps}"))))
+
 (provide 'test-ox-latex)
 ;;; test-ox-latex.el ends here
-- 
2.51.2

Reply via email to