diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index a7f32671e..e51bb6233 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -489,6 +489,13 @@ support in LaTeX. The new =babel= syntax for loading languages via =ini= files and the new command =\babelprovide= (see: https://mirrors.ctan.org/macros/latex/required/babel/base/babel.pdf) are also supported. +*** Texinfo exports include LaTeX + +With the new customization option ~org-texinfo-with-latex~ set to (its +default value) ~'detect~, if the system runs Texinfo 6.8 (3 July 2021) +or newer, Org will export all LaTeX fragments and environments using +Texinfo ~@math~ and ~@displaymath~ commands respectively. + * Version 9.5 ** Important announcements and breaking changes diff --git a/lisp/ox-texinfo.el b/lisp/ox-texinfo.el index 623c1632c..1f2e8d9a1 100644 --- a/lisp/ox-texinfo.el +++ b/lisp/ox-texinfo.el @@ -33,7 +33,7 @@ (require 'ox) (defvar orgtbl-exp-regexp) - +(defvar org-texinfo-supports-math--cache) ;;; Define Back-End @@ -58,6 +58,8 @@ (italic . org-texinfo-italic) (item . org-texinfo-item) (keyword . org-texinfo-keyword) + (latex-environment . org-texinfo-latex-environment) + (latex-fragment . org-texinfo-latex-fragment) (line-break . org-texinfo-line-break) (link . org-texinfo-link) (node-property . org-texinfo-node-property) @@ -123,7 +125,9 @@ (:texinfo-text-markup-alist nil nil org-texinfo-text-markup-alist) (:texinfo-format-drawer-function nil nil org-texinfo-format-drawer-function) (:texinfo-format-inlinetask-function nil nil org-texinfo-format-inlinetask-function) - (:texinfo-compact-itemx nil "compact-itemx" org-texinfo-compact-itemx))) + (:texinfo-compact-itemx nil "compact-itemx" org-texinfo-compact-itemx) + ;; Redefine regular options. + (:with-latex nil "tex" org-texinfo-with-latex))) ;;; User Configurable Variables @@ -358,6 +362,22 @@ The function should return the string to be exported." :group 'org-export-texinfo :type 'function) +;;;; LaTeX + +(defcustom org-texinfo-with-latex (and org-export-with-latex 'detect) + "When non-nil, the Texinfo exporter attempts to process LaTeX math. + +When set to t, the exporter will process LaTeX environments and +fragments as Texinfo \"@displaymath\" and \"@math\" commands +respectively. Alternatively, when set to `detect', the exporter +does so only if the installed version of Texinfo supports the +necessary commands." + :group 'org-export-texinfo + :type '(choice + (const :tag "Detect" detect) + (const :tag "Yes" t) + (const :tag "No" nil))) + ;;;; Itemx (defcustom org-texinfo-compact-itemx nil @@ -1215,6 +1235,52 @@ CONTENTS is nil. INFO is a plist holding contextual information." (concat "@listoffloats " (org-export-translate "Listing" :utf-8 info)))))))) +;;;; LaTeX Environment + +(defun org-texinfo-latex-environment (environment _contents info) + "Transcode a LaTeX ENVIRONMENT from Org to Texinfo. +CONTENTS is ignored. INFO is a plist holding contextual information." + (let ((with-latex (plist-get info :with-latex))) + (when (or (eq with-latex t) + (and (eq with-latex 'detect) + (org-texinfo-supports-math-p))) + (let ((value (org-element-property :value environment))) + (string-join (list "@displaymath" + (string-trim (org-remove-indentation value)) + "@end displaymath") + "\n"))))) + +;;;; LaTeX Fragment + +(defun org-texinfo-latex-fragment (fragment _contents info) + "Transcode a LaTeX FRAGMENT from Org to Texinfo. +INFO is a plist holding contextual information." + (let ((with-latex (plist-get info :with-latex))) + (when (or (eq with-latex t) + (and (eq with-latex 'detect) + (org-texinfo-supports-math-p))) + (let ((value (org-remove-indentation + (org-element-property :value fragment)))) + (cond + ((or (string-match-p "^\\\\\\[" value) + (string-match-p "^\\$\\$" value)) + (concat "\n" + "@displaymath" + "\n" + (string-trim (substring value 2 -2)) + "\n" + "@end displaymath" + "\n")) + ((string-match-p "^\\$" value) + (concat "@math{" + (string-trim (substring value 1 -1)) + "}")) + ((string-match-p "^\\\\(" value) + (concat "@math{" + (string-trim (substring value 2 -2)) + "}")) + (t value)))))) + ;;;; Line Break (defun org-texinfo-line-break (_line-break _contents _info) @@ -1951,6 +2017,31 @@ Return INFO file name or an error if it couldn't be produced." (message "Process completed.") output)) +(defun org-texinfo-supports-math-p () + "Return t if the installed version of Texinfo supports \"@math\". + +Once computed, the results remain cached." + (unless (boundp 'org-texinfo-supports-math--cache) + (setq org-texinfo-supports-math--cache + (let ((math-example "1 + 1 = 2")) + (let* ((input-file + (make-temp-file "test" nil ".info")) + (input-content + (concat (format "@setfilename %s" input-file) "\n" + "@node Top" "\n" + (format "@displaymath{%s}" math-example) "\n"))) + (with-temp-file input-file + (insert input-content)) + (let* ((output-file (org-texinfo-compile input-file)) + (output-content (with-temp-buffer + (insert-file-contents output-file) + (buffer-string)))) + (let ((result (string-match-p (regexp-quote math-example) + output-content))) + (delete-file input-file) + (delete-file output-file) + (if result t nil))))))) + org-texinfo-supports-math--cache) (provide 'ox-texinfo) diff --git a/testing/lisp/test-ox-texinfo.el b/testing/lisp/test-ox-texinfo.el new file mode 100644 index 000000000..51fdb3606 --- /dev/null +++ b/testing/lisp/test-ox-texinfo.el @@ -0,0 +1,296 @@ +;;; test-ox-texinfo.el --- Tests for ox-texinfo.el + +;; Copyright (C) 2022 Rudolf Adamkovič + +;; Author: Rudolf Adamkovič + +;; This file is not part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Code: + +(require 'cl-lib) +(require 'ox-texinfo) + +(unless (featurep 'ox-texinfo) + (signal 'missing-test-dependency "org-export-texinfo")) + + +;;; TeX fragments + +(ert-deftest test-ox-texinfo/tex-fragment-inline () + "Test inline TeX fragment." + (should + (equal "@math{a^2 = b}" + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + '(:value "$a^2 = b$")) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/tex-fragment-inline-padded () + "Test inline TeX fragment padded with whitespace." + (should + (equal "@math{a^2 = b}" + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + '(:value "$ a^2 = b $")) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/tex-fragment-displayed () + "Test displayed TeX fragment." + (should + (equal (string-join + (list "" + "@displaymath" + "a ^ 2 = b" + "@end displaymath" + "") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + (list :value "$$a ^ 2 = b$$")) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/tex-fragment-displayed-padded () + "Test displayed TeX fragment padded with whitespace." + (should + (equal (string-join + (list "" + "@displaymath" + "a ^ 2 = b" + "@end displaymath" + "") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + (list :value "$$ a ^ 2 = b $$")) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/tex-fragment-displayed-multi-line () + "Test displayed TeX fragment with multiple lines." + (should + (equal (string-join + (list "" + "@displaymath" + "a ^ 2 = b" + "b ^ 2 = c" + "@end displaymath" + "") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + (list :value + (string-join + (list "$$" + "a ^ 2 = b" + "b ^ 2 = c" + "$$") + "\n"))) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/tex-fragment-displayed-indented () + "Test displayed TeX fragment with indentation." + (should + (equal (string-join + (list "" + "@displaymath" + "a ^ 2 = b" + "b ^ 2 = c" + "@end displaymath" + "") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + (list :value + (string-join + (list " $$" + " a ^ 2 = b" + " b ^ 2 = c" + " $$") + "\n"))) + nil + '(:with-latex t)))))) + + +;;; LaTeX fragments + +(ert-deftest test-ox-texinfo/latex-fragment-inline () + "Test inline LaTeX fragment." + (should + (equal "@math{a^2 = b}" + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + '(:value "\\(a^2 = b\\)")) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/latex-fragment-inline-padded () + "Test inline LaTeX fragment padded with whitespace." + (should + (equal "@math{a^2 = b}" + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + '(:value "\\( a^2 = b \\)")) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/latex-fragment-displayed () + "Test displayed LaTeX fragment." + (should + (equal (string-join + (list "" + "@displaymath" + "a ^ 2 = b" + "@end displaymath" + "") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + (list :value "\\[a ^ 2 = b\\]")) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/latex-fragment-displayed-padded () + "Test displayed LaTeX fragment with multiple lines." + (should + (equal (string-join + (list "" + "@displaymath" + "a ^ 2 = b" + "@end displaymath" + "") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + (list :value "\\[ a ^ 2 = b \\]")) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/latex-fragment-displayed-multi-line () + "Test displayed LaTeX fragment with multiple lines." + (should + (equal (string-join + (list "" + "@displaymath" + "a ^ 2 = b" + "b ^ 2 = c" + "@end displaymath" + "") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + (list :value + (string-join + (list "\\[" + "a ^ 2 = b" + "b ^ 2 = c" + "\\]") + "\n"))) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/latex-fragment-displayed-indented () + "Test displayed LaTeX fragment with indentation." + (should + (equal (string-join + (list "" + "@displaymath" + "a ^ 2 = b" + "b ^ 2 = c" + "@end displaymath" + "") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-fragment + (org-element-create 'latex-fragment + (list :value + (string-join + (list " \\[" + " a ^ 2 = b" + " b ^ 2 = c" + " \\]") + "\n"))) + nil + '(:with-latex t)))))) + + +;;; LaTeX environments + +(ert-deftest test-ox-texinfo/latex-environment () + "Test LaTeX environment." + (should + (equal (string-join + (list "@displaymath" + "\\begin{equation}" + "a ^ 2 = b" + "b ^ 2 = c" + "\\end{equation}" + "@end displaymath") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-environment + (org-element-create 'latex-environment + (list :value + (string-join + (list "\\begin{equation}" + "a ^ 2 = b" + "b ^ 2 = c" + "\\end{equation}") + "\n"))) + nil + '(:with-latex t)))))) + +(ert-deftest test-ox-texinfo/latex-environment-indented () + "Test LaTeX environment with indentation." + (should + (equal (string-join + (list "@displaymath" + "\\begin{equation}" + "a ^ 2 = b" + "b ^ 2 = c" + "\\end{equation}" + "@end displaymath") + "\n") + (let ((org-texinfo-with-latex t)) + (org-texinfo-latex-environment + (org-element-create 'latex-environment + (list :value + (string-join + (list " \\begin{equation}" + " a ^ 2 = b" + " b ^ 2 = c" + " \\end{equation}") + "\n"))) + nil + '(:with-latex t)))))) + +(provide 'test-ox-texinfo) +;;; test-ox-texinfo.el end here