So here comes the fontspec...

On 26/07/2026 08:53, Ihor Radchenko wrote:
Pedro Andres Aranda Gutierrez <[email protected]> writes:

Subject: [PATCH 1/2] LaTeX export: add org-latex-fontspec-config

Thanks for the updated patch. The new wording on the manual reads much
smoother compared to the early versions.

Some minor comments inline.

* doc/org-manual.org
(*** Controlling font setup for LuaLaTeX andXeLaTeX): New section
describing org-latex-fontspec-config.

You should just use "(Controlling font setup for LuaLaTeX andXeLaTeX)"
to refer to section.
Also, 'org-latex-fontspec-config' (quoted).

Done

* etc/ORG-NEWS: Annouce org-latex-fontspec-config.

'org-latex-fontspec-config'.
And you missed the section in ORG-NEWS where you put the announcement.

Done

(org-latex-guess-fontspec): New funciton to insert the fontspec
configuration when the fontspec package is requested in the LaTeX
prelude.

Shouldn't "prelude" be preamble?

As mentioned before, both are used interchangeably, but anyhow...
changed

(org-latex-make-preamble): Add the previous function in the chain of
guessers, just before the babel and polyglossia guessers. Fix null class
options or empty string.

Double space between sentences.

Done
(test-ox-latex/lualatex-fontspec-recognised): Test correct
implementation of fontspec handling.
(test-ox-latex/lualatex-fontspec-recognised): Test fallbacks.

You have a duplicate changelog entry here.

Fixed

+This mapping takes place in ~org-latex-fontspec-config~.  Although not
+mandatory, it is recommended that you define mapping for the four
+families.  An example mapping the fonts to use another popular font family,
+[[https://www.gust.org.pl/projects/e-foundry/tex-gyre][the TeX Gyre
+(TG) Collection of Fonts]] could be [fn::We use this family, because
+it is also included in most LaTeX distributions.]:

Nit: Maybe fn can go right after "the TeX Gyre (TG) Collection of
Fonts", not after "could be".

Kept because it looks too cluttered in the PDF.

+As you see, each element in the list maps one of the LaTeX font
+families to a property =:font=, that indicates the font to use for the

~:font~ (it is a code)
done>
+Additionally, you can also define "=:features="[fn::You may also use
+=:props= as a synonym for =:features=.] for each of the fonts.

~:features~, ~:props~.
done>
+the missing glyph set, by using the =:fallback= property for the

~:fallback~ (similar problems later on as well)
done (hoperfully) everywhere>
+*** New custom variable ~org-latex-fontspec-config~
+
+When using LuaLaTeX (or XeLaTeX in a reduced scope), you can use the
+new custom variable ~org-latex-fontspec-config~ to change the
+document's fonts and add fallback fonts for missing scripts.

I think we can refer to the new section in the manual for more details.

Done, I referred to the section.
    "Return complete document string after Beamer conversion.
  CONTENTS is the transcoded contents string.  INFO is a plist
  holding export options."
+  ;; Before doing anything else, add the script information
+  ;; to the INFO channel. Used by org-latex-make-preamble
+  ;; to add fallback fonts for lualatex.
+  (setq info (plist-put info
+                        :doc-scripts
+                        (org-get-string-scripts contents)))
+

May we do the same in :filter-body instead? Then, we can just arrange
calling org-get-string-scripts in ox-latex without a need to repeat the
procedure in ox-beamer.

Done... hopefully the correct way... At least tests don't fail.


+(defcustom org-latex-fontspec-config nil
+  "An alist with the configuration for the fontspec package.
+
+This configuration will be generated when using lualatex or xelatex.
+
+Each element is defined as
+(`font-name' . `font-plist')
+ where `font-name' one of \"main\", \"sans\", \"mono\" or \"math\"
+ and `font-plist' is a plist. The keys for this plist are
+  `:font':     font name for font installed in your system
+  `:features': string or list of strings with font features (optional).
+  `:fallback': an alist of (`script' . `mapping') to map _Emacs_ script names
+               to their fallback font (optional). The exporter will warn you
+               about scripts in your document that need a fallback font."

These are not many details.
We should at least refer to the new manual section.

Done>
+  :group 'org-export-latex
+  :package-version '(Org . "10.0")
+  :type 'alist

We could use a more detailed type definition.

FFS


+(defun org-latex-guess-fontspec (header info)
+  "Add the fontspec package configuration passed in INFO to HEADER.
+
+The HEADER contains \"\\usepackage{fontspec}\",
+and INFO contains fontspec font conguration and
+add the fontspec configuration after the package."
+    (when-let* ((fonst (plist-get info :latex-fontspec-config))

fonst?
Replaced with `_'

+                (matched (string-match "\\\\usepackage{fontspec}\n" header))

Why trailing \n?
Also, what about \usepackage[...]{fontspec}?
Removed.

EXTRA stuff to make the patch as feature-completeas possible:

1.- I included the :languages -> :language in the INFO channel from feature/all-tex-fonts. We can't use it, however, because some exporters rely on the original #+LANGUAGE implementation overwriting the current language.

2.- I reintroduced handling CJK fonts with fontspec+xelatex (w/ test and documentation).

3.- unicode-math is only included when needed, i.e. when we use \setmonofont.

So, with this, we should have fontspec in a compatible way with current uses of babel and polyglossia. It is enough of a 'feature' to go on it's own (IMvvvHO).

I would be nice to test drive it in 'main' for a couple of months and then tackle font configuration with lualatex and babel/polyglossia.


--
Pedro A. Aranda
ox-latex & ox-beamer maintainer

Sagen's Paradeiser! (ORF: Als Radiohören gefährlich war) => write BE!
IA: onomatopeya del rebuzno
2nd year of the New Koprocracy
From 19a04ada70e5761f82bf7d939087b19d7c94c210 Mon Sep 17 00:00:00 2001
From: "Pedro A. Aranda" <[email protected]>
Date: Sun, 26 Jul 2026 18:05:35 +0200
Subject: [PATCH] LaTeX export: add org-latex-fontspec-config

This is (hopefully) the full fontspec support as a single patch.

* doc/org-manual.org (Controlling font setup for LuaLaTeX andXeLaTeX):
New section describing `org-latex-fontspec-config', including fallback
fonts and CJK fonts with XeLaTeX
* etc/ORG-NEWS: (New and changed options): Announce
`org-latex-fontspec-config'.
Announce that LANGUAGE accepts a list of language codes for the LaTeX
exporters and present the `:languages' property.
* lisp/org.el (org-get-string-scripts): New function to extract the
script information from a string.  This function is currently used by
ox-latex and ox-beamer.
* lisp/ox-beamer.el (org-beamer-template): Keep the reference to
`org-get-string-scripts' as a comment in the header.
* lisp/ox-latex.el (Backend): Add `org-latex-get-font-list' as a filter
for the body of the document.
(Information channel): Inject `org-latex-fontspec-config', make the
LANGUAGE keyword accept a list of language codes.
(org-latex-fontspec-config): New custom variable to store the font
configuration for the fontspec package.
(org-latex-get-font-list): New body filter function to inject the Emacs
script list into the information channel.
(org-latex--needs-math-font): New function checking whether the math
font has been configured.
(org-latex--needs-xecjk): New function to check for the presence pof CJK
fonts in the fontspec configuration and warn if they are detected but
the compiler is not xelatex.
(org-latex--fontspec-prelude): New function to create the fontspec
configuration as a string.
(org-latex-guess-fontspec): New function to insert the fontspec
configuration when the fontspec package is requested in the LaTeX
preamble.
(org-latex-make-preamble): Add the previous function in the chain of
guessers, just before the babel and polyglossia guessers.  Fix null class
options or empty string.
(org-latex--mk-options): Options can be a list of strings.
lisp/ox.el (Information channel): Keep multi-code LANGUAGE as a comment
and why I don't use it.
(org-export-get-environment): Inject :language when :languages is
present in the information channel.
* testing/lisp/test-ox-latex.el (test-ox-latex/pdf-metadata): Clean up
output.
(test-ox-latex/lualatex-fontspec-recognised): Test correct
implementation of fontspec handling.
(test-ox-latex/lualatex-fontspec-fallback): Test script detection with
and Emoji and fallback generation.
(test-ox-latex/lualatex-fontspec-latex-header-not-lost): LATEX_HEADER
prevails over fontspec configuration.  Cater for legacy documents.
(test-ox-latex/back-in-time-cjk-xelatex): Test CJK fonts with xelatex.
---
 doc/org-manual.org            | 165 +++++++++++++++++++++++++++
 etc/ORG-NEWS                  |  16 +++
 lisp/org.el                   |  25 +++++
 lisp/ox-beamer.el             |   5 +
 lisp/ox-latex.el              | 202 +++++++++++++++++++++++++++++++---
 lisp/ox.el                    |  27 +++--
 testing/lisp/test-ox-latex.el | 104 +++++++++++++++++
 7 files changed, 517 insertions(+), 27 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index 961aa7db9..e557c7ac0 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -15203,6 +15203,171 @@ contents, unless you set the =UNNUMBERED= property to =notoc=:
 :END:
 #+end_example

+*** Controlling font setup for LuaLaTeX and XeLaTeX
+:PROPERTIES:
+:CUSTOM_ID: fontspec-configuration
+:END:
+#+cindex: fontspec in @LaTeX{} export
+#+vindex: org-latex-fontspec-config
+
+Modern, UTF8-capable LaTeX compilers LuaLaTeX and XeLaTeX configure
+fonts using the [[https://latex3.github.io/fontspec/][~fontspec~]]
+package.  Org provides a custom variable to fine-tune font
+configuration when the ~fontspec~ package is used.  With the default
+configuration provided by Org, this package is included when LuaLaTeX
+or XeLaTeX are declared as the LaTeX target compiler.
+
+LaTeX document classes include a set of default fonts, usually from
+the [[https://www.gust.org.pl/projects/e-foundry/latin-modern][Latin
+Modern (LM) Family of Fonts]].  If you need to use other fonts, you can
+do so by mapping your font preferences to the four main font families
+defined in LaTeX:
+
+| "main" | Roman or Serif font (default)     |
+| "sans" | Sans Serif font                   |
+| "mono" | Monospaced (terminal) font        |
+| "math" | Font for mathematical expressions |
+
+This mapping takes place in ~org-latex-fontspec-config~.  Although not
+mandatory, it is recommended that you define mapping for the four
+families.  An example mapping the fonts to use another popular font family,
+[[https://www.gust.org.pl/projects/e-foundry/tex-gyre][the TeX Gyre
+(TG) Collection of Fonts]] could be [fn::We use this family, because
+it is also included in most LaTeX distributions.]:
+
+#+BEGIN_SRC emacs-lisp
+(setq org-latex-fontspec-config
+      '(("main" :font "TeX Gyre Termes")
+        ("sans" :font "TeX Gyre Heros")
+        ("mono" :font "TeX Gyre Cursor")
+        ("math" :font "TeX Gyre Math"))
+#+END_SRC
+
+As you see, each element in the list maps one of the LaTeX font
+families to a property ~:font~, that indicates the font to use for the
+specific family.  This font must be installed in your system,
+
+Additionally, you can also define "~:features~"[fn::You may also use
+~:props~ as a synonym for ~:features~.] for each of the fonts.
+Features are additional
+[[https://latex3.github.io/fontspec/user.html][font options]] that
+modify their appearance.  A common example is scaling the mono-typed
+font to approximately match the size of the ~main~ font and provide a
+more uniform appearance to your document:
+
+#+BEGIN_SRC emacs-lisp
+(setq org-latex-fontspec-config
+      '(("main" :font "TeX Gyre Termes")
+        ("sans" :font "TeX Gyre Heros")
+        ("mono" :font "TeX Gyre Cursor" :features "Scale=MatchLowercase")
+        ("math" :font "TeX Gyre Math")))
+#+END_SRC
+
+**** Fallback fonts with LuaLaTeX
+:PROPERTIES:
+:UNNUMBERED: t
+:END:
+
+You will be warned by Org that your document could not be correctly
+typeset, if your selection of fonts doesn't cover all scripts used
+in the document:
+#+BEGIN_EXAMPLE
+Warning (ox-latex): PDF file produced with warnings: [Missing
+character(s): please load an appropriate font with the fontspec package]
+#+END_EXAMPLE
+
+In the event this message appears, Emacs also provides you with a list
+of fonts and *Emacs* script families that caused problems:
+
+#+BEGIN_EXAMPLE
+Warning (emacs): Missing scripts:
+For font TeXGyreHeros: emoji
+#+END_EXAMPLE
+
+Although modern Unicode-compatible fonts can represent a wide range of
+characters (or glyphs), it is virtually impossible to cover all possible
+glyphs in one font.  In this case, *and only if you are using
+LuaLaTeX*, you will be able to use other fonts you have installed in
+your system to provide the missing glyphs. LuaLaTeX can replace glyphs
+that are not provided by the document's fonts on the fly, using a
+feature called /fallback fonts/.
+
+An example is a document using emojis.  In this case, and provided you
+have the =Noto Color Emoji= font installed in your system[fn::Another
+font providing emojis is =Apple Color Emoji=], you can use it to add
+the missing glyph set, by using the =:fallback= property for the
+reported font. [fn::The
+[[https://www.gust.org.pl/projects/e-foundry/latin-modern#download][Latin
+Modern fonts]], are reported abbreviated as =lm...=. They are used by
+default in well-known LaTeX classes as =article=, =report=, etc.]
+
+#+BEGIN_SRC emacs-lisp
+(setq org-latex-fontspec-config
+      '(("main" :font "Latin Modern Roman")
+        ("sans" :font "Latin Modern Sans"
+         :fallback (("emoji" . "Noto Color Emoji:mode=harf")))
+        ("mono" :font "Latin Modern Mono" :props "Scale=MatchLowercase")
+        ("math" :font "Latin Modern Math")))
+#+END_SRC
+
+Note the following in the code above:
+1. The fallback font is followed by additional indications
+   (=:mode=harf=). You need them in this case to instruct LuaLaTeX to
+   use a special glyph renderer (harfbuzz).  This is not always
+   needed.  These and other options are described in
+   [[https://mirrors.mit.edu/CTAN/macros/luatex/generic/luaotfload/luaotfload-latex.pdf][the
+   luaotfload package manual]].
+2. You can add fallback fonts for more Emacs scripts by adding
+   elements to the =:fallback= property, which is an associative list:
+   #+BEGIN_SRC emacs-lisp
+     ("main" :font "Latin Modern Roman"
+       :fallback (("emoji" . "Noto Color Emoji:mode=harf")
+                  ("han"   . "Noto Serif CJK JP")
+                  ("kana"  . "Noto Serif CJK JP")))
+   #+END_SRC
+   Org will adjust the fallback font list depending on the document's
+   contents. You can maintain this variable in a /grow-as-you-go/
+   fashion, adding new elements when you need them.  You can keep them,
+   because they will not be added in documents that don't need them.
+3. The font names reported by Org are the font names used by
+   =lualatex=.  The spaces may not be included in the system font
+   names.
+
+**** Creating CJK documents with ~xelatex~
+:PROPERTIES:
+:UNNUMBERED: t
+:END:
+
+You can also create CJK documents with ~xelatex~ and ~fontspec~
+without resorting to fallback fonts.  You will be able to control the
+fonts in the exported PDF by adding mappings for any of the =CJKmain=,
+=CJKsans= or =CJKmono= fonts to the configuration.  For example, using
+the following settings in your Org document header:
+
+#+BEGIN_SRC org
+,#+LANGUAGE: zh
+,#+LATEX_COMPILER: xelatex
+,#+LATEX_HEADER: \usepackage[AUTO]{babel}
+,#+LATEX_HEADER: \urlstyle{same}
+#+END_SRC
+
+#+texinfo: @noindent
+and adding the following to your file-local variables in the Org file:
+
+#+BEGIN_EXAMPLE
+# org-latex-fontspec-config: (("main" :font "Noto Sans")
+#   ("CJKmain" :font "Noto Serif CJK SC")
+#   ("CJKsans" :font "Noto Sans CJK SC")
+#   ("CJKmono" :font "Noto Sans Mono CJK SC"))
+#+END_EXAMPLE
+
+#+texinfo: @noindent
+you should be able to use =xelatex= to generate a Chinese document
+with the Simplified Chinese fonts supplied by the =Noto= font
+family[fn::Select fonts that match your language! For example, the
+fonts in the =Noto= family for Japanese are the =...CJK JP=
+specimens.].
+
 ** Markdown Export
 :PROPERTIES:
 :DESCRIPTION: Exporting to Markdown.
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index b7c894b1d..2ff46bf7c 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -43,6 +43,12 @@ header for =beamer= that includes theme configuration. For example:
 will make the Beamer exporter ignore the ~#+BEAMER_...~ keywords
 in the document that are related to theme configuration.

+*** The ~#+LANGUAGE~ keyword accepts a list of languages
+
+The ~#+LANGUAGE~ keyword now accepts a list of languages for the LaTeX
+and derived exporters. The information channel with return the full
+list through property ~:languages~ and the first through ~:language~.
+
 *** ~org-mouse~ tag and priority menus are now separate

 The "Tags and Priorities" section of the global context menu is split
@@ -287,6 +293,16 @@ information.
 Given the completed and total number of tasks, format the percent
 cookie =[N%]=.

+*** New custom variable ~org-latex-fontspec-config~
+
+When using LuaLaTeX or XeLaTeX, you can use the
+new custom variable ~org-latex-fontspec-config~ to change the
+document's fonts and add fallback fonts for missing scripts with
+LuaLaTeX, or generate PDFs with CJK fonts in the case of XeLaTeX.
+Refer to "Controlling font setup for LuaLaTeX and XeLaTeX" in the
+"LaTeX Export" chapter in the Org manual.
+
+
 ** Removed or renamed functions and variables

 *** ~org-babel-remote-temporary-directory~ is now obsolete
diff --git a/lisp/org.el b/lisp/org.el
index 294a5eb52..c34132202 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -21163,6 +21163,31 @@ (defun org-reftex-citation ()
 	       org--rds (list (list 'bib bib))))))
     (call-interactively 'reftex-citation)))

+;;; Emacs scripts in Org documents
+
+(defun org-get-string-scripts (str)
+  "Return the list of Emacs scripts in STR.
+
+This function is called by ox-latex and ox-beamer (initially) and
+returns a list of strings with the names of scripts in STR.
+An empty list implies that all characters in STR are Latin-1.
+
+Derived from the initial version proposed by Juan Manuel Macías in
+https://list.orgmode.org/orgmode/[email protected]/
+"
+  (save-match-data
+    (let ((scripts)
+          (match-offset t)
+          (offset 0))
+      (while match-offset
+        (setq match-offset (string-match "\\([^\u0000-\u007F\u0080-\u00FF\u0100-\u017F]\\)" str offset))
+        (when match-offset
+          (when-let* ((matched (match-string 0 str))
+                      (script (aref char-script-table (string-to-char matched))))
+            (setq offset (match-end 1))
+            (cl-pushnew (prin1-to-string script) scripts :test #'string=))))
+      scripts)))
+
 ;;;; Functions extending outline functionality

 (defun org-beginning-of-line (&optional n)
diff --git a/lisp/ox-beamer.el b/lisp/ox-beamer.el
index 8910d2246..4679377f0 100644
--- a/lisp/ox-beamer.el
+++ b/lisp/ox-beamer.el
@@ -36,6 +36,10 @@ (org-assert-version)
 (require 'cl-lib)
 (require 'ox-latex)

+;;; Function declarations
+
+;; (declare-function org-get-string-scripts "org" (str))
+
 ;; Install a default set-up for Beamer export.
 (unless (assoc "beamer" org-latex-classes)
   (add-to-list 'org-latex-classes
@@ -930,6 +934,7 @@ (defun org-beamer-template (contents info)
   "Return complete document string after Beamer conversion.
 CONTENTS is the transcoded contents string.  INFO is a plist
 holding export options."
+
   (let ((title (org-export-data (plist-get info :title) info))
 	(subtitle (org-export-data (plist-get info :subtitle) info))
         (beamer-class (plist-get info :latex-class)))
diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index c2b1eb79f..8c972c602 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -49,6 +49,7 @@ (defvar engrave-faces-latex-output-style)
 (defvar engrave-faces-current-preset-style)
 (defvar engrave-faces-latex-mathescape)

+(declare-function org-get-string-scripts "org" (str))
 
 ;;; Define Backend

@@ -114,6 +115,7 @@ (org-export-define-backend 'latex
 	        (if a (org-latex-export-to-pdf t s v b)
 		  (org-open-file (org-latex-export-to-pdf nil s v b)))))))
   :filters-alist '((:filter-options . org-latex-math-block-options-filter)
+                   (:filter-body . org-latex-get-font-list)
 		   (:filter-paragraph . org-latex-clean-invalid-line-breaks)
 		   (:filter-parse-tree org-latex-math-block-tree-filter
 				       org-latex-matrices-tree-filter
@@ -174,7 +176,9 @@ (org-export-define-backend 'latex
     (:latex-compiler "LATEX_COMPILER" nil org-latex-compiler)
     (:latex-descriptive-env "LATEX_DESCRIPTIVE_ENV" nil org-latex-descriptive-environment)
     (:latex-use-sans nil "latex-use-sans" org-latex-use-sans)
+    (:latex-fontspec-config nil nil org-latex-fontspec-config)
     ;; Redefine regular options.
+    (:languages "LANGUAGE" nil (list org-export-default-language) split)
     (:date "DATE" nil "\\today" parse)))


@@ -1611,9 +1615,43 @@ (defcustom org-latex-toc-include-unnumbered nil
   :type 'boolean
   :safe #'booleanp)

+;;;; Font management
+
+(defcustom org-latex-fontspec-config nil
+  "An alist with the configuration for the fontspec package.
+
+This configuration will be generated when using lualatex or xelatex.
+
+Each element is defined as
+(`font-name' . `font-plist')
+ where `font-name' one of \"main\", \"sans\", \"mono\" or \"math\"
+ and `font-plist' is a plist. The keys for this plist are
+  `:font':     font name for font installed in your system
+  `:features': string or list of strings with font features (optional).
+  `:fallback': an alist of (`script' . `mapping') to map _Emacs_ script names
+               to their fallback font (optional). The exporter will warn you
+               about scripts in your document that need a fallback font.
+
+Refer to \"Controlling font setup for LuaLaTeX and XeLaTeX\" in the
+\"LaTeX Export\" chapter in the Org manual."
+  :group 'org-export-latex
+  :package-version '(Org . "10.0")
+  :type 'alist
+  :safe  #'listp
+)
+
 
 ;;; Internal Functions

+(defun org-latex-get-font-list(contents _backend info)
+  "Add the Emacs script list into the INFO channel.
+  Used by org-latex-make-preamble to add fallback fonts for lualatex."
+  (prog1
+      contents
+      (let ((script-list (org-get-string-scripts contents)))
+        ;; (message "org-latex-get-font-list: %s" script-list)
+        (setq info (plist-put info :doc-scripts script-list)))))
+
 (defun org-latex--caption-above-p (element info)
   "Non-nil when caption is expected to be located above ELEMENT.
 INFO is a plist holding contextual information."
@@ -1850,7 +1888,127 @@ (defun org-latex-guess-polyglossia-language (header info)
 		  languages
 		  ""))
 	 t t header 0)))))
-
+;;;
+(defun org-latex--needs-math-font (info)
+  "Return t if INFO contains a :latex-fontspec-config with a math font."
+
+  (when-let* ((fontspec-config (plist-get info :latex-fontspec-config)))
+    (assoc-string "math" fontspec-config)))
+
+(defun org-latex--needs-xecjk (info)
+  "Return t if INFO contains a :latex-fontspec-config with a CJK font."
+
+  (let ((result nil)
+        (compiler (plist-get info :latex-compiler)))
+    (when-let* ((fontspec-config (plist-get info :latex-fontspec-config)))
+      (dolist (fontdef fontspec-config result)
+        (setq result (or result (string-prefix-p "CJK" (car fontdef))))))
+    (when result
+      (unless (equal "xelatex" compiler)
+        (warn "Defining CJK fonts and using %s!" compiler)))
+    result))
+;;;;
+(defun org-latex--fontspec-prelude (info)
+  "Return the fontspec configuration for the INFO channel as a string.
+
+If COMPILER is \"xelatex\", omit fallback font detection."
+  (let ((doc-scripts (plist-get info :doc-scripts))
+        (compiler (plist-get info :latex-compiler))
+        (fontspec-config (plist-get info :latex-fontspec-config))
+        (need-math (org-latex--needs-math-font info))
+        (need-cjk  (org-latex--needs-xecjk info))
+        (fallback-found)  ;; a list of fallbacks that are really needed
+        (fallback-alist)) ;; an alist (font_name . fallback-name)
+
+    ;; (message "fontspec-prelude")
+    (with-temp-buffer
+      ;; add all fonts with fallback to fallback-alist for lualatex
+      ;; leave empty for xelatex
+      (when (string= compiler "lualatex")
+        (dolist (fconfig fontspec-config)
+          (when-let* ((fname (car fconfig))
+                      (config-plist (cdr fconfig))
+                      (fallback (plist-get config-plist :fallback)))
+            (push (cons fname (concat "fallback_" fname)) fallback-alist))))
+      ;; (message "fallback-alist ==> %s" fallback-alist)
+      (when fallback-alist ;; if there are fonts with fallbacks (and we use lualatex)
+        (let ((directlua nil)) ;; Did we write the beginning of this block?
+          ;; create the directlua header
+          (dolist (fallback fallback-alist)
+            ;; (message "fallback ===> %s" fallback)
+            (when-let*
+                ((fbf-fname (car fallback))
+                 (fbf-name (cdr fallback))
+                 (fbf-plist (alist-get fbf-fname fontspec-config nil nil #'string=))
+                 (fbf-flist (plist-get fbf-plist :fallback)))
+              ;; collect all falbacks for scripts that are present in the doc
+              (let ((fallback-flist
+                     (cl-loop for fpair in fbf-flist
+                              ;; check (car fpair) is in document scripts
+                              ;; and the fallback is not already in the result
+                              when (and (member-ignore-case (car fpair) doc-scripts)
+                                        (null (member-ignore-case (cdr fpair) fresult)))
+                              collect (cdr fpair) into fresult
+                              finally return fresult)))
+                ;; (message "fallback-flist ==> %s" fallback-flist)
+                (when fallback-flist
+                  (unless directlua ;; add the heading before the first lua block
+                    (insert "\\directlua{\n")
+                    (setq directlua t))
+                  ;; (setq fallback-flist (cl-remove-duplicates fallback-flist
+                  ;;                                            :test #'string=))
+                  (setq fallback-found (append fallback-found (list fbf-name)))
+                  (insert (format " luaotfload.add_fallback (\"%s\",{\n" fbf-name))
+                  ;; Here we get the font fallbacks list
+                  (dolist (fname fallback-flist)
+                    ;; TODO; when (car fpair) in document charsets
+                    (unless (string-match-p ":" fname) ;; fallback declarations may omit ending ':'
+                      (setq fname (concat fname ":"))) ;; when they are plain font names
+                    (insert (format "  \"%s\",\n" fname)))
+                  (insert " })\n")))))
+          (when directlua ;; if we have found any lua fallbacks, close the lua block
+            (insert "}\n"))))
+      ;; (message "fallbacks: %s" fallback-alist)
+      (when need-math
+        (insert "\\RequirePackage{unicode-math}\n"))
+      (when need-cjk
+        (insert "\\usepackage{xeCJK}\n"))
+      (dolist (fpair fontspec-config)
+        (when-let* ((ffamily (car fpair))
+                    (fplist  (cdr fpair))
+                    (ffont (plist-get fplist :font)))
+          (insert (format "\\set%sfont{%s}" ffamily ffont))
+          ;; add the features
+          (let ((ffeatures (or (plist-get fplist :features)
+                               (plist-get fplist :props))))
+            (when (stringp ffeatures)
+              (setq ffeatures (list ffeatures))) ;; needs to be a list to concat a possible fallback
+            ;; (message "--> ffeatures: %s" ffeatures)
+            (when-let* ((fallback-fn (alist-get ffamily fallback-alist nil nil #'string=))
+                        (fallback-test (member fallback-fn fallback-found)) ;; nil if not found -> don't add to ffeatures
+                        (fallback-spec (format "RawFeature={fallback=%s}" fallback-fn)))
+              ;; Add the fallback font spec
+              (setq ffeatures (cl-concatenate #'list ffeatures (list fallback-spec))))
+            ;; (message "ffeatures %s" ffeatures)
+            (when ffeatures
+              (insert (org-latex--mk-options ffeatures))))
+          (insert "\n")))
+      (buffer-string))))
+;;;;
+(defun org-latex-guess-fontspec (header info)
+  "Add the fontspec package configuration passed in INFO to HEADER.
+
+When the HEADER contains \"\\usepackage{fontspec}\",
+and INFO contains fontspec font conguration and
+add the fontspec configuration after the package."
+    (if-let* ((_ (plist-get info :latex-fontspec-config))
+                (matched (string-match "\\\\usepackage{fontspec}" header))
+                (matcher (match-string 0 header))
+                (replacer (concat matcher "\n"
+                                  (org-latex--fontspec-prelude info))))
+        (string-replace matcher replacer header)
+      header))
+;;;
 (defun org-latex--remove-packages (pkg-alist info)
   "Remove packages based on the current LaTeX compiler.

@@ -2063,9 +2221,12 @@ ;;; Template
 (defun org-latex--mk-options (str)
   "Make STR be enclosed in [ ] or return an empty string if nil or empty.

+If STR is a list of strings, join them with \",\".
 If STR is nil or an empty string, return STR.
 If STR is a traditional LATEX_CLASS_OPTIONS enclosed in [ ], return it as is.
 If the square brackets are missing, return STR enclosed in square brackets."
+  (when (listp str)
+    (setq str (mapconcat #'identity str ",")))
   (if (or (not str) (length= str 0)) str
     (save-match-data  ; just in case it is used in a search/replace context
       (let ((str (concat "[" str "]"))) ; make sure it is enclosed in []
@@ -2097,7 +2258,8 @@ (defun org-latex-make-preamble (info &optional template snippet?)
                                       (format "\\DocumentMetadata{%s}" doc-metadata))
                                  (and (not snippet?)
                                       (plist-get info :latex-class-pre))
-		                 (if (not class-options) header
+		                 (if (or (not class-options)
+                                         (string-empty-p class-options)) header
 		                   (replace-regexp-in-string
 			            "^[ \t]*\\\\documentclass\\(\\(\\[[^]]*\\]\\)?\\)"
 			            class-options header t nil 1)))
@@ -2105,22 +2267,24 @@ (defun org-latex-make-preamble (info &optional template snippet?)
 	      (user-error "Unknown LaTeX class `%s'" class))))
     (org-latex-guess-polyglossia-language
      (org-latex-guess-babel-language
-      (org-latex-guess-inputenc
-       (org-element-normalize-string
-	(org-splice-latex-header
-	 class-template
-	 (org-latex--remove-packages org-latex-default-packages-alist info)
-	 (org-latex--remove-packages org-latex-packages-alist info)
-	 snippet?
-	 (mapconcat #'org-element-normalize-string
-		    (list (plist-get info :latex-header)
-			  (and (not snippet?)
-			       (plist-get info :latex-header-extra))
-                          (and (not snippet?)
-                               (plist-get info :latex-use-sans)
-                               "\\renewcommand*\\familydefault{\\sfdefault}"))
-
-		    ""))))
+      (org-latex-guess-fontspec
+       (org-latex-guess-inputenc
+        (org-element-normalize-string
+	 (org-splice-latex-header
+	  class-template
+	  (org-latex--remove-packages org-latex-default-packages-alist info)
+	  (org-latex--remove-packages org-latex-packages-alist info)
+	  snippet?
+	  (mapconcat #'org-element-normalize-string
+		     (list (plist-get info :latex-header)
+			   (and (not snippet?)
+			        (plist-get info :latex-header-extra))
+                           (and (not snippet?)
+                                (plist-get info :latex-use-sans)
+                                "\\renewcommand*\\familydefault{\\sfdefault}"))
+
+		     ""))))
+       info)
       info)
      info)))

@@ -2128,6 +2292,8 @@ (defun org-latex-template (contents info)
   "Return complete document string after LaTeX conversion.
 CONTENTS is the transcoded contents string.  INFO is a plist
 holding export options."
+  ;; Before doing anything else, add the script information
+  ;; to the INFO channel
   (let ((title (org-export-data (plist-get info :title) info))
 	(spec (org-latex--format-spec info)))
     (concat
diff --git a/lisp/ox.el b/lisp/ox.el
index 90c853d76..ac3ac42f0 100644
--- a/lisp/ox.el
+++ b/lisp/ox.el
@@ -111,6 +111,8 @@ (defconst org-export-options-alist
     (:date "DATE" nil nil parse)
     (:author "AUTHOR" nil user-full-name parse)
     (:email "EMAIL" nil user-mail-address t)
+    ;; This breaks language handling in exporters other than the LaTeX derived ones
+    ;; (:languages "LANGUAGE" nil (list org-export-default-language) split)
     (:language "LANGUAGE" nil org-export-default-language t)
     (:select-tags "SELECT_TAGS" nil org-export-select-tags split)
     (:exclude-tags "EXCLUDE_TAGS" nil org-export-exclude-tags split)
@@ -1429,15 +1431,22 @@ (defun org-export-get-environment (&optional backend subtreep ext-plist)
   ;; global options are read.
   (org-export--set-variables (org-export--list-bound-variables))
   ;; Get and prioritize export options...
-  (org-combine-plists
-   ;; ... from global variables...
-   (org-export--get-global-options backend)
-   ;; ... from an external property list...
-   ext-plist
-   ;; ... from in-buffer settings...
-   (org-export--get-inbuffer-options backend)
-   ;; ... and from subtree, when appropriate.
-   (and subtreep (org-export--get-subtree-options backend))))
+  (let ((info
+         (org-combine-plists
+          ;; ... from global variables...
+          (org-export--get-global-options backend)
+          ;; ... from an external property list...
+          ext-plist
+          ;; ... from in-buffer settings...
+          (org-export--get-inbuffer-options backend)
+          ;; ... and from subtree, when appropriate.
+          (and subtreep (org-export--get-subtree-options backend)))))
+    ;; Get :languages and insert it into :language
+    (when-let* ((languages (plist-get info :languages))
+                (lang (car languages)))
+      (setq info (plist-put info :language lang)))
+    info))
+

 (defun org-export--parse-option-keyword (options &optional backend)
   "Parse an OPTIONS line and return values as a plist.
diff --git a/testing/lisp/test-ox-latex.el b/testing/lisp/test-ox-latex.el
index be1086557..455241994 100644
--- a/testing/lisp/test-ox-latex.el
+++ b/testing/lisp/test-ox-latex.el
@@ -579,6 +579,110 @@ (ert-deftest test-ox-latex/example-env-options ()
    (should (search-forward "\\begin{Verbatim}[fontsize=\\footnotesize]" nil t))
    (should (search-forward "\\begin{Verbatim}[fontsize=\\HUGE]" nil t)))))

+(ert-deftest test-ox-latex/lualatex-fontspec-recognised ()
+  "Test that org-latex-fontspec-config is recognised for lualatex."
+  (let ((org-latex-fontspec-config
+         '(("main" :font "FreeSerif")
+           ("sans" :font "FreeSans"))))
+   (org-test-with-exported-text
+   'latex
+   "#+TITLE: LuaLaTeX fonts
+#+LANGUAGE: en-gb es
+#+OPTIONS: toc:nil H:3 num:nil
+#+LATEX_COMPILER: lualatex
+#+LATEX_CLASS: report
+* Testing
+
+Just to see that I get the fonts Iwant...
+"
+   ;; (message "simple fontspec: %s" (buffer-string))
+   (goto-char (point-min))
+   (should (search-forward "\\usepackage{fontspec}" nil t))
+   (should (search-forward "\\setmainfont{FreeSerif}" nil t))
+   (should (search-forward "\\setsansfont{FreeSans}" nil t)))))
+
+(ert-deftest test-ox-latex/lualatex-fontspec-fallback ()
+  "Test that org-latex-fontspec-config is recognised for lualatex.
+Emojis are added."
+  (let ((org-latex-fontspec-config
+         '(("main" :font "FreeSerif"
+            :fallback (("emoji" . "Noto Color Emoji:mode=harf")))
+           ("sans" :font "FreeSans"))))
+   (org-test-with-exported-text
+   'latex
+   "#+TITLE: LuaLaTeX fonts with emojis
+#+LANGUAGE: en-gb es
+#+OPTIONS: toc:nil H:3 num:nil
+#+LATEX_COMPILER: lualatex
+#+LATEX_CLASS: report
+* Testing
+
+Just to see that I get the fonts I want...
+
+And my emojis too, 😀
+"
+   ;; (message "lualatex fallback: %s" (buffer-string))
+   (goto-char (point-min))
+   (should (search-forward "\\usepackage{fontspec}" nil t))
+   (should (search-forward "\\directlua{" nil t))
+   (should (search-forward "\\setmainfont{FreeSerif}[RawFeature={fallback=" nil t))
+   (should (search-forward "\\setsansfont{FreeSans}" nil t)))))
+
+(ert-deftest test-ox-latex/lualatex-fontspec-latex-header-not-lost ()
+  "Test that org-latex-fontspec-config is recognised for lualatex.
+
+It will be placed *before* LATEX_HEADER, so that any font configuration
+there will prevail."
+  (let ((org-latex-fontspec-config
+         '(("main" :font "FreeSerif")
+           ("sans" :font "FreeSans"))))
+   (org-test-with-exported-text
+   'latex
+   "#+TITLE: LuaLaTeX fonts
+#+LANGUAGE: en-gb es
+#+OPTIONS: toc:nil H:3 num:nil
+#+LATEX_COMPILER: lualatex
+#+LATEX_HEADER: \\setsansfont{TeX Gyre Heros}
+#+LATEX_CLASS: report
+* Testing
+
+Just to see that I get the fonts Iwant...
+"
+   ;; (message "fontspec: latex-header\n%s" (buffer-string))
+   (goto-char (point-min))
+   (should (search-forward "\\usepackage{fontspec}" nil t))
+   (should (search-forward "\\setmainfont{FreeSerif}" nil t))
+   (should (search-forward "\\setsansfont{FreeSans}" nil t))
+   (should (search-forward "\\setsansfont{TeX Gyre Heros}" nil t)))))
+
+(ert-deftest test-ox-latex/back-in-time-cjk-xelatex ()
+  "Test that we use xeCKJ with xelatex throigh fontspec."
+  (let ((org-latex-fontspec-config '(("main" :font "Noto Sans")
+                                     ("CJKmain" :font "Noto Serif CJK SC")
+                                     ("CJKsans" :font "Noto Sans CJK SC")
+                                     ("CJKmono" :font "Noto Sans Mono CJK SC"))))
+   (org-test-with-exported-text
+   'latex
+   "#+TITLE: Test stuff
+#+OPTIONS: toc:nil H:3 num:nil
+#+LANGUAGE: zh
+#+LATEX_COMPILER: xelatex
+#+LATEX_HEADER: \\urlstyle{same}
+
+* 回到过去。
+
+这是第一个补丁版本里有的!
+
+我们说的是晚了一年多!
+"
+   (message "CJK xelatex --> \n%s" (buffer-string))
+   (goto-char (point-min))
+   (should (search-forward "\\usepackage{fontspec}" nil t))
+   (should (search-forward "\\usepackage{xeCJK}" nil t))
+   (should (search-forward "\\setmainfont{Noto Sans}" nil t))
+   (should (search-forward "\\setCJKmainfont{Noto Serif CJK SC}" nil t))
+   (should (search-forward "\\setCJKsansfont{Noto Sans CJK SC}" nil t))
+   (should (search-forward "\\setCJKmonofont{Noto Sans Mono CJK SC}" nil t)))))

 (provide 'test-ox-latex)
 ;;; test-ox-latex.el ends here
--
2.43.0

Reply via email to