emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
* [PATCH] Introduce "export features"
@ 2023-02-10 17:20 Timothy
  2023-02-11 11:37 ` Ihor Radchenko
  2023-02-20 17:41 ` Timothy
  0 siblings, 2 replies; 11+ messages in thread
From: Timothy @ 2023-02-10 17:20 UTC (permalink / raw)
  To: emacs-orgmode

[-- Attachment #1: Type: text/plain, Size: 2724 bytes --]

Hello everyone,

I’m thrilled to finally be presenting a feature that I’ve been incubating for a
while now that I call “export features”. This work is based on the observation
that often we include content in export templates that is only relevant in
particular situations.

This leaves one having to choose between a “kitchen sink” approach where
everything that could be used is all included, or a “manual inclusion” approach
where the template is minimal and the relevant setup code must be manually
included each time.

I think it’s fair to say neither situation is ideal, and I’ve become
dissatisfied enough that I’ve sunk some time into working on a better way of
handling this. In this patch set it’s just being applied to LaTeX preambles, but
this is just a start — there are plans to apply this more broadly.

“export features” allow for the specification of qualities of the org buffer
being exported that imply certain “features”, and how those features may be
implemented in a particular export.

This is done by augmenting the backend struct with two new fields:
`feature-conditions' and `feature-implementations'.

The feature conditions are resolved during the annotation of `info', in the Org
buffer after `#+include' expansion and the removal of comments.

The feature implementations are expanded by the backend itself, in the case of
`ox-latex' this currently means during preamble construction.

With this change, `ox-latex' produces more minimal /and/ more capable exports out of
the box. The number of default packages has been ~halved, but OOTB capability
has been improved by dynamically adding them to the preamble when needed, and
now `\usepackage{svg}' is automatically added when exporting a buffer that
includes SVG images.

This also opens new frontiers for user customisation. For instance, adding
particular beamer customisations when using the `metropolis' beamer theme with the
following snippet:

┌────
│ (org-export-update-features 'beamer
│   (beamer-metropolis
│    :condition (string-match-p \"metropolis$\" (plist-get info :beamer-theme))
│    :snippet my-org-beamer-metropolis-tweaks
│    :order 3))
└────


Hopefully this gives you an idea of the feature. See the patches attached for
the implementation (and some hopefully informative code comments). Feel free to
toss any you may have question my way :)

All the best,
Timothy

-- 
Timothy (‘tecosaur’/‘TEC’), Org mode contributor.
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/tec>.

[-- Attachment #2: 0001-ox-Introduce-conditional-generated-preamble.patch --]
[-- Type: text/x-patch, Size: 11529 bytes --]

From 5575a0f18277ef34f4003c1bccf650e4237e6048 Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Mon, 25 Jul 2022 23:37:13 +0800
Subject: [PATCH 1/6] ox: Introduce conditional/generated preamble

* lisp/ox.el (org-export-detect-features, org-export-expand-features,
org-export-generate-features-preamble): New functions for detecting
features and generating content based on them.
* lisp/ox.el (org-export-conditional-features): Customisation for
feature detection.
* lisp/ox.el (org-export-as): Add detected to features to info in the
slot :features.
---
 lisp/ox.el | 217 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 217 insertions(+)

diff --git a/lisp/ox.el b/lisp/ox.el
index 0a48e850a..1a75ed28d 100644
--- a/lisp/ox.el
+++ b/lisp/ox.el
@@ -2030,6 +2030,221 @@ (defun org-export-expand (blob contents &optional with-affiliated)
 	    (funcall (intern (format "org-element-%s-interpreter" type))
 		     blob contents))))
 
+\f
+;;; Conditional/Generated Preamble
+;;
+;; Many formats have some version of a preamble, whether it be HTML's
+;; <head>...</head> or the content before LaTeX's \begin{document}.
+;; Depending on the particular features in the Org document being
+;; exported, different setup snippets will be needed.  There's the
+;; "everything and the kitchen sink" approach of adding absolutely
+;; everything that might be needed, and the post-translation editing
+;; with filters approach, but neither really solve this problem nicely.
+;;
+;; The conditional/generated preamble defines mechanisms of detecting
+;; which "features" are used in a document, handles interactions
+;; between features, and provides/generates preamble content to
+;; support the features.
+
+(defcustom org-export-conditional-features
+  `(("^[ \t]*#\\+print_bibliography:" . bibliography)
+    (,(lambda (info)
+       (org-element-map (plist-get info :parse-tree)
+           'link
+         (lambda (link)
+           (and (member (org-element-property :type link)
+                        '("http" "https" "ftp" "file"))
+                (file-name-extension (org-element-property :path link))
+                (string= (downcase (file-name-extension
+                                    (org-element-property :path link)))
+                         "svg")))
+         info t))
+     . svg)
+    (,(lambda (info)
+       (org-element-map (plist-get info :parse-tree)
+           'link
+         (lambda (link)
+           (and (member (org-element-property :type link)
+                        '("http" "https" "ftp" "file"))
+                (file-name-extension (org-element-property :path link))
+                (member (downcase (file-name-extension
+                                   (org-element-property :path link)))
+                        image-file-name-extensions)))
+         info t))
+     . image)
+    (,(lambda (info)
+       (org-element-map (plist-get info :parse-tree)
+           'table #'identity info t))
+     . table)
+    (,(lambda (info)
+       (org-element-map (plist-get info :parse-tree)
+           '(src-block inline-src-block) #'identity info t))
+     . code))
+  "Org feature tests and associated feature flags.
+
+Alist where the car is a test for the presense of the feature,
+and the CDR is either a single feature symbol or a list of
+feature symbols.
+
+Feature tests can take any of the following forms:
+- Variable symbol, the value of which is fetched.
+- Function symbol, which is called with the export info
+  as the argument.
+- A string, which is used as a regexp search in the buffer.
+  The regexp matching is taken as confirmation of the existence
+  of the feature.
+
+When the test is a variable or function and produces a string
+value, that value is itself used as a test. Any other non-nil
+value will imply the existance of the feature."
+  :group 'org-export-general
+  :type '(alist :key-type
+                (choice (regexp :tag "Feature test regexp")
+                        (variable :tag "Feature variable")
+                        (function :tag "Feature test function"))
+                :value-type
+                (choice (symbol :tag "Feature symbol")
+                        (repeat symbol :tag "Feature symbols"))))
+
+(defun org-export-detect-features (info)
+  "Detect features from `org-export-conditional-features' in INFO."
+  (let (case-fold-search)
+    (delete-dups
+     (mapcan
+      (lambda (construct-feature)
+        (and (let ((out (pcase (car construct-feature)
+                          ((pred stringp) (car construct-feature))
+                          ((pred functionp)
+                           (funcall (car construct-feature) info))
+                          ((pred symbolp) (symbol-value (car construct-feature)))
+                          (_ (error "org-export-conditional-features key %s unable to be used" (car construct-feature))))))
+               (if (stringp out)
+                   (save-excursion
+                     (goto-char (point-min))
+                     (re-search-forward out nil t))
+                 out))
+             (if (listp (cdr construct-feature))
+                 (copy-sequence (cdr construct-feature))
+               (list (cdr construct-feature)))))
+      (append org-export-conditional-features
+              (plist-get info :conditional-features))))))
+
+(defun org-export-expand-features (info)
+  "Identify all implied implementations from features, in INFO.
+
+(plist-get info :feature-implementations) should be an alist of feature symbols
+and specification plists with the following keys:
+- :snippet, which is either,
+  - A string, which should be included in the preamble verbatim.
+  - A variable, the value of which should be included in the preamble.
+  - A function, which is called with two arguments — the export info,
+    and the list of feature flags.  The returned value is included in
+    the preamble.
+- :requires, a feature or list of features this feature will enable.
+- :when, a feature or list of features which are required for this
+  feature to be active.
+- :prevents, a feature or list of features that should be masked.
+- :order, for when inclusion order matters.  Feature implementations
+  with a lower order appear first.  The default is 0.
+
+This function processes :requires, :when, and :prevents in turn
+before finally sorting according to :order.
+
+After resolving the features, the :features key of INFO is
+updated to reflect the expanded set of features being used."
+  (let ((initial-features (plist-get info :features))
+        (implementations (plist-get info :feature-implementations))
+        required-features current-implementations)
+    ;; Process :requires.
+    (while initial-features
+      (push (car initial-features) required-features)
+      (setq initial-features
+            (if-let (requirements
+                     (plist-get (alist-get (car initial-features) implementations)
+                                :requires))
+                (if (consp requirements)
+                    (append requirements (cdr initial-features))
+                  (cons requirements (cdr initial-features)))
+              (cdr initial-features))))
+    ;; Get the implementations of required features.
+    (setq current-implementations
+          (mapcar (lambda (f) (assq f implementations))
+                  (delete-dups required-features)))
+    ;; Remove features with unfulfilled :when conditions.
+    (let ((processing t)
+          confirmed-features conditional-implementations
+          when)
+      ;; To correctly resolve all the various :when conditions,
+      ;; do not make any assumptions about which features are active.
+      ;; Initially only consider non-:when implementations to be
+      ;; active, then run through the list of unconfirmed :when
+      ;; implementations and check their conditions against the list
+      ;; of confirmed features.  Continue doing this until no more
+      ;; features are confirmed.
+      (dolist (impl current-implementations)
+        (if (plist-get (cdr impl) :when)
+            (push impl conditional-implementations)
+          (push (car impl) confirmed-features)))
+      (while processing
+        (setq processing nil)
+        (dolist (impl conditional-implementations)
+          (setq when (plist-get (cdr impl) :when))
+          (when (cond
+                 ((symbolp when)
+                  (memq when confirmed-features))
+                 ((consp when)
+                  (not (cl-set-difference when confirmed-features))))
+            (push (car impl) confirmed-features)
+            (setq conditional-implementations
+                  (delq impl conditional-implementations)
+                  processing t))))
+      ;; Now all that remains is implementations with unsatisfiable
+      ;; :when conditions.
+      (dolist (impl conditional-implementations)
+        (setq current-implementations
+              (delq impl current-implementations))))
+    ;; Get rid of prevented features.
+    (dolist (impl current-implementations)
+      (when-let ((prevented (pcase (plist-get (cdr impl) :prevents)
+                              ((and (pred consp) p) p)
+                              ((pred null) nil)
+                              ((and (pred atom) p) (list p)))))
+        (setq current-implementations
+              (cl-remove-if
+               (lambda (i) (memq (car i) prevented))
+               current-implementations))))
+    ;; Sort by :order.
+    (setq current-implementations
+          (sort current-implementations
+                (lambda (impl1 impl2)
+                  (< (or (plist-get (cdr impl1) :order) 0)
+                     (or (plist-get (cdr impl2) :order) 0)))))
+    ;; Update :features to reflect the features actually used.
+    (plist-put info :features (mapcar #'car current-implementations))
+    current-implementations))
+
+(defun org-export-generate-features-preamble (info)
+  "Generate preamble string according to features an implementations in INFO.
+More specifically, this function resolves feature implementations
+with `org-export-expand-features' and concatenates the snippets."
+  (let* ((feat-impl (org-export-expand-features info))
+         (feat-snippets
+          (mapcar
+           (lambda (impl)
+             (let ((snippet (plist-get (cdr impl) :snippet)))
+               (cond
+                ((null snippet) nil)
+                ((functionp snippet)
+                 (funcall snippet info))
+                ((symbolp snippet) (symbol-value snippet))
+                ((stringp snippet) snippet)
+                (t (error "org-export feature snippet %S is invalid." snippet)
+                   nil))))
+           feat-impl)))
+    (mapconcat
+     #'identity
+     (append (delq nil feat-snippets) (list ""))
+     "\n")))
 
 \f
 ;;; The Filter System
@@ -3186,6 +3401,8 @@ (defun org-export--annotate-info (backend info &optional subtreep visible-only e
     ;; the output of the selected citation export processor.
     (org-cite-process-citations info)
     (org-cite-process-bibliography info)
+    ;; With the complete tree, detect features.
+    (plist-put info :features (org-export-detect-features info))
     info))
 
 ;;;###autoload
-- 
2.39.0


[-- Attachment #3: 0002-ox-latex-Apply-new-generated-preamble-to-export.patch --]
[-- Type: text/x-patch, Size: 22798 bytes --]

From 064e802169ce4ba99c019e908eb945f2a96274ba Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Mon, 6 Feb 2023 00:01:26 +0800
Subject: [PATCH 2/6] ox-latex: Apply new generated preamble to export

* lisp/ox-latex.el (org-latex-template, org-latex-make-preamble,
org-latex-guess-polyglossia-language, org-latex-guess-babel-language,
org-latex-guess-inputenc, org-latex-generate-engraved-preamble):
Refactor to make use of the generated export, and add a few new bells
and whistles.

* lisp/ox-beamer.el (org-beamer-template): Adjust to account for the
changes in ox-latex.el.
---
 lisp/org.el       |  15 --
 lisp/ox-beamer.el |  10 +-
 lisp/ox-latex.el  | 353 +++++++++++++++++++++++++++++-----------------
 3 files changed, 223 insertions(+), 155 deletions(-)

diff --git a/lisp/org.el b/lisp/org.el
index 4d12084d9..4fb16d079 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -3385,14 +3385,6 @@ (defun org-get-packages-alist (var)
 (defcustom org-latex-default-packages-alist
   '(("AUTO" "inputenc"  t ("pdflatex"))
     ("T1"   "fontenc"   t ("pdflatex"))
-    (""     "graphicx"  t)
-    (""     "longtable" nil)
-    (""     "wrapfig"   nil)
-    (""     "rotating"  nil)
-    ("normalem" "ulem"  t)
-    (""     "amsmath"   t)
-    (""     "amssymb"   t)
-    (""     "capt-of"   nil)
     (""     "hyperref"  nil))
   "Alist of default packages to be inserted in the header.
 
@@ -3403,15 +3395,8 @@ (defcustom org-latex-default-packages-alist
 Org mode to function properly:
 
 - inputenc, fontenc:  for basic font and character selection
-- graphicx: for including images
-- longtable: For multipage tables
 - wrapfig: for figure placement
 - rotating: for sideways figures and tables
-- ulem: for underline and strike-through
-- amsmath: for subscript and superscript and math environments
-- amssymb: for various symbols used for interpreting the entities
-  in `org-entities'.  You can skip some of this package if you don't
-  use any of the symbols.
 - capt-of: for captions outside of floats
 - hyperref: for cross references
 
diff --git a/lisp/ox-beamer.el b/lisp/ox-beamer.el
index 5df78d5a4..8924b412b 100644
--- a/lisp/ox-beamer.el
+++ b/lisp/ox-beamer.el
@@ -821,9 +821,7 @@ (defun org-beamer-template (contents info)
      ;; Time-stamp.
      (and (plist-get info :time-stamp-file)
 	  (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))
-     ;; LaTeX compiler
-     (org-latex--insert-compiler info)
-     ;; Document class and packages.
+     ;; Document class, packages, and some configuration.
      (org-latex-make-preamble info)
      ;; Insert themes.
      (let ((format-theme
@@ -872,12 +870,6 @@ (defun org-beamer-template (contents info)
      (let ((template (plist-get info :latex-hyperref-template)))
        (and (stringp template)
 	    (format-spec template (org-latex--format-spec info))))
-     ;; engrave-faces-latex preamble
-     (when (and (eq org-latex-src-block-backend 'engraved)
-                (org-element-map (plist-get info :parse-tree)
-                    '(src-block inline-src-block) #'identity
-                    info t))
-       (org-latex-generate-engraved-preamble info))
      ;; Document start.
      "\\begin{document}\n\n"
      ;; Title command.
diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index dc2062df5..9794e6ecd 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -127,6 +127,8 @@ (org-export-define-backend 'latex
     (:description "DESCRIPTION" nil nil parse)
     (:keywords "KEYWORDS" nil nil parse)
     (:subtitle "SUBTITLE" nil nil parse)
+    (:conditional-features nil nil org-latex-conditional-features)
+    (:feature-implementations nil nil org-latex-feature-implementations)
     ;; Other variables.
     (:latex-active-timestamp-format nil nil org-latex-active-timestamp-format)
     (:latex-caption-above nil nil org-latex-caption-above)
@@ -1378,6 +1380,95 @@ (defun org-latex-generate-engraved-preamble (info)
        "% WARNING syntax highlighting unavailable as engrave-faces-latex was missing.\n")
      "\n")))
 
+;;;; Generated preamble
+
+(defcustom org-latex-conditional-features
+  `((t . !announce-start)
+    (t . !announce-end)
+    (t . !guess-pollyglossia)
+    (t . !guess-babel)
+    (t . !guess-inputenc)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            '(latex-fragment latex-environment) #'identity info t))
+     . maths)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            'underline #'identity info t))
+     . underline)
+    ("\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith"
+     . underline)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            'link
+          (lambda (link)
+            (and (member (org-element-property :type link)
+                         '("http" "https" "ftp" "file"))
+                 (file-name-extension (org-element-property :path link))
+                 (equal (downcase (file-name-extension
+                                   (org-element-property :path link)))
+                        "svg")))
+          info t))
+     . svg)
+    (org-latex-tables-booktabs . booktabs)
+    (,(lambda (info)
+        (eq (plist-get info :latex-src-block-backend) 'engraved))
+     . engraved-code)
+    ("^[ \t]*#\\+attr_latex: .*:float +wrap"
+     . float-wrap)
+    ("^[ \t]*#\\+attr_latex: .*:float +sideways"
+     . rotate)
+    ("^[ \t]*#\\+caption:\\|\\\\caption{" . caption))
+  "A LaTeX-specific extension to `org-export-conditional-features', which see.")
+
+(defcustom org-latex-feature-implementations
+  `((!announce-start
+     :snippet ,(lambda (info)
+                 (format "\n%%%% ox-latex features: %s"
+                         (plist-get info :features)))
+     :order -100)
+    (maths :snippet "\\usepackage{amsmath}\n\\usepackage{amssymb}" :order 0.2)
+    (underline :snippet "\\usepackage[normalem]{ulem}" :order 0.5)
+    (image :snippet "\\usepackage{graphicx}" :order 2)
+    (svg :snippet "\\usepackage[inkscapelatex=false]{svg}" :order 2 :when image)
+    (longtable :snippet "\\usepackage{longtable}" :when table :order 2)
+    (booktabs :snippet "\\usepackage{booktabs}" :when table :order 2)
+    (float-wrap :snippet "\\usepackage{wrapfig}" :order 2)
+    (rotate :snippet "\\usepackage{rotating}" :order 2)
+    (caption :snippet "\\usepackage{capt-of}")
+    (engraved-code :snippet org-latex-generate-engraved-preamble :when code)
+    (!guess-pollyglossia :snippet org-latex-guess-polyglossia-language)
+    (!guess-babel :snippet org-latex-guess-babel-language)
+    (!guess-inputenc :snippet org-latex-guess-inputenc)
+    (!announce-end :snippet "%% end ox-latex features\n" :order 100))
+  "Alist describing how export features should be supported in the preamble.
+
+Implementation alist has the feature symbol as the car, with the
+cdr forming a plist with the following keys:
+- :snippet, which is either,
+  - A string, which should be included in the preamble verbatim.
+  - A variable, the value of which should be included in the preamble.
+  - A function, which is called with two arguments — the export info,
+    and the list of feature flags. The returned value is included in
+    the preamble.
+- :requires, a feature or list of features which are needed.
+- :when, a feature or list of features which imply this feature.
+- :prevents, a feature or list of features that should be masked.
+- :order, for when inclusion order matters. Feature implementations
+  with a lower order appear first.  The default is 0."
+  :group 'org-export-general
+  :type '(plist :key-type
+          (choice (const :snippet)
+                  (const :requires)
+                  (const :when)
+                  (const :prevents)
+                  (const :order)
+                  (const :trigger))
+          :value-type
+          (choice (string :tag "Verbatim content")
+                  (variable :tag "Content variable")
+                  (function :tag "Generating function"))))
+
 ;;;; Compilation
 
 (defcustom org-latex-compiler-file-string "%% Intended LaTeX compiler: %s\n"
@@ -1620,29 +1711,29 @@ (defun org-latex--caption/label-string (element info)
 	      (org-trim label)
 	      (org-export-data main info))))))
 
-(defun org-latex-guess-inputenc (header)
+(defun org-latex-guess-inputenc (info)
   "Set the coding system in inputenc to what the buffer is.
 
-HEADER is the LaTeX header string.  This function only applies
-when specified inputenc option is \"AUTO\".
+INFO is the plist used as a communication channel.
+This function only applies when specified inputenc option is \"AUTO\".
 
 Return the new header, as a string."
-  (let* ((cs (or (ignore-errors
-		   (latexenc-coding-system-to-inputenc
-		    (or org-export-coding-system buffer-file-coding-system)))
-		 "utf8")))
-    (if (not cs) header
+  (let ((header (plist-get info :latex-full-header))
+        (cs (or (ignore-errors
+                  (latexenc-coding-system-to-inputenc
+                   (or org-export-coding-system buffer-file-coding-system)))
+                "utf8")))
+    (when (and cs (string-match "\\\\usepackage\\[\\(AUTO\\)\\]{inputenc}" header))
       ;; First translate if that is requested.
       (setq cs (or (cdr (assoc cs org-latex-inputenc-alist)) cs))
-      ;; Then find the \usepackage statement and replace the option.
-      (replace-regexp-in-string "\\\\usepackage\\[\\(AUTO\\)\\]{inputenc}"
-				cs header t nil 1))))
+      (plist-put info :latex-full-header
+                 (replace-match cs t t header 1))))
+  nil)
 
-(defun org-latex-guess-babel-language (header info)
+(defun org-latex-guess-babel-language (info)
   "Set Babel's language according to LANGUAGE keyword.
 
-HEADER is the LaTeX header string.  INFO is the plist used as
-a communication channel.
+INFO is the plist used as a communication channel.
 
 Insertion of guessed language only happens when Babel package has
 explicitly been loaded.  Then it is added to the rest of
@@ -1656,50 +1747,46 @@ (defun org-latex-guess-babel-language (header info)
 
 Return the new header."
   (let* ((language-code (plist-get info :language))
-	 (plist (cdr
-		 (assoc language-code org-latex-language-alist)))
-	 (language (plist-get plist :babel))
-	 (language-ini-only (plist-get plist :babel-ini-only))
-	 ;; If no language is set, or Babel package is not loaded, or
-	 ;; LANGUAGE keyword value is a language served by Babel
-	 ;; exclusively through ini files, return HEADER as-is.
-	 (header (if (or language-ini-only
-			 (not (stringp language-code))
-			 (not (string-match "\\\\usepackage\\[\\(.*\\)\\]{babel}" header)))
-		     header
-		   (let ((options (save-match-data
-				    (org-split-string (match-string 1 header) ",[ \t]*"))))
-		     ;; If LANGUAGE is already loaded, return header
-		     ;; without AUTO.  Otherwise, replace AUTO with language or
-		     ;; append language if AUTO is not present.  Languages that are
-		     ;; served in Babel exclusively through ini files are not added
-		     ;; to the babel argument, and must be loaded using
-		     ;; `\babelprovide'.
-		     (replace-match
-		      (mapconcat (lambda (option) (if (equal "AUTO" option) language option))
-				 (cond ((member language options) (delete "AUTO" options))
-				       ((member "AUTO" options) options)
-				       (t (append options (list language))))
-				 ", ")
-		      t nil header 1)))))
+         (plist (cdr (assoc language-code org-latex-language-alist)))
+         (language (plist-get plist :babel))
+         (header (plist-get info :latex-full-header))
+         (language-ini-only (plist-get plist :babel-ini-only))
+         (babel-header-options
+          ;; If no language is set, or Babel package is not loaded, or
+          ;; LANGUAGE keyword value is a language served by Babel
+          ;; exclusively through ini files, return HEADER as-is.
+          (and (not language-ini-only)
+               (stringp language-code)
+               (string-match "\\\\usepackage\\[\\(.*\\)\\]{babel}" header)
+               (let ((options (save-match-data
+                                (org-split-string (match-string 1 header) ",[ \t]*"))))
+                 (cond ((member language options) (delete "AUTO" options))
+                       ((member "AUTO" options) options)
+                       (t (append options (list language))))))))
+    (when babel-header-options
+      ;; If AUTO is present in the header options, replace it with `language'.
+      (setq header
+            (replace-match
+             (mapconcat (lambda (option) (if (equal "AUTO" option) language option))
+                        babel-header-options
+                        ", ")
+             t nil header 1)))
     ;; If `\babelprovide[args]{AUTO}' is present, AUTO is
     ;; replaced by LANGUAGE.
-    (if (not (string-match "\\\\babelprovide\\[.*\\]{\\(.+\\)}" header))
-	header
-      (let ((prov (match-string 1 header)))
-	(if (equal "AUTO" prov)
-	    (replace-regexp-in-string (format
-				       "\\(\\\\babelprovide\\[.*\\]\\)\\({\\)%s}" prov)
-				      (format "\\1\\2%s}"
-					      (or language language-ini-only))
-				      header t)
-	  header)))))
-
-(defun org-latex-guess-polyglossia-language (header info)
+    (when (string-match "\\\\babelprovide\\[.*\\]{AUTO}" header)
+      (setq header
+            (replace-regexp-in-string
+             (format
+              "\\(\\\\babelprovide\\[.*\\]\\)\\({\\)%s}" prov)
+             (format "\\1\\2%s}" (or language language-ini-only))
+             header t)))
+    (plist-put info :latex-full-header header))
+  nil)
+
+(defun org-latex-guess-polyglossia-language (info)
   "Set the Polyglossia language according to the LANGUAGE keyword.
 
-HEADER is the LaTeX header string.  INFO is the plist used as
-a communication channel.
+INFO is the plist used as a communication channel.
 
 Insertion of guessed language only happens when the Polyglossia
 package has been explicitly loaded.
@@ -1710,48 +1797,50 @@ (defun org-latex-guess-polyglossia-language (header info)
 using \setdefaultlanguage and not as an option to the package.
 
 Return the new header."
-  (let* ((language (plist-get info :language)))
+  (let ((header (plist-get info :latex-full-header))
+        (language (plist-get info :language)))
     ;; If no language is set or Polyglossia is not loaded, return
     ;; HEADER as-is.
-    (if (or (not (stringp language))
-	    (not (string-match
-		  "\\\\usepackage\\(?:\\[\\([^]]+?\\)\\]\\){polyglossia}\n"
-		  header)))
-	header
+    (when (and (stringp language)
+               (string-match
+                "\\\\usepackage\\(?:\\[\\([^]]+?\\)\\]\\){polyglossia}\n"
+                header))
       (let* ((options (org-string-nw-p (match-string 1 header)))
-	     (languages (and options
-			     ;; Reverse as the last loaded language is
-			     ;; the main language.
-			     (nreverse
-			      (delete-dups
-			       (save-match-data
-				 (org-split-string
-				  (replace-regexp-in-string
-				   "AUTO" language options t)
-				  ",[ \t]*"))))))
-	     (main-language-set
-	      (string-match-p "\\\\setmainlanguage{.*?}" header)))
-	(replace-match
-	 (concat "\\usepackage{polyglossia}\n"
-		 (mapconcat
-		  (lambda (l)
-		    (let* ((plist (cdr
-				   (assoc language org-latex-language-alist)))
-			   (polyglossia-variant (plist-get plist :polyglossia-variant))
-			   (polyglossia-lang (plist-get plist :polyglossia))
-			   (l (if (equal l language)
-				  polyglossia-lang
-				l)))
-		      (format (if main-language-set (format "\\setotherlanguage{%s}\n" l)
-				(setq main-language-set t)
-				"\\setmainlanguage%s{%s}\n")
-			      (if polyglossia-variant
-				  (format "[variant=%s]" polyglossia-variant)
-				"")
-			      l)))
-		  languages
-		  ""))
-	 t t header 0)))))
+             (languages (and options
+                             ;; Reverse as the last loaded language is
+                             ;; the main language.
+                             (nreverse
+                              (delete-dups
+                               (save-match-data
+                                 (org-split-string
+                                  (replace-regexp-in-string
+                                   "AUTO" language options t)
+                                  ",[ \t]*"))))))
+             (main-language-set
+              (string-match-p "\\\\setmainlanguage{.*?}" header))
+             (polyglossia-modified-header
+              (replace-match
+               (concat "\\usepackage{polyglossia}\n"
+                       (mapconcat
+                        (lambda (l)
+                          (let* ((plist (cdr (assoc language org-latex-language-alist)))
+                                 (polyglossia-variant (plist-get plist :polyglossia-variant))
+                                 (polyglossia-lang (plist-get plist :polyglossia))
+                                 (l (if (equal l language)
+                                        polyglossia-lang
+                                      l)))
+                            (format (if main-language-set (format "\\setotherlanguage{%s}\n" l)
+                                      (setq main-language-set t)
+                                      "\\setmainlanguage%s{%s}\n")
+                                    (if polyglossia-variant
+                                        (format "[variant=%s]" polyglossia-variant)
+                                      "")
+                                    l)))
+                        languages
+                        ""))
+               t t header 0)))
+        (plist-put info :latex-full-header polyglossia-modified-header))))
+  nil)
 
 (defun org-latex--remove-packages (pkg-alist info)
   "Remove packages based on the current LaTeX compiler.
@@ -1952,32 +2041,45 @@ (defun org-latex-make-preamble (info &optional template snippet?)
 specified in `org-latex-default-packages-alist' or
 `org-latex-packages-alist'."
   (let* ((class (plist-get info :latex-class))
-	 (class-template
-	  (or template
-	      (let* ((class-options (plist-get info :latex-class-options))
-		     (header (nth 1 (assoc class (plist-get info :latex-classes)))))
-		(and (stringp header)
-		     (if (not class-options) header
-		       (replace-regexp-in-string
-			"^[ \t]*\\\\documentclass\\(\\(\\[[^]]*\\]\\)?\\)"
-			class-options header t nil 1))))
-	      (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)))
-		    ""))))
-      info)
-     info)))
+         (class-template
+          (or template
+              (let* ((class-options (plist-get info :latex-class-options))
+                     (header (nth 1 (assoc class (plist-get info :latex-classes)))))
+                (and (stringp header)
+                     (if (not class-options) header
+                       (replace-regexp-in-string
+                        "^[ \t]*\\\\documentclass\\(\\(\\[[^]]*\\]\\)?\\)"
+                        class-options header t nil 1))))
+              (user-error "Unknown LaTeX class `%s'" class)))
+         generated-preamble)
+    (plist-put info :latex-full-header
+               (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)))
+                            ""))))
+    (setq generated-preamble
+          (if snippet?
+              (progn
+                (org-latex-guess-inputenc info)
+                (org-latex-guess-babel-language info)
+                (org-latex-guess-polyglossia-language info)
+                "\n% Generated preamble omitted for snippets.")
+            (org-export-generate-features-preamble info)))
+    (concat
+     ;; Time-stamp.
+     (and (plist-get info :time-stamp-file)
+          (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))
+     ;; LaTeX compiler.
+     (org-latex--insert-compiler info)
+     (plist-get info :latex-full-header)
+     generated-preamble)))
 
 (defun org-latex-template (contents info)
   "Return complete document string after LaTeX conversion.
@@ -1986,12 +2088,7 @@ (defun org-latex-template (contents info)
   (let ((title (org-export-data (plist-get info :title) info))
 	(spec (org-latex--format-spec info)))
     (concat
-     ;; Time-stamp.
-     (and (plist-get info :time-stamp-file)
-	  (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))
-     ;; LaTeX compiler.
-     (org-latex--insert-compiler info)
-     ;; Document class and packages.
+     ;; Timestamp, compiler statement, document class and packages.
      (org-latex-make-preamble info)
      ;; Possibly limit depth for headline numbering.
      (let ((sec-num (plist-get info :section-numbers)))
@@ -2028,12 +2125,6 @@ (defun org-latex-template (contents info)
      (let ((template (plist-get info :latex-hyperref-template)))
        (and (stringp template)
             (format-spec template spec)))
-     ;; engrave-faces-latex preamble
-     (when (and (eq org-latex-src-block-backend 'engraved)
-                (org-element-map (plist-get info :parse-tree)
-                    '(src-block inline-src-block) #'identity
-                    info t))
-       (org-latex-generate-engraved-preamble info))
      ;; Document start.
      "\\begin{document}\n\n"
      ;; Title command.
-- 
2.39.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-ox-Feature-condition-implementation-convenience.patch --]
[-- Type: text/x-patch, Size: 3049 bytes --]

From ae319fccc64f35c52a247633c67f72c924396b03 Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Sun, 5 Feb 2023 12:34:55 +0800
Subject: [PATCH 3/6] ox: Feature condition/implementation convenience

* lisp/ox.el (org-export-update-features): Add a convenience function
for users to edit the feature condition/implementation lists.
---
 lisp/ox.el | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/lisp/ox.el b/lisp/ox.el
index 1a75ed28d..32f1c6016 100644
--- a/lisp/ox.el
+++ b/lisp/ox.el
@@ -2246,6 +2246,48 @@ (defun org-export-generate-features-preamble (info)
      (append (delq nil feat-snippets) (list ""))
      "\n")))
 
+(defun org-export-update-features (backend &rest feature-property-value-lists)
+  "For BACKEND's export spec, set all FEATURE-PROPERTY-VALUE-LISTS.
+
+Specifically, for each (FEATURE . PROPERTY-VALUE-LIST) entry of
+FEATURE-PROPERTY-VALUE-LISTS, each :PROPERTY VALUE pair of
+PROPERTY-VALUE-PAIRS is set to VALUE within the backend's feature
+implementation plist.  The sole exception to this is the
+:condition property, the value of which is set in the backend's
+feature condition plist instead.
+
+This can be used to both modify existing entries, and create new ones.
+
+\(fn BACKEND &rest (FEATURE PROPERTY-VALUE-PAIRS...)...)"
+  (declare (indent 1))
+  (let ((backend-var (intern (format "org-%s-feature-implementations" backend)))
+        (backend-cf (intern (format "org-%s-conditional-features" backend))))
+    (unless (boundp backend-var)
+      (error "Feature implementations for %s cannot be set as %s is undefined"
+             backend backend-var))
+    (dolist (feature-property-value-set feature-property-value-lists)
+      (let ((feature (car feature-property-value-set))
+            (property-value-pairs (copy-sequence (cdr feature-property-value-set))))
+        (while property-value-pairs
+          (if (eq (car property-value-pairs) :condition)
+              (let ((condition (progn (pop property-value-pairs)
+                                      (pop property-value-pairs))))
+                (unless (boundp backend-cf)
+                  (error "Feature condition for %s cannot be set as %s is undefined"
+                         backend backend-cf))
+                (cond
+                 ((rassoc feature (symbol-value backend-cf))
+                  (if condition
+                      (setcar (rassoc feature (symbol-value backend-cf))
+                              condition)
+                    (set backend-cf (delq (rassoc feature (symbol-value backend-cf))
+                                          (symbol-value backend-cf)))))
+                 (condition
+                  (add-to-list backend-cf (cons condition feature)))))
+            (setf (alist-get feature (symbol-value backend-var))
+                  (plist-put (alist-get feature (symbol-value backend-var))
+                             (pop property-value-pairs) (pop property-value-pairs)))))))))
+
 \f
 ;;; The Filter System
 ;;
-- 
2.39.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-oc-Make-use-of-conditional-preamble-for-export.patch --]
[-- Type: text/x-patch, Size: 9635 bytes --]

From 70bd6bcf11597d9865dba107a44030e1da17154b Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Mon, 6 Feb 2023 00:01:41 +0800
Subject: [PATCH 4/6] oc-*: Make use of conditional preamble for export

* lisp/oc-natbib.el (org-cite-natbib-use-package): Refactor to make use
of the conditional/generated preamble.

* lisp/oc-csl.el (org-cite-csl-finalizer): Refactor to make use of
the conditional/generated preamble.

* lisp/oc-biblatex.el (org-cite-biblatex-prepare-preamble): Refactor to
make use of the conditional/generated preamble.
---
 lisp/oc-biblatex.el | 82 ++++++++++++++++++---------------------------
 lisp/oc-csl.el      | 16 ---------
 lisp/oc-natbib.el   | 33 +++++++-----------
 lisp/ox-latex.el    | 15 +++++++++
 4 files changed, 61 insertions(+), 85 deletions(-)

diff --git a/lisp/oc-biblatex.el b/lisp/oc-biblatex.el
index b2d31f0f6..e8e420891 100644
--- a/lisp/oc-biblatex.el
+++ b/lisp/oc-biblatex.el
@@ -375,61 +375,45 @@ (defun org-cite-biblatex-export-citation (citation style _ info)
        (other
         (user-error "Invalid entry %S in `org-cite-biblatex-styles'" other))))))
 
-(defun org-cite-biblatex-prepare-preamble (output _keys files style &rest _)
-  "Prepare document preamble for \"biblatex\" usage.
-
-OUTPUT is the final output of the export process.  FILES is the list of file
-names used as the bibliography.
-
-This function ensures \"biblatex\" package is required.  It also adds resources
-to the document, and set styles."
-  (with-temp-buffer
-    (save-excursion (insert output))
-    (when (search-forward "\\begin{document}" nil t)
-      ;; Ensure there is a \usepackage{biblatex} somewhere or add one.
-      ;; Then set options.
-      (goto-char (match-beginning 0))
-      (let ((re (rx "\\usepackage"
-                    (opt (group "[" (*? anything) "]"))
-                    "{biblatex}")))
-        (cond
-         ;; No "biblatex" package loaded.  Insert "usepackage" command
-         ;; with appropriate options, including style.
-         ((not (re-search-backward re nil t))
-          (save-excursion
-            (insert
-             (format "\\usepackage%s{biblatex}\n"
-                     (org-cite-biblatex--package-options
-                      org-cite-biblatex-options style)))))
-         ;; "biblatex" package loaded, but without any option.
-         ;; Include style only.
-         ((not (match-beginning 1))
-          (search-forward "{" nil t)
-          (insert (org-cite-biblatex--package-options nil style)))
-         ;; "biblatex" package loaded with some options set.  Override
-         ;; style-related options with ours.
-         (t
-          (replace-match
-           (save-match-data
-             (org-cite-biblatex--package-options (match-string 1) style))
-           nil nil nil 1))))
-      ;; Insert resources below.
-      (forward-line)
-      (insert (mapconcat (lambda (f)
-                           (format "\\addbibresource%s{%s}"
-                                   (if (org-url-p f) "[location=remote]" "")
-                                   f))
-                         files
-                         "\n")
-              "\n"))
-    (buffer-string)))
+(defun org-cite-biblatex--generate-latex-preamble (info)
+  "Ensure that the biblatex package is loaded, and the necessary resources.
+This is performed by extracting relevant information from the
+INFO export plist, and modifying any existing
+\\usepackage{biblatex} statement in the LaTeX header."
+  (let ((style (org-cite-bibliography-style info))
+        (files (plist-get info :bibliography))
+        (usepackage-rx (rx "\\usepackage"
+                           (opt (group "[" (*? anything) "]"))
+                           "{biblatex}")))
+    (concat
+     (if (string-match usepackage-rx (plist-get info :latex-full-header))
+         ;; "biblatex" package loaded, but with none (or different) options.
+         ;; Replace with style-including command.
+         (plist-put info :latex-full-header
+                    (replace-match
+                     (format "\\usepackage%s{biblatex}"
+                             (save-match-data
+                               (org-cite-biblatex--package-options nil style)))
+                     t t
+                     (plist-get info :latex-full-header)))
+       ;; No "biblatex" package loaded.  Insert "usepackage" command
+       ;; with appropriate options, including style.
+       (format "\\usepackage%s{biblatex}\n"
+               (org-cite-biblatex--package-options
+                org-cite-biblatex-options style)))
+     ;; Load resources.
+     (mapconcat (lambda (f)
+                  (format "\\addbibresource%s{%s}"
+                          (if (org-url-p f) "[location=remote]" "")
+                          f))
+                files
+                "\n"))))
 
 \f
 ;;; Register `biblatex' processor
 (org-cite-register-processor 'biblatex
   :export-bibliography #'org-cite-biblatex-export-bibliography
   :export-citation #'org-cite-biblatex-export-citation
-  :export-finalizer #'org-cite-biblatex-prepare-preamble
   :cite-styles #'org-cite-biblatex-list-styles)
 
 (provide 'oc-biblatex)
diff --git a/lisp/oc-csl.el b/lisp/oc-csl.el
index 432738a97..f85646379 100644
--- a/lisp/oc-csl.el
+++ b/lisp/oc-csl.el
@@ -840,27 +840,11 @@ (defun org-cite-csl-render-bibliography (_keys _files _style props _backend info
        ;; process.
        (org-cite-parse-elements output)))))
 
-(defun org-cite-csl-finalizer (output _keys _files _style _backend info)
-  "Add \"hanging\" package if missing from LaTeX output.
-OUTPUT is the export document, as a string.  INFO is the export state, as a
-property list."
-  (org-cite-csl--barf-without-citeproc)
-  (if (not (eq 'org-latex (org-cite-csl--output-format info)))
-      output
-    (with-temp-buffer
-      (save-excursion (insert output))
-      (when (search-forward "\\begin{document}" nil t)
-	(goto-char (match-beginning 0))
-	;; Insert the CSL-specific parts of the LaTeX preamble.
-	(insert (org-cite-csl--generate-latex-preamble info)))
-      (buffer-string))))
-
 \f
 ;;; Register `csl' processor
 (org-cite-register-processor 'csl
   :export-citation #'org-cite-csl-render-citation
   :export-bibliography #'org-cite-csl-render-bibliography
-  :export-finalizer #'org-cite-csl-finalizer
   :cite-styles
   '((("author" "a") ("bare" "b") ("caps" "c") ("full" "f") ("bare-caps" "bc") ("caps-full" "cf") ("bare-caps-full" "bcf"))
     (("noauthor" "na") ("bare" "b") ("caps" "c") ("bare-caps" "bc"))
diff --git a/lisp/oc-natbib.el b/lisp/oc-natbib.el
index 855be2a5c..b5cb193e7 100644
--- a/lisp/oc-natbib.el
+++ b/lisp/oc-natbib.el
@@ -157,32 +157,25 @@ (defun org-cite-natbib-export-citation (citation style _ info)
           (org-cite-natbib--build-optional-arguments citation info)
           (org-cite-natbib--build-arguments citation)))
 
-(defun org-cite-natbib-use-package (output &rest _)
-  "Ensure output requires \"natbib\" package.
-OUTPUT is the final output of the export process."
-  (with-temp-buffer
-    (save-excursion (insert output))
-    (when (search-forward "\\begin{document}" nil t)
-      ;; Ensure there is a \usepackage{natbib} somewhere or add one.
-      (goto-char (match-beginning 0))
-      (let ((re (rx "\\usepackage" (opt "[" (*? nonl) "]") "{natbib}")))
-        (unless (re-search-backward re nil t)
-          (insert
-           (format "\\usepackage%s{natbib}\n"
-                   (if (null org-cite-natbib-options)
-                       ""
-                     (format "[%s]"
-                             (mapconcat #'symbol-name
-                                        org-cite-natbib-options
-                                        ","))))))))
-    (buffer-string)))
+(defun org-cite-natbib--generate-latex-preamble (info)
+  "Ensure that the \"natbib\" package is loaded.
+INFO is a plist used as a communication channel."
+  (and (not (string-match
+             (rx "\\usepackage" (opt "[" (*? nonl) "]") "{natbib}")
+             (plist-get info :latex-full-header)))
+       (format "\\usepackage%s{natbib}\n"
+               (if (null org-cite-natbib-options)
+                   ""
+                 (format "[%s]"
+                         (mapconcat #'symbol-name
+                                    org-cite-natbib-options
+                                    ","))))))
 
 \f
 ;;; Register `natbib' processor
 (org-cite-register-processor 'natbib
   :export-bibliography #'org-cite-natbib-export-bibliography
   :export-citation #'org-cite-natbib-export-citation
-  :export-finalizer #'org-cite-natbib-use-package
   :cite-styles
   '((("author" "a") ("caps" "a") ("full" "f"))
     (("noauthor" "na") ("bare" "b"))
diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index 9794e6ecd..040824f45 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -1468,6 +1468,21 @@ (defcustom org-latex-feature-implementations
           (choice (string :tag "Verbatim content")
                   (variable :tag "Content variable")
                   (function :tag "Generating function"))))
+;; Citation features
+
+(org-export-update-features 'latex
+  (bibliography-csl
+   :condition (eq (org-cite-processor info) 'csl)
+   :when bibliography
+   :snippet org-cite-csl--generate-latex-preamble)
+  (bibliography-biblatex
+   :condition (eq (org-cite-processor info) 'biblatex)
+   :when bibliography
+   :snippet org-cite-biblatex--generate-latex-preamble)
+  (bibliography-natbib
+   :condition (eq (org-cite-processor info) 'natbib)
+   :when bibliography
+   :snippet org-cite-natbib--generate-latex-preamble))
 
 ;;;; Compilation
 
-- 
2.39.0


[-- Attachment #6: 0005-ox-Add-struct-feature-conditions-implementations.patch --]
[-- Type: text/x-patch, Size: 39169 bytes --]

From 32a2610cc301572bc14a6cb18d6138b653e91e80 Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Tue, 7 Feb 2023 01:57:06 +0800
Subject: [PATCH 5/6] ox: Add struct feature conditions/implementations

* lisp/ox.el (org-export--annotate-info, org-export-detect-features,
org-export-define-derived-backend, org-export-define-backend,
org-export-conditional-features): Refactor backend feature
conditions/implementations into a struct field.  This allows for parent
inheritance to be properly managed, and leads into future work making
features more widely used in the export process.
(org-export-expand-features, org-export-resolve-feature-implementations,
org-export-generate-features-preamble,
org-export-expand-feature-snippets): Rework `org-export-expand-features`
into `org-export-resolve-feature-implementations`, and
`org-export-generate-features-preamble` into
`org-export-expand-feature-snippets`.
(org-export-process-features, org-export-update-features): Introduce
`org-export-process-features' to simplify the application of features to
INFO.

* lisp/ox-latex.el (org-latex-make-preamble): Move the LaTeX feature
conditions/implementations into the backend definition, and use the
reworked function `org-export-expand-feature-snippets'.
---
 lisp/ox-latex.el | 156 +++++++--------
 lisp/ox.el       | 482 ++++++++++++++++++++++++++++++++++-------------
 2 files changed, 415 insertions(+), 223 deletions(-)

diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index 040824f45..6221cc486 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -127,8 +127,6 @@ (org-export-define-backend 'latex
     (:description "DESCRIPTION" nil nil parse)
     (:keywords "KEYWORDS" nil nil parse)
     (:subtitle "SUBTITLE" nil nil parse)
-    (:conditional-features nil nil org-latex-conditional-features)
-    (:feature-implementations nil nil org-latex-feature-implementations)
     ;; Other variables.
     (:latex-active-timestamp-format nil nil org-latex-active-timestamp-format)
     (:latex-caption-above nil nil org-latex-caption-above)
@@ -172,7 +170,64 @@ (org-export-define-backend 'latex
     (:latex-toc-command nil nil org-latex-toc-command)
     (:latex-compiler "LATEX_COMPILER" nil org-latex-compiler)
     ;; Redefine regular options.
-    (:date "DATE" nil "\\today" parse)))
+    (:date "DATE" nil "\\today" parse))
+  :feature-conditions-alist
+  `((t !announce-start)
+    (t !announce-end)
+    (t !guess-pollyglossia)
+    (t !guess-babel)
+    (t !guess-inputenc)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            '(latex-fragment latex-environment) #'identity info t))
+     maths)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            'underline #'identity info t))
+     underline)
+    ("\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith"
+     underline)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            'link
+          (lambda (link)
+            (and (member (org-element-property :type link)
+                         '("http" "https" "ftp" "file"))
+                 (file-name-extension (org-element-property :path link))
+                 (equal (downcase (file-name-extension
+                                   (org-element-property :path link)))
+                        "svg")))
+          info t))
+     svg)
+    (org-latex-tables-booktabs booktabs)
+    (,(lambda (info)
+        (eq (plist-get info :latex-src-block-backend) 'engraved))
+     engraved-code)
+    ("^[ \t]*#\\+attr_latex: .*:float +wrap"
+     float-wrap)
+    ("^[ \t]*#\\+attr_latex: .*:float +sideways"
+     rotate)
+    ("^[ \t]*#\\+caption:\\|\\\\caption{" caption))
+  :feature-implementations-alist
+  `((!announce-start
+     :snippet ,(lambda (info)
+                 (format "\n%%%% ox-latex features: %s"
+                         (plist-get info :features)))
+     :order -100)
+    (maths :snippet "\\usepackage{amsmath}\n\\usepackage{amssymb}" :order 0.2)
+    (underline :snippet "\\usepackage[normalem]{ulem}" :order 0.5)
+    (image :snippet "\\usepackage{graphicx}" :order 2)
+    (svg :snippet "\\usepackage[inkscapelatex=false]{svg}" :order 2 :when image)
+    (longtable :snippet "\\usepackage{longtable}" :when table :order 2)
+    (booktabs :snippet "\\usepackage{booktabs}" :when table :order 2)
+    (float-wrap :snippet "\\usepackage{wrapfig}" :order 2)
+    (rotate :snippet "\\usepackage{rotating}" :order 2)
+    (caption :snippet "\\usepackage{capt-of}")
+    (engraved-code :snippet org-latex-generate-engraved-preamble :when code)
+    (!guess-pollyglossia :snippet org-latex-guess-polyglossia-language)
+    (!guess-babel :snippet org-latex-guess-babel-language)
+    (!guess-inputenc :snippet org-latex-guess-inputenc)
+    (!announce-end :snippet "%% end ox-latex features\n" :order 100)))
 
 
 \f
@@ -1380,94 +1435,6 @@ (defun org-latex-generate-engraved-preamble (info)
        "% WARNING syntax highlighting unavailable as engrave-faces-latex was missing.\n")
      "\n")))
 
-;;;; Generated preamble
-
-(defcustom org-latex-conditional-features
-  `((t . !announce-start)
-    (t . !announce-end)
-    (t . !guess-pollyglossia)
-    (t . !guess-babel)
-    (t . !guess-inputenc)
-    (,(lambda (info)
-        (org-element-map (plist-get info :parse-tree)
-            '(latex-fragment latex-environment) #'identity info t))
-     . maths)
-    (,(lambda (info)
-        (org-element-map (plist-get info :parse-tree)
-            'underline #'identity info t))
-     . underline)
-    ("\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith"
-     . underline)
-    (,(lambda (info)
-        (org-element-map (plist-get info :parse-tree)
-            'link
-          (lambda (link)
-            (and (member (org-element-property :type link)
-                         '("http" "https" "ftp" "file"))
-                 (file-name-extension (org-element-property :path link))
-                 (equal (downcase (file-name-extension
-                                   (org-element-property :path link)))
-                        "svg")))
-          info t))
-     . svg)
-    (org-latex-tables-booktabs . booktabs)
-    (,(lambda (info)
-        (eq (plist-get info :latex-src-block-backend) 'engraved))
-     . engraved-code)
-    ("^[ \t]*#\\+attr_latex: .*:float +wrap"
-     . float-wrap)
-    ("^[ \t]*#\\+attr_latex: .*:float +sideways"
-     . rotate)
-    ("^[ \t]*#\\+caption:\\|\\\\caption{" . caption))
-  "A LaTeX-specific extension to `org-export-conditional-features', which see.")
-
-(defcustom org-latex-feature-implementations
-  `((!announce-start
-     :snippet ,(lambda (info)
-                 (format "\n%%%% ox-latex features: %s"
-                         (plist-get info :features)))
-     :order -100)
-    (maths :snippet "\\usepackage{amsmath}\n\\usepackage{amssymb}" :order 0.2)
-    (underline :snippet "\\usepackage[normalem]{ulem}" :order 0.5)
-    (image :snippet "\\usepackage{graphicx}" :order 2)
-    (svg :snippet "\\usepackage[inkscapelatex=false]{svg}" :order 2 :when image)
-    (longtable :snippet "\\usepackage{longtable}" :when table :order 2)
-    (booktabs :snippet "\\usepackage{booktabs}" :when table :order 2)
-    (float-wrap :snippet "\\usepackage{wrapfig}" :order 2)
-    (rotate :snippet "\\usepackage{rotating}" :order 2)
-    (caption :snippet "\\usepackage{capt-of}")
-    (engraved-code :snippet org-latex-generate-engraved-preamble :when code)
-    (!guess-pollyglossia :snippet org-latex-guess-polyglossia-language)
-    (!guess-babel :snippet org-latex-guess-babel-language)
-    (!guess-inputenc :snippet org-latex-guess-inputenc)
-    (!announce-end :snippet "%% end ox-latex features\n" :order 100))
-  "Alist describing how export features should be supported in the preamble.
-
-Implementation alist has the feature symbol as the car, with the
-cdr forming a plist with the following keys:
-- :snippet, which is either,
-  - A string, which should be included in the preamble verbatim.
-  - A variable, the value of which should be included in the preamble.
-  - A function, which is called with two arguments — the export info,
-    and the list of feature flags. The returned value is included in
-    the preamble.
-- :requires, a feature or list of features which are needed.
-- :when, a feature or list of features which imply this feature.
-- :prevents, a feature or list of features that should be masked.
-- :order, for when inclusion order matters. Feature implementations
-  with a lower order appear first.  The default is 0."
-  :group 'org-export-general
-  :type '(plist :key-type
-          (choice (const :snippet)
-                  (const :requires)
-                  (const :when)
-                  (const :prevents)
-                  (const :order)
-                  (const :trigger))
-          :value-type
-          (choice (string :tag "Verbatim content")
-                  (variable :tag "Content variable")
-                  (function :tag "Generating function"))))
 ;; Citation features
 
 (org-export-update-features 'latex
@@ -2086,7 +2053,12 @@ (defun org-latex-make-preamble (info &optional template snippet?)
                 (org-latex-guess-babel-language info)
                 (org-latex-guess-polyglossia-language info)
                 "\n% Generated preamble omitted for snippets.")
-            (org-export-generate-features-preamble info)))
+            (concat
+             "\n"
+             (string-join
+              (org-export-expand-feature-snippets info)
+              "\n\n")
+             "\n")))
     (concat
      ;; Time-stamp.
      (and (plist-get info :time-stamp-file)
diff --git a/lisp/ox.el b/lisp/ox.el
index 32f1c6016..6c7a11b66 100644
--- a/lisp/ox.el
+++ b/lisp/ox.el
@@ -1030,7 +1030,7 @@ ;;; Defining Back-ends
 
 (cl-defstruct (org-export-backend (:constructor org-export-create-backend)
 				  (:copier nil))
-  name parent transcoders options filters blocks menu)
+  name parent transcoders options filters blocks menu feature-conditions feature-implementations)
 
 ;;;###autoload
 (defun org-export-get-backend (name)
@@ -1136,6 +1136,62 @@ (defun org-export-get-all-filters (backend)
 	(setq filters (append filters (org-export-backend-filters backend))))
       filters)))
 
+(defvar org-export-conditional-features)
+
+(defun org-export-get-all-feature-conditions (backend)
+  "Return full feature condition alist for BACKEND.
+
+BACKEND is an export back-end, as return by, e.g,,
+`org-export-create-backend'.  Return value is an alist where keys
+are feature conditions, and values are feature symbols.
+
+Unlike `org-export-backend-feature-conditions', this function
+also returns conditions inherited from parent back-ends, if any."
+  (when (symbolp backend) (setq backend (org-export-get-backend backend)))
+  (and backend
+       (let ((conditions (org-export-backend-feature-conditions backend))
+             parent)
+         (while (setq parent (org-export-backend-parent backend))
+           (setq backend (org-export-get-backend parent))
+           (dolist (condition (org-export-backend-feature-conditions backend))
+             (push condition conditions)))
+         (dolist (condition org-export-conditional-features)
+           (unless (assq (car condition) conditions)
+             (push condition conditions)))
+         conditions)))
+
+(defun org-export-get-all-feature-implementations (backend)
+  "Return full feature implementation alist for BACKEND.
+
+BACKEND is an export back-end, as return by, e.g,,
+`org-export-create-backend'.  Return value is an alist where keys
+are feature symbols, and values are an implementation
+specification plist.
+
+Unlike `org-export-backend-feature-implementations', this function
+also returns implementations inherited from parent back-ends, if any."
+  (when (symbolp backend) (setq backend (org-export-get-backend backend)))
+  (and backend
+       (let ((implementations (org-export-backend-feature-implementations backend))
+             parent)
+         (while (setq parent (org-export-backend-parent backend))
+           (setq backend (org-export-get-backend parent))
+           (dolist (implementation (org-export-backend-feature-implementations backend))
+             (unless (assq (car implementation) implementations)
+               (push implementation implementations))))
+         implementations)))
+
+(defun org-export-install-features (info)
+  "Install feature conditions and implementations in the communication channel.
+INFO is a plist containing the current communication channel.
+Return the updated communication channel."
+  (plist-put info :feature-conditions
+             (org-export-get-all-feature-conditions
+              (plist-get info :back-end)))
+  (plist-put info :feature-implementations
+             (org-export-get-all-feature-implementations
+              (plist-get info :back-end))))
+
 (defun org-export-define-backend (backend transcoders &rest body)
   "Define a new back-end BACKEND.
 
@@ -1247,20 +1303,24 @@ (defun org-export-define-backend (backend transcoders &rest body)
     `org-export-options-alist' for more information about
     structure of the values."
   (declare (indent 1))
-  (let (filters menu-entry options)
+  (let (filters menu-entry options feature-conditions feature-implementations)
     (while (keywordp (car body))
       (let ((keyword (pop body)))
 	(pcase keyword
 	  (:filters-alist (setq filters (pop body)))
 	  (:menu-entry (setq menu-entry (pop body)))
 	  (:options-alist (setq options (pop body)))
+	  (:feature-conditions-alist (setq feature-conditions (pop body)))
+	  (:feature-implementations-alist (setq feature-implementations (pop body)))
 	  (_ (error "Unknown keyword: %s" keyword)))))
     (org-export-register-backend
      (org-export-create-backend :name backend
 				:transcoders transcoders
 				:options options
 				:filters filters
-				:menu menu-entry))))
+				:menu menu-entry
+                                :feature-conditions feature-conditions
+                                :feature-implementations feature-implementations))))
 
 (defun org-export-define-derived-backend (child parent &rest body)
   "Create a new back-end as a variant of an existing one.
@@ -1307,7 +1367,7 @@ (defun org-export-define-derived-backend (child parent &rest body)
 
   (org-export-to-buffer \\='my-latex \"*Test my-latex*\")"
   (declare (indent 2))
-  (let (filters menu-entry options transcoders)
+  (let (filters menu-entry options transcoders feature-conditions feature-implementations)
     (while (keywordp (car body))
       (let ((keyword (pop body)))
 	(pcase keyword
@@ -1315,6 +1375,8 @@ (defun org-export-define-derived-backend (child parent &rest body)
 	  (:menu-entry (setq menu-entry (pop body)))
 	  (:options-alist (setq options (pop body)))
 	  (:translate-alist (setq transcoders (pop body)))
+	  (:feature-conditions-alist (setq feature-conditions (pop body)))
+	  (:feature-implementations-alist (setq feature-implementations (pop body)))
 	  (_ (error "Unknown keyword: %s" keyword)))))
     (org-export-register-backend
      (org-export-create-backend :name child
@@ -1322,7 +1384,9 @@ (defun org-export-define-derived-backend (child parent &rest body)
 				:transcoders transcoders
 				:options options
 				:filters filters
-				:menu menu-entry))))
+				:menu menu-entry
+                                :feature-conditions feature-conditions
+                                :feature-implementations feature-implementations))))
 
 
 \f
@@ -2031,7 +2095,7 @@ (defun org-export-expand (blob contents &optional with-affiliated)
 		     blob contents))))
 
 \f
-;;; Conditional/Generated Preamble
+;;; Conditional/Generated Features
 ;;
 ;; Many formats have some version of a preamble, whether it be HTML's
 ;; <head>...</head> or the content before LaTeX's \begin{document}.
@@ -2042,12 +2106,35 @@ ;;; Conditional/Generated Preamble
 ;; with filters approach, but neither really solve this problem nicely.
 ;;
 ;; The conditional/generated preamble defines mechanisms of detecting
-;; which "features" are used in a document, handles interactions
-;; between features, and provides/generates preamble content to
+;; which "export features" are used in a document, handles
+;; interactions between features, and provides/generates content to
 ;; support the features.
+;;
+;; Each export feature condition takes the form of a
+;; (CONDITION . FEATURES) cons cell (see `org-export-detect-features'),
+;; and each implementation takes the form of a (FEATURE . (:KEY VALUE ...))
+;; associated plist (see `org-export-resolve-feature-implementations'
+;; and `org-export-expand-feature-snippets').
+;;
+;; This functionality is applied during export as follows:
+;; 1. The export feature conditions and implementations are installed
+;;    into the INFO plist with `org-export-install-features'.
+;;    This simply applies `org-export-get-all-feature-conditions' and
+;;    `org-export-get-all-feature-implementations', which merges the
+;;    backend's conditions/implementations with all of it's parents and
+;;    finally the global condition list
+;;    `org-export-conditional-features'.
+;; 2. The "export features" used in a document are detected with
+;;    `org-export-detect-features'.
+;; 3. The interaction between different feature implementations is
+;;    resolved with `org-export-resolve-feature-implementations',
+;;    producing an ordered list of implementations to be actually used
+;;    in an export.
+;; 4. The feature implementation's snippets are transformed into strings
+;;    to be inserted with `org-export-expand-feature-snippets'.
 
 (defcustom org-export-conditional-features
-  `(("^[ \t]*#\\+print_bibliography:" . bibliography)
+  `(("^[ \t]*#\\+print_bibliography:" bibliography)
     (,(lambda (info)
        (org-element-map (plist-get info :parse-tree)
            'link
@@ -2059,7 +2146,7 @@ (defcustom org-export-conditional-features
                                     (org-element-property :path link)))
                          "svg")))
          info t))
-     . svg)
+     svg)
     (,(lambda (info)
        (org-element-map (plist-get info :parse-tree)
            'link
@@ -2071,69 +2158,95 @@ (defcustom org-export-conditional-features
                                    (org-element-property :path link)))
                         image-file-name-extensions)))
          info t))
-     . image)
+     image)
     (,(lambda (info)
        (org-element-map (plist-get info :parse-tree)
            'table #'identity info t))
-     . table)
+     table)
     (,(lambda (info)
        (org-element-map (plist-get info :parse-tree)
            '(src-block inline-src-block) #'identity info t))
-     . code))
+     code))
   "Org feature tests and associated feature flags.
 
 Alist where the car is a test for the presense of the feature,
 and the CDR is either a single feature symbol or a list of
 feature symbols.
 
-Feature tests can take any of the following forms:
-- Variable symbol, the value of which is fetched.
-- Function symbol, which is called with the export info
-  as the argument.
-- A string, which is used as a regexp search in the buffer.
-  The regexp matching is taken as confirmation of the existence
-  of the feature.
-
-When the test is a variable or function and produces a string
-value, that value is itself used as a test. Any other non-nil
-value will imply the existance of the feature."
+See `org-export-detect-features' for how this is processed."
   :group 'org-export-general
   :type '(alist :key-type
                 (choice (regexp :tag "Feature test regexp")
                         (variable :tag "Feature variable")
                         (function :tag "Feature test function"))
                 :value-type
-                (choice (symbol :tag "Feature symbol")
-                        (repeat symbol :tag "Feature symbols"))))
+                (repeat symbol :tag "Feature symbols")))
 
 (defun org-export-detect-features (info)
-  "Detect features from `org-export-conditional-features' in INFO."
-  (let (case-fold-search)
-    (delete-dups
-     (mapcan
-      (lambda (construct-feature)
-        (and (let ((out (pcase (car construct-feature)
-                          ((pred stringp) (car construct-feature))
-                          ((pred functionp)
-                           (funcall (car construct-feature) info))
-                          ((pred symbolp) (symbol-value (car construct-feature)))
-                          (_ (error "org-export-conditional-features key %s unable to be used" (car construct-feature))))))
-               (if (stringp out)
-                   (save-excursion
-                     (goto-char (point-min))
-                     (re-search-forward out nil t))
-                 out))
-             (if (listp (cdr construct-feature))
-                 (copy-sequence (cdr construct-feature))
-               (list (cdr construct-feature)))))
-      (append org-export-conditional-features
-              (plist-get info :conditional-features))))))
-
-(defun org-export-expand-features (info)
-  "Identify all implied implementations from features, in INFO.
-
-(plist-get info :feature-implementations) should be an alist of feature symbols
-and specification plists with the following keys:
+  "Detect features from `org-export-conditional-features' in INFO.
+
+More specifically, for each (CONDITION . FEATURES) cons cell of
+the :feature-conditions list in INFO, the CONDITION is evaluated
+in two phases.
+
+In phase one, CONDITION is transformed like so:
+- If a variable symbol, the value is fetched
+- If a function symbol, the function is called with INFO as the
+  sole argument
+- If a string, passed on unmodified
+
+In phase two, if the CONDITION result is a string, it is used as
+a case-sensitive regexp search in the buffer.  The regexp
+matching is taken as confirmation of the existance of FEATURES.
+Any other non-nil value indicates the existance of FEATURES.
+
+A list of all detected feature symbols is returned.
+
+This function should be run in the processed export Org buffer,
+after includes have been expanded and commented trees removed."
+  (delete-dups
+   (mapcan
+    (org-export--single-feature-detector info)
+    (plist-get info :feature-conditions))))
+
+(defun org-export--single-feature-detector (info)
+  "Return a feature detection lambda that operates on INFO.
+
+The lambda has the signature ((CONDITION . FEATURES)), and
+return FEATURES if CONDITION is found to apply.
+
+CONDITION is evaluated in the context of INFO and the current buffer,
+in accordance with the docstring of `org-export-detect-features'.
+`copy-sequence' is applied to the FEATURES list so that `nconc' can
+safely be applied to it."
+  (lambda (condition-features)
+    (let ((condition (car condition-features))
+          (features (cdr condition-features))
+          (case-fold-search nil)
+          matcher)
+      (setq matcher
+            (cond
+             ((stringp condition) condition)
+             ((functionp condition) (funcall condition info))
+             ((symbolp condition) (symbol-value condition))
+             (t (error "org-export: Feature condition %s (for %s) unable to be used"
+                       condition features))))
+      (and (if (stringp matcher)
+               (save-excursion
+                 (goto-char (point-min))
+                 (re-search-forward matcher nil t))
+             matcher)
+           (copy-sequence features)))))
+
+(defun org-export-resolve-feature-implementations (info &optional features implementations)
+  "Resolve the IMPLEMENTATIONS of FEATURES, of INFO.
+
+FEATURES should be a list of all feature symbols to be resolved,
+and defaults to (plist-get info :features).  IMPLEMENTATIONS
+should be an alist of feature symbols and specification plists,
+and defaults to (plist-get info :feature-implementations).
+
+The following keys of the each implementation plist are recognised:
 - :snippet, which is either,
   - A string, which should be included in the preamble verbatim.
   - A variable, the value of which should be included in the preamble.
@@ -2148,12 +2261,10 @@ (plist-get info :feature-implementations) should be an alist of feature symbols
   with a lower order appear first.  The default is 0.
 
 This function processes :requires, :when, and :prevents in turn
-before finally sorting according to :order.
-
-After resolving the features, the :features key of INFO is
-updated to reflect the expanded set of features being used."
-  (let ((initial-features (plist-get info :features))
-        (implementations (plist-get info :feature-implementations))
+before finally sorting according to :order.  The final
+implementation list is returned."
+  (let ((initial-features (or features (plist-get info :features)))
+        (implementations (or implementations (plist-get info :feature-implementations)))
         required-features current-implementations)
     ;; Process :requires.
     (while initial-features
@@ -2214,79 +2325,188 @@ (plist-get info :feature-implementations) should be an alist of feature symbols
                (lambda (i) (memq (car i) prevented))
                current-implementations))))
     ;; Sort by :order.
-    (setq current-implementations
-          (sort current-implementations
-                (lambda (impl1 impl2)
-                  (< (or (plist-get (cdr impl1) :order) 0)
-                     (or (plist-get (cdr impl2) :order) 0)))))
-    ;; Update :features to reflect the features actually used.
-    (plist-put info :features (mapcar #'car current-implementations))
-    current-implementations))
-
-(defun org-export-generate-features-preamble (info)
-  "Generate preamble string according to features an implementations in INFO.
-More specifically, this function resolves feature implementations
-with `org-export-expand-features' and concatenates the snippets."
-  (let* ((feat-impl (org-export-expand-features info))
-         (feat-snippets
-          (mapcar
-           (lambda (impl)
-             (let ((snippet (plist-get (cdr impl) :snippet)))
+    (sort current-implementations
+          (lambda (impl1 impl2)
+            (< (or (plist-get (cdr impl1) :order) 0)
+               (or (plist-get (cdr impl2) :order) 0))))))
+
+(defun org-export-expand-feature-snippets (info &optional feature-implementations)
+  "Expand each of the feature :snippet keys in FEATURE-IMPLEMENTATIONS.
+FEATURE-IMPLEMENTATIONS is expected to be a list of implementation
+plists, if not provided explicitly it is extracted from the
+:feature-implementations key of INFO.
+
+Each implementation plist's :snippet value is expanded in order, in
+the following manner:
+- nil values are ignored
+- functions are called with INFO, and must produce a string or nil
+- variable symbols use the value, which must be a string or nil
+- strings are included verbatim
+- all other values throw an `error'."
+  (let (expanded-snippets snippet value)
+    (dolist (impl (or feature-implementations
+                      (plist-get info :feature-implementations)))
+      (setq snippet (plist-get (cdr impl) :snippet)
+            value (cond
+                   ((null snippet) nil)
+                   ((functionp snippet) (funcall snippet info))
+                   ((symbolp snippet) (symbol-value snippet))
+                   ((stringp snippet) snippet)
+                   (t (error "org-export: The %s feature snippet %S is invalid (must be either nil, a function/variable symbol, or a string)"
+                             (car impl) snippet))))
+      (cond
+       ((stringp value)
+        (push value expanded-snippets))
+       (value ; Non-string value, could come from function or variable.
+        (error "org-export: The %s feature snippet %s must give nil or a string, but instead gave %S"
+               (car impl)
                (cond
-                ((null snippet) nil)
-                ((functionp snippet)
-                 (funcall snippet info))
-                ((symbolp snippet) (symbol-value snippet))
-                ((stringp snippet) snippet)
-                (t (error "org-export feature snippet %S is invalid." snippet)
-                   nil))))
-           feat-impl)))
-    (mapconcat
-     #'identity
-     (append (delq nil feat-snippets) (list ""))
-     "\n")))
-
-(defun org-export-update-features (backend &rest feature-property-value-lists)
-  "For BACKEND's export spec, set all FEATURE-PROPERTY-VALUE-LISTS.
-
-Specifically, for each (FEATURE . PROPERTY-VALUE-LIST) entry of
-FEATURE-PROPERTY-VALUE-LISTS, each :PROPERTY VALUE pair of
-PROPERTY-VALUE-PAIRS is set to VALUE within the backend's feature
-implementation plist.  The sole exception to this is the
-:condition property, the value of which is set in the backend's
-feature condition plist instead.
+                ((and (functionp snippet) (symbolp snippet))
+                 (format "function (`%s')" snippet))
+                ((functionp snippet) "anonymous function")
+                (t (format "variable (`%s')" snippet)))
+               value))))
+    (nreverse expanded-snippets)))
+
+(defun org-export-process-features (info)
+  "Install feature conditions/implementations in INFO, and resolve them.
+See `org-export-detect-features' and `org-export-resolve-feature-implementations' for
+more information on what this entails."
+  (org-export-install-features info)
+  (let* ((features (org-export-detect-features info))
+         (resolved-implementations
+          (org-export-resolve-feature-implementations info features)))
+    (plist-put info :feature-implementations resolved-implementations)
+    (plist-put info :features (mapcar #'car resolved-implementations))))
 
-This can be used to both modify existing entries, and create new ones.
-
-\(fn BACKEND &rest (FEATURE PROPERTY-VALUE-PAIRS...)...)"
+;;;###autoload
+(defmacro org-export-update-features (backend &rest feature-property-value-lists)
+  "For BACKEND's export spec, set each FEATURE's :PROPERTY to VALUE.
+
+The behaviour of this macro is best behaved with an example.
+For instance, to add some preamble content from the variable
+\"my-org-beamer-metropolis-tweaks\" when using the metropolis theme
+with beamer export:
+
+  (org-export-update-features \\='beamer
+    (beamer-metropolis
+     :condition (string-match-p \"metropolis$\" (plist-get info :beamer-theme))
+     :snippet my-org-beamer-metropolis-tweaks
+     :order 3))
+
+The modifies the beamer backend, either creating or updating the
+\"beamer-metropolis\" feature.  The :condition property adds a
+condition which detects the feature, and all other properties are
+applied to the feature's implementation plist.  Setting
+:condition to t means the feature will always be enabled, and
+conversely setting :condition to nil means the feature will never
+be enabled.
+
+When setting the :condition and :snippet properties, any sexp is
+is implicitly converted to,
+  (lambda (info) SEXPR)
+
+Each (FEATURE . (:PROPERTY VALUE)) form that is processed is
+taken from the single &rest argument
+FEATURE-PROPERTY-VALUE-LISTS.
+
+\(fn BACKEND &rest (FEATURE . (:PROPERTY VALUE)...)...)"
   (declare (indent 1))
-  (let ((backend-var (intern (format "org-%s-feature-implementations" backend)))
-        (backend-cf (intern (format "org-%s-conditional-features" backend))))
-    (unless (boundp backend-var)
-      (error "Feature implementations for %s cannot be set as %s is undefined"
-             backend backend-var))
-    (dolist (feature-property-value-set feature-property-value-lists)
-      (let ((feature (car feature-property-value-set))
-            (property-value-pairs (copy-sequence (cdr feature-property-value-set))))
-        (while property-value-pairs
-          (if (eq (car property-value-pairs) :condition)
-              (let ((condition (progn (pop property-value-pairs)
-                                      (pop property-value-pairs))))
-                (unless (boundp backend-cf)
-                  (error "Feature condition for %s cannot be set as %s is undefined"
-                         backend backend-cf))
-                (cond
-                 ((rassoc feature (symbol-value backend-cf))
-                  (if condition
-                      (setcar (rassoc feature (symbol-value backend-cf))
-                              condition)
-                    (set backend-cf (delq (rassoc feature (symbol-value backend-cf))
-                                          (symbol-value backend-cf)))))
-                 (condition
-                  (add-to-list backend-cf (cons condition feature)))))
-            (setf (alist-get feature (symbol-value backend-var))
-                  (plist-put (alist-get feature (symbol-value backend-var))
-                             (pop property-value-pairs) (pop property-value-pairs)))))))))
+  (org-with-gensyms (backend-struct the-entry the-features the-condition the-feat-impl cond-feat)
+    (let ((backend-expr
+           (if (and (eq (car-safe backend) 'quote)
+                    (symbolp (cadr backend))
+                    (not (cddr backend)))
+               (or (org-export-get-backend (cadr backend))
+                   `(org-export-get-backend ',(cadr backend)))
+             `(if (symbolp ,backend)
+                  (org-export-get-backend ,backend)
+                backend)))
+          (backend-impls
+           (list 'aref backend-struct
+                 (cl-struct-slot-offset 'org-export-backend 'feature-implementations)))
+          (backend-conds
+           (list 'aref backend-struct
+                 (cl-struct-slot-offset 'org-export-backend 'feature-conditions)))
+          body condition-set-p implementation-set-p)
+      (dolist (feature-property-value-set feature-property-value-lists)
+        (when (eq (car feature-property-value-set) 'quote)
+          (pop feature-property-value-set))
+        (let ((features (car feature-property-value-set))
+              (property-value-pairs (cdr feature-property-value-set))
+              let-body property value)
+          (while property-value-pairs
+            (setq property (pop property-value-pairs)
+                  value (pop property-value-pairs))
+            (cond
+             ((consp value)
+              (unless (memq (car value) '(function quote))
+                (if (and (memq property '(:condition :snippet))
+                         (not (functionp value)))
+                    (setq value `(lambda (info) ,value))
+                  (setq value (list 'quote value)))))
+             ((memq value '(nil t))) ; Leave unmodified.
+             ((symbolp value)
+              (setq value (list 'quote value))))
+            (if (eq property :condition)
+                (progn
+                  (unless condition-set-p
+                    (setq condition-set-p t))
+                  (push
+                   (if value
+                       (let ((the-features
+                              (if (consp features) features (list features))))
+                         `(let* ((,the-condition ,value)
+                                 (,the-entry (assoc ,the-condition ,backend-conds)))
+                            (if ,the-entry
+                                (setcdr ,the-entry
+                                        (append ',the-features (cdr ,the-entry)))
+                              (push (cons ,the-condition ',the-features)
+                                    ,backend-conds))))
+                     (let ((single-feature
+                            (if (consp features)
+                                (intern (string-join (mapcar #'symbol-name features)
+                                                     "-and-"))
+                              features)))
+                       `(dolist (,cond-feat ,backend-conds)
+                          (cond
+                           ((equal (cdr ,cond-feat) (list ,single-feature))
+                            (setf ,backend-conds (delq ,cond-feat ,backend-conds)))
+                           ((memq ,single-feature (cdr ,cond-feat))
+                            (setcdr ,cond-feat
+                                    (delq ,single-feature (cdr ,cond-feat))))))))
+                   body))
+              (unless implementation-set-p
+                (setq implementation-set-p t))
+              (push
+               (if let-body
+                   `(plist-put (cdr ,the-feat-impl) ,property ,value)
+                 `(setcdr ,the-feat-impl
+                   (plist-put (cdr ,the-feat-impl) ,property ,value)))
+               let-body)))
+          (when let-body
+            (let ((the-feature
+                   (if (consp features)
+                       (intern (string-join (mapcar #'symbol-name features)
+                                            "-and-"))
+                     features)))
+              (when (consp features)
+                (push
+                 `(plist-put (cdr ,the-feat-impl) :when ',features)
+                 let-body))
+              (push
+               `(let ((,the-feat-impl
+                       (or (assoc ',the-feature ,backend-impls)
+                           (car (push (list ',the-feature ,property nil)
+                                      ,backend-impls)))))
+                  ,@(nreverse let-body))
+               body)))))
+      `(let ((,backend-struct ,backend-expr))
+         ,@(and (not (org-export-backend-p backend-expr))
+                `((unless (org-export-backend-p ,backend-struct)
+                    (error "`%s' is not a loaded export backend" ,backend))))
+         ,@(nreverse body)
+         nil))))
 
 \f
 ;;; The Filter System
@@ -3443,8 +3663,8 @@ (defun org-export--annotate-info (backend info &optional subtreep visible-only e
     ;; the output of the selected citation export processor.
     (org-cite-process-citations info)
     (org-cite-process-bibliography info)
-    ;; With the complete tree, detect features.
-    (plist-put info :features (org-export-detect-features info))
+    ;; Install all the feature conditions and implementations.
+    (org-export-process-features info)
     info))
 
 ;;;###autoload
-- 
2.39.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-ox-latex-Reformat-feature-announcement-comment.patch --]
[-- Type: text/x-patch, Size: 1561 bytes --]

From 41a5c7bf4a668c3ae296da6cf46ecf654f393465 Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Thu, 9 Feb 2023 01:17:49 +0800
Subject: [PATCH 6/6] ox-latex: Reformat feature announcement comment

* lisp/ox-latex.el: Reformat the feature announcement comment to make it
easier to read.
---
 lisp/ox-latex.el | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index 6221cc486..4e7f30f8b 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -211,8 +211,19 @@ (org-export-define-backend 'latex
   :feature-implementations-alist
   `((!announce-start
      :snippet ,(lambda (info)
-                 (format "\n%%%% ox-latex features: %s"
-                         (plist-get info :features)))
+                 (with-temp-buffer
+                   (setq-local left-margin 2)
+                   (insert (string-join
+                            (mapcar #'symbol-name
+                                    (plist-get info :features))
+                            ", ")
+                           ".")
+                   (fill-region-as-paragraph (point-min) (point-max))
+                   (goto-char (point-min))
+                   (insert "%% ox-latex features:\n% ")
+                   (while (search-forward "\n" nil t)
+                     (insert "%"))
+                   (buffer-string)))
      :order -100)
     (maths :snippet "\\usepackage{amsmath}\n\\usepackage{amssymb}" :order 0.2)
     (underline :snippet "\\usepackage[normalem]{ulem}" :order 0.5)
-- 
2.39.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-10 17:20 Timothy
@ 2023-02-11 11:37 ` Ihor Radchenko
  2023-02-20 17:41 ` Timothy
  1 sibling, 0 replies; 11+ messages in thread
From: Ihor Radchenko @ 2023-02-11 11:37 UTC (permalink / raw)
  To: Timothy; +Cc: emacs-orgmode

Timothy <orgmode@tec.tecosaur.net> writes:

> “export features” allow for the specification of qualities of the org buffer
> being exported that imply certain “features”, and how those features may be
> implemented in a particular export.
>
> This is done by augmenting the backend struct with two new fields:
> `feature-conditions' and `feature-implementations'.
>
> The feature conditions are resolved during the annotation of `info', in the Org
> buffer after `#+include' expansion and the removal of comments.
>
> The feature implementations are expanded by the backend itself, in the case of
> `ox-latex' this currently means during preamble construction.

I am in favour of this.

Some comments on the code are below.

> +  `(("^[ \t]*#\\+print_bibliography:" . bibliography)

This will also match bibliography statements, which are not keywords.
For example, inside quote blocks.

> +  :group 'org-export-general
> +  :type '(alist :key-type
> +                (choice (regexp :tag "Feature test regexp")
> +                        (variable :tag "Feature variable")
> +                        (function :tag "Feature test function"))
> +                :value-type
> +                (choice (symbol :tag "Feature symbol")
> +                        (repeat symbol :tag "Feature symbols"))))

:package-version is missing in this defcustom. It should be added.

> +(defun org-export-detect-features (info)
> +  "Detect features from `org-export-conditional-features' in INFO."
> +  (let (case-fold-search)

This unconditionally sets case-sensitive search. Thus, for example,
#+PRINT_BIBLIOGRAPHY (as opposed to #+print_bibliography) will not be
recognized as 'bibliography feature.

> +    (delete-dups
> +     (mapcan
> +      (lambda (construct-feature)

This lambda could be a private function instead. A function would be
byte-compiled.

>  (defcustom org-latex-default-packages-alist
>    '(("AUTO" "inputenc"  t ("pdflatex"))
>      ("T1"   "fontenc"   t ("pdflatex"))
> -    (""     "graphicx"  t)

You need to update :package-version upon changing the defcustom value.

> +;;;; Generated preamble
> +
> +(defcustom org-latex-conditional-features

defcustom :keywords are missing here. Please, add.

> +    (,(lambda (info)
> +        (org-element-map (plist-get info :parse-tree)
> +            'link
> +          (lambda (link)
> +            (and (member (org-element-property :type link)
> +                         '("http" "https" "ftp" "file"))
> +                 (file-name-extension (org-element-property :path link))
> +                 (equal (downcase (file-name-extension
> +                                   (org-element-property :path link)))
> +                        "svg")))
> +          info t))
> +     . svg)

This is a duplicate of an entry in `org-export-conditional-features'. Is
it intentional?

> +(defcustom org-latex-feature-implementations

:package-version is missing.

> +  "Alist describing how export features should be supported in the preamble.
> +
> +Implementation alist has the feature symbol as the car, with the
> +cdr forming a plist with the following keys:
> +- :snippet, which is either,
> +  - A string, which should be included in the preamble verbatim.
> +  - A variable, the value of which should be included in the preamble.
> +  - A function, which is called with two arguments — the export info,
> +    and the list of feature flags. The returned value is included in
> +    the preamble.
> +- :requires, a feature or list of features which are needed.
> +- :when, a feature or list of features which imply this feature.
> +- :prevents, a feature or list of features that should be masked.
> +- :order, for when inclusion order matters. Feature implementations
> +  with a lower order appear first.  The default is 0."
> +  :group 'org-export-general

What is this group?

> +  :type '(plist :key-type
> +          (choice (const :snippet)
> +                  (const :requires)
> +                  (const :when)
> +                  (const :prevents)
> +                  (const :order)
> +                  (const :trigger))
> +          :value-type
> +          (choice (string :tag "Verbatim content")
> +                  (variable :tag "Content variable")
> +                  (function :tag "Generating function"))))

The docstring and :type are rather generic wrt backend. Can they be
abstracted away?

> -(defun org-latex-guess-inputenc (header)
> +(defun org-latex-guess-inputenc (info)

It is a breaking change. Can we make something to not break existing
third-party code?

> -(defun org-latex-guess-babel-language (header info)
> +(defun org-latex-guess-babel-language (info)

Again, backwards-incompatible.

> -(defun org-latex-guess-polyglossia-language (header info)
> +(defun org-latex-guess-polyglossia-language (info)

backwards-incompatible

> +    (concat
> +     ;; Time-stamp.
> +     (and (plist-get info :time-stamp-file)
> +          (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))

May it also be a feature?

> +     ;; LaTeX compiler.
> +     (org-latex--insert-compiler info)

And this.

> +(defun org-export-update-features (backend &rest feature-property-value-lists)
> +  "For BACKEND's export spec, set all FEATURE-PROPERTY-VALUE-LISTS.

what about

(defun org-export-update-features (backend &rest feature-settings)
  "Update FEATURE-SETTINGS for BACKEND.

> +Specifically, for each (FEATURE . PROPERTY-VALUE-LIST) entry of
> +FEATURE-PROPERTY-VALUE-LISTS, each :PROPERTY VALUE pair of
> +PROPERTY-VALUE-PAIRS is set to VALUE within the backend's feature
> +implementation plist.  The sole exception to this is the
> +:condition property, the value of which is set in the backend's
> +feature condition plist instead.

This patch is not what how it looks at the end, right? One of the
following patches completely amends the docstring.

Could you please restructure the patches in a way that is easier to
review? Without significant changes in implementations.

I am not sure if all the above comments will apply to the final
patchset.

Also, you will need to add ORG-NEWS entry and document the new feature
system in "13.17 Advanced Export Configuration" and "A.4 Adding Export
Back-ends" sections of the manual.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
       [not found] <mailman.13.1676134175.1258.emacs-orgmode@gnu.org>
@ 2023-02-11 17:03 ` No Wayman
  0 siblings, 0 replies; 11+ messages in thread
From: No Wayman @ 2023-02-11 17:03 UTC (permalink / raw)
  To: emacs-orgmode


> often we include content in export templates that is only 
> relevant in
> particular situations.
>
> “export features” allow for the specification of qualities of 
> the org buffer
> being exported that imply certain “features”, and how those 
> features may be
> implemented in a particular export.

> now `\usepackage{svg}' is automatically added when exporting a 
> buffer that
> includes SVG images.
 
> ┌────
> │ (org-export-update-features 'beamer
> │   (beamer-metropolis
> │    :condition (string-match-p \"metropolis$\" (plist-get info 
> :beamer-theme))
> │    :snippet my-org-beamer-metropolis-tweaks
> │    :order 3))
> └────

As discussed on the Doom Emacs Discord, I strongly suggest 
renaming to avoid overloading the term "feature".
To me, your description indicates sinppets are inserted 
contextually.
Org has a similar notion of contexts with 
`org-capture-templates-contexts'.
A similar name would be better as it does not overload the term 
"feature", which already has a different meaning in elisp.
e.g. org-export-backend-contexts


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-10 17:20 Timothy
  2023-02-11 11:37 ` Ihor Radchenko
@ 2023-02-20 17:41 ` Timothy
  2023-02-24 12:51   ` Sébastien Miquel
  1 sibling, 1 reply; 11+ messages in thread
From: Timothy @ 2023-02-20 17:41 UTC (permalink / raw)
  To: emacs-orgmode

[-- Attachment #1: Type: text/plain, Size: 585 bytes --]

Hi All,

Following some feedback I’ve received from a few people (including off-list),
here’s a v2 set of patches.

Notably, I’ve now got a draft manual entry (see the last patch attached), which
should go a long way to better explaining what this is without asking you to
wade through all the code comments :)

All the best,
Timothy

-- 
Timothy (‘tecosaur’/‘TEC’), Org mode contributor.
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/tec>.

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-org-compat-Add-ensure-list-as-org-ensure-list.patch --]
[-- Type: text/x-patch, Size: 1082 bytes --]

From a8ed768515e4cf305ba52566f372e096f8ed449a Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Sun, 19 Feb 2023 12:28:31 +0800
Subject: [PATCH 1/6] org-compat: Add ensure-list as org-ensure-list

* lisp/org-compat.el (org-ensure-list): Add `ensure-list' from Emacs 28,
as `org-ensure-list'.
---
 lisp/org-compat.el | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/lisp/org-compat.el b/lisp/org-compat.el
index fadb51df6..e11de3639 100644
--- a/lisp/org-compat.el
+++ b/lisp/org-compat.el
@@ -193,6 +193,18 @@   (defun org-format-prompt (prompt default &rest format-args)
                     default)))
      ": ")))
 
+(if (fboundp 'ensure-list)
+    (defalias 'org-ensure-list #'ensure-list)
+  (defun org-ensure-list (object)
+    "Return OBJECT as a list.
+If OBJECT is already a list, return OBJECT itself.  If it's
+not a list, return a one-element list containing OBJECT.
+
+Compatability substitute for `ensure-list' in Emacs 28."
+    (if (listp object)
+        object
+      (list object))))
+
 \f
 ;;; Emacs < 27.1 compatibility
 
-- 
2.39.1


[-- Attachment #3: 0002-ox-Introduce-conditional-generated-preamble.patch --]
[-- Type: text/x-patch, Size: 34592 bytes --]

From 3a1d9eeb4e77a15f1466bc2377838b38d97d5b22 Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Mon, 25 Jul 2022 23:37:13 +0800
Subject: [PATCH 2/6] ox: Introduce conditional/generated preamble

* lisp/ox.el (org-export-detect-features, org-export-expand-features,
org-export-generate-features-preamble): New functions for detecting
features and generating content based on them.
(org-export-conditional-features): Customisation for feature detection.
(org-export-as): Add detected to features to info in the slot :features.
(org-export-update-features): Add a convenience function for users to
edit the feature condition/implementation lists.
(org-export--annotate-info, org-export-detect-features,
org-export-define-derived-backend, org-export-define-backend,
org-export-conditional-features): Refactor backend feature
conditions/implementations into a struct field.  This allows for parent
inheritance to be properly managed, and leads into future work making
features more widely used in the export process.
(org-export-expand-features, org-export-resolve-feature-implementations,
org-export-generate-features-preamble,
org-export-expand-feature-snippets): The main functions for working with
export features.
(org-export-process-features, org-export-update-features): Introduce
`org-export-process-features' to simplify the application of features to
INFO.
---
 lisp/ox.el | 609 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 604 insertions(+), 5 deletions(-)

diff --git a/lisp/ox.el b/lisp/ox.el
index 7e4042bb8..7d9c5eb26 100644
--- a/lisp/ox.el
+++ b/lisp/ox.el
@@ -1030,7 +1030,7 @@ ;;; Defining Back-ends
 
 (cl-defstruct (org-export-backend (:constructor org-export-create-backend)
 				  (:copier nil))
-  name parent transcoders options filters blocks menu)
+  name parent transcoders options filters blocks menu feature-conditions feature-implementations)
 
 ;;;###autoload
 (defun org-export-get-backend (name)
@@ -1136,6 +1136,62 @@ (defun org-export-get-all-filters (backend)
 	(setq filters (append filters (org-export-backend-filters backend))))
       filters)))
 
+(defvar org-export-conditional-features)
+
+(defun org-export-get-all-feature-conditions (backend)
+  "Return full feature condition alist for BACKEND.
+
+BACKEND is an export back-end, as return by, e.g,,
+`org-export-create-backend'.  Return value is an alist where keys
+are feature conditions, and values are feature symbols.
+
+Unlike `org-export-backend-feature-conditions', this function
+also returns conditions inherited from parent back-ends, if any."
+  (when (symbolp backend) (setq backend (org-export-get-backend backend)))
+  (and backend
+       (let ((conditions (org-export-backend-feature-conditions backend))
+             parent)
+         (while (setq parent (org-export-backend-parent backend))
+           (setq backend (org-export-get-backend parent))
+           (dolist (condition (org-export-backend-feature-conditions backend))
+             (push condition conditions)))
+         (dolist (condition org-export-conditional-features)
+           (unless (assq (car condition) conditions)
+             (push condition conditions)))
+         conditions)))
+
+(defun org-export-get-all-feature-implementations (backend)
+  "Return full feature implementation alist for BACKEND.
+
+BACKEND is an export back-end, as return by, e.g,,
+`org-export-create-backend'.  Return value is an alist where keys
+are feature symbols, and values are an implementation
+specification plist.
+
+Unlike `org-export-backend-feature-implementations', this function
+also returns implementations inherited from parent back-ends, if any."
+  (when (symbolp backend) (setq backend (org-export-get-backend backend)))
+  (and backend
+       (let ((implementations (org-export-backend-feature-implementations backend))
+             parent)
+         (while (setq parent (org-export-backend-parent backend))
+           (setq backend (org-export-get-backend parent))
+           (dolist (implementation (org-export-backend-feature-implementations backend))
+             (unless (assq (car implementation) implementations)
+               (push implementation implementations))))
+         implementations)))
+
+(defun org-export-install-features (info)
+  "Install feature conditions and implementations in the communication channel.
+INFO is a plist containing the current communication channel.
+Return the updated communication channel."
+  (plist-put info :feature-conditions
+             (org-export-get-all-feature-conditions
+              (plist-get info :back-end)))
+  (plist-put info :feature-implementations
+             (org-export-get-all-feature-implementations
+              (plist-get info :back-end))))
+
 (defun org-export-define-backend (backend transcoders &rest body)
   "Define a new back-end BACKEND.
 
@@ -1247,20 +1303,24 @@ (defun org-export-define-backend (backend transcoders &rest body)
     `org-export-options-alist' for more information about
     structure of the values."
   (declare (indent 1))
-  (let (filters menu-entry options)
+  (let (filters menu-entry options feature-conditions feature-implementations)
     (while (keywordp (car body))
       (let ((keyword (pop body)))
 	(pcase keyword
 	  (:filters-alist (setq filters (pop body)))
 	  (:menu-entry (setq menu-entry (pop body)))
 	  (:options-alist (setq options (pop body)))
+	  (:feature-conditions-alist (setq feature-conditions (pop body)))
+	  (:feature-implementations-alist (setq feature-implementations (pop body)))
 	  (_ (error "Unknown keyword: %s" keyword)))))
     (org-export-register-backend
      (org-export-create-backend :name backend
 				:transcoders transcoders
 				:options options
 				:filters filters
-				:menu menu-entry))))
+				:menu menu-entry
+                                :feature-conditions feature-conditions
+                                :feature-implementations feature-implementations))))
 
 (defun org-export-define-derived-backend (child parent &rest body)
   "Create a new back-end as a variant of an existing one.
@@ -1307,7 +1367,7 @@ (defun org-export-define-derived-backend (child parent &rest body)
 
   (org-export-to-buffer \\='my-latex \"*Test my-latex*\")"
   (declare (indent 2))
-  (let (filters menu-entry options transcoders)
+  (let (filters menu-entry options transcoders feature-conditions feature-implementations)
     (while (keywordp (car body))
       (let ((keyword (pop body)))
 	(pcase keyword
@@ -1315,6 +1375,8 @@ (defun org-export-define-derived-backend (child parent &rest body)
 	  (:menu-entry (setq menu-entry (pop body)))
 	  (:options-alist (setq options (pop body)))
 	  (:translate-alist (setq transcoders (pop body)))
+	  (:feature-conditions-alist (setq feature-conditions (pop body)))
+	  (:feature-implementations-alist (setq feature-implementations (pop body)))
 	  (_ (error "Unknown keyword: %s" keyword)))))
     (org-export-register-backend
      (org-export-create-backend :name child
@@ -1322,7 +1384,9 @@ (defun org-export-define-derived-backend (child parent &rest body)
 				:transcoders transcoders
 				:options options
 				:filters filters
-				:menu menu-entry))))
+				:menu menu-entry
+                                :feature-conditions feature-conditions
+                                :feature-implementations feature-implementations))))
 
 
 \f
@@ -2030,6 +2094,539 @@ (defun org-export-expand (blob contents &optional with-affiliated)
 	    (funcall (intern (format "org-element-%s-interpreter" type))
 		     blob contents))))
 
+\f
+;;; Conditional/Generated Features
+;;
+;; Many formats have some version of a preamble, whether it be HTML's
+;; <head>...</head> or the content before LaTeX's \begin{document}.
+;; Depending on the particular features in the Org document being
+;; exported, different setup snippets will be needed.  There's the
+;; "everything and the kitchen sink" approach of adding absolutely
+;; everything that might be needed, and the post-translation editing
+;; with filters approach, but neither really solve this problem nicely.
+;;
+;; The conditional/generated preamble defines mechanisms of detecting
+;; which "export features" are used in a document, handles
+;; interactions between features, and provides/generates content to
+;; support the features.
+;;
+;; Each export feature condition takes the form of a
+;; (CONDITION . FEATURES) cons cell (see `org-export-detect-features'),
+;; and each implementation takes the form of a (FEATURE . (:KEY VALUE ...))
+;; associated plist (see `org-export-resolve-feature-implementations'
+;; and `org-export-expand-feature-snippets').
+;;
+;; This functionality is applied during export as follows:
+;; 1. The export feature conditions and implementations are installed
+;;    into the INFO plist with `org-export-install-features'.
+;;    This simply applies `org-export-get-all-feature-conditions' and
+;;    `org-export-get-all-feature-implementations', which merges the
+;;    backend's conditions/implementations with all of it's parents and
+;;    finally the global condition list
+;;    `org-export-conditional-features'.
+;; 2. The "export features" used in a document are detected with
+;;    `org-export-detect-features'.
+;; 3. The interaction between different feature implementations is
+;;    resolved with `org-export-resolve-feature-implementations',
+;;    producing an ordered list of implementations to be actually used
+;;    in an export.
+;; 4. The feature implementation's snippets are transformed into strings
+;;    to be inserted with `org-export-expand-feature-snippets'.
+
+(defcustom org-export-conditional-features
+  `(("^[ \t]*#\\+print_bibliography:" bibliography)
+    (,(lambda (info)
+       (org-element-map (plist-get info :parse-tree)
+           'link
+         (lambda (link)
+           (and (member (org-element-property :type link)
+                        '("http" "https" "ftp" "file"))
+                (file-name-extension (org-element-property :path link))
+                (member (downcase (file-name-extension
+                                   (org-element-property :path link)))
+                        image-file-name-extensions)))
+         info t))
+     image)
+    (,(lambda (info)
+       (org-element-map (plist-get info :parse-tree)
+           'table #'identity info t))
+     table)
+    (,(lambda (info)
+       (org-element-map (plist-get info :parse-tree)
+           '(src-block inline-src-block) #'identity info t))
+     code))
+  "Org feature tests and associated feature flags.
+
+Alist where the car is a test for the presense of the feature,
+and the CDR is either a single feature symbol or a list of
+feature symbols.
+
+See `org-export-detect-features' for how this is processed."
+  :group 'org-export-general
+  :type '(alist :key-type
+                (choice (regexp :tag "Feature test regexp")
+                        (variable :tag "Feature variable")
+                        (function :tag "Feature test function"))
+                :value-type
+                (repeat symbol :tag "Feature symbols")))
+
+(defun org-export-detect-features (info)
+  "Detect features from `org-export-conditional-features' in INFO.
+
+More specifically, for each (CONDITION . FEATURES) cons cell of
+the :feature-conditions list in INFO, the CONDITION is evaluated
+in two phases.
+
+In phase one, CONDITION is transformed like so:
+- If a variable symbol, the value is fetched
+- If a function symbol, the function is called with INFO as the
+  sole argument
+- If a string, passed on unmodified
+
+In phase two, if the CONDITION result is a string, it is used as
+a case-sensitive regexp search in the buffer.  The regexp
+matching is taken as confirmation of the existance of FEATURES.
+Any other non-nil value indicates the existance of FEATURES.
+
+A list of all detected feature symbols is returned.
+
+This function should be run in the processed export Org buffer,
+after includes have been expanded and commented trees removed."
+  (delete-dups
+   (cl-loop
+    for (condition . features) in (plist-get info :feature-conditions)
+    for matcher =
+    (cond
+     ((stringp condition) condition)
+     ((functionp condition) (funcall condition info))
+     ((symbolp condition) (symbol-value condition))
+     (t (error "org-export: Feature condition %s (for %s) unable to be used"
+               condition features)))
+    for active-features =
+    (and (if (stringp matcher)
+             (save-excursion
+               (goto-char (point-min))
+               (re-search-forward matcher nil t))
+           matcher)
+         (copy-sequence features))
+    when active-features
+    nconc active-features)))
+
+(define-error 'org-missing-feature-dependency
+  "A feature was asked for, but is not availible")
+
+(define-error 'org-circular-feature-dependency
+  "There was a circular dependency between some features")
+
+(defun org-export-resolve-feature-implementations (info &optional features implementations)
+  "Resolve the IMPLEMENTATIONS of FEATURES, of INFO.
+
+FEATURES should be a list of all feature symbols to be resolved,
+and defaults to (plist-get info :features).  IMPLEMENTATIONS
+should be an alist of feature symbols and specification plists,
+and defaults to (plist-get info :feature-implementations).
+
+The following keys of the each implementation plist are recognised:
+- :snippet, which is either,
+  - A string, which should be included in the preamble verbatim.
+  - A variable, the value of which should be included in the preamble.
+  - A function, which is called with two arguments — the export info,
+    and the list of feature flags.  The returned value is included in
+    the preamble.
+- :requires, a feature or list of features this feature will enable.
+- :when, a feature or list of features which are required for this
+  feature to be active.
+- :prevents, a feature or list of features that should be masked.
+- :order, for when inclusion order matters.  Feature implementations
+  with a lower order appear first.  The default is 0.
+- :after, a feature or list of features that must be preceding.
+- :before, a feature or list of features that must be succeeding.
+
+This function processes :requires, :when, and :prevents in turn,
+sorting according by :order both before processing :requires and
+after processing :prevents.  The final implementation list is
+returned."
+  (let* ((explicit-features (or features (plist-get info :features)))
+         (implementations (or implementations
+                              (plist-get info :feature-implementations)))
+         (current-implementations
+          (sort (cl-loop for feat in explicit-features
+                         collect (assq feat implementations))
+                (lambda (a b)
+                  (< (or (plist-get (cdr a) :order) 0)
+                     (or (plist-get (cdr b) :order) 0)))))
+         ;; require-records serves to record /why/ a particular implementation
+         ;; is used.  It takes the form of an alist with feature symbols as the
+         ;; keys, and a list of features that ask for that feature as values.
+         ;; A t value is used to indicate the feature has been explicitly
+         ;; required.
+         (require-records
+          (cl-loop for feat in explicit-features
+                   collect (list feat t))))
+    ;; * Process ~:requires~
+    ;; Here we temporarily treat current-implementations as a queue of
+    ;; unproceesed implementations, and for each implemention move
+    ;; it to processed-implementations if not already present.
+    ;; :requires are processed by being added to the current-implementations
+    ;; stack as they are seen.  Along the way require-records is built for
+    ;; the sake of the subsequent :prevents processing.
+    (let ((impl-queue-last (last current-implementations))
+          processed-implementations impl)
+      (while current-implementations
+        (setq impl (pop current-implementations))
+        (unless (memq impl processed-implementations)
+          (push impl processed-implementations)
+          (dolist (req (org-ensure-list
+                        (plist-get (cdar processed-implementations) :requires)))
+            (unless (assq req processed-implementations)
+              (let ((required-impl (assq req implementations)))
+                (unless required-impl
+                  (signal 'org-missing-feature-dependency
+                          (format "The feature `%s' was asked for but could not be found"
+                                  req)))
+                (setq impl-queue-last
+                      (if current-implementations
+                          (setcdr impl-queue-last (list required-impl))
+                        (setq current-implementations (list required-impl))))
+                (push (car impl) (alist-get req require-records)))))))
+      (setq current-implementations
+            (nreverse (delq nil processed-implementations))))
+    ;; * Process ~:when~
+    ;; More specifically, remove features with unfulfilled :when conditions.
+    ;; To correctly resolve all the various :when conditions,
+    ;; do not make any assumptions about which features are active.
+    ;; Initially only consider non-:when implementations to be
+    ;; active, then run through the list of unconfirmed :when
+    ;; implementations and check their conditions against the list
+    ;; of confirmed features.  Continue doing this until no more
+    ;; features are confirmed.
+    (let ((processing t)
+          (confirmed-features
+           (cl-remove-if ; Count unimplemented features as present.
+            (lambda (feat) (assq feat current-implementations))
+            explicit-features))
+          conditional-implementations when)
+      ;; Sort all features by the presense of :when.
+      (dolist (impl current-implementations)
+        (if (plist-get (cdr impl) :when)
+            (push impl conditional-implementations)
+          (push (car impl) confirmed-features)))
+      (while processing
+        (setq processing nil)
+        ;; Check for implementations which have satisfied :when
+        ;; contions.
+        (dolist (impl conditional-implementations)
+          (setq when (plist-get (cdr impl) :when))
+          (when (cond
+                 ((symbolp when)
+                  (memq when confirmed-features))
+                 ((consp when)
+                  (not (cl-set-difference when confirmed-features))))
+            (push (car impl) confirmed-features)
+            (setq conditional-implementations
+                  (delq impl conditional-implementations)
+                  processing t))))
+      ;; Now all that remains is implementations with unsatisfiable
+      ;; :when conditions.
+      (dolist (impl conditional-implementations)
+        (setq current-implementations
+              (delq impl current-implementations))))
+    ;; * Process ~:prevents~
+    ;; Go through every implementation and for prevented features
+    ;; 1. Remove them from current-implementations
+    ;; 2. Go through require-records and remove them from the cdrs.
+    ;; By modifying require-records in this way, features that are
+    ;; only present due to a now-prevented feature will have a
+    ;; nil cdr.  We can then (recursively) check for these features
+    ;; with `rassq' and remove them.
+    ;; Since we used a queue rather than a stack when processing
+    ;; :requires, we know that second order requires (i.e. :requires
+    ;; of :requires) will come after after first order requires.
+    ;; This means that should a n-th order require be prevented by
+    ;; (n-1)-th order require, it will be removed before being
+    ;; processed, and hence handled correctly.
+    (let (feats-to-remove removed null-require)
+      (dolist (impl current-implementations)
+        (setq feats-to-remove (org-ensure-list (plist-get (cdr impl) :prevents)))
+        (while feats-to-remove
+          ;; Remove each of feats-to-remove.
+          (dolist (feat feats-to-remove)
+            (unless (memq feat removed)
+              (push feat removed)
+              (setq current-implementations
+                    (delq (assq feat current-implementations)
+                          current-implementations))
+              (when (assq feat require-records)
+                (setq require-records
+                      (delq (assq feat require-records) require-records)))))
+          (dolist (rec require-records)
+            (setcdr rec (cl-set-difference (cdr rec) feats-to-remove)))
+          ;; The features have now been removed.
+          (setq feats-to-remove nil)
+          ;; Look for orphan requires.
+          (when (setq null-require (rassq nil require-records))
+            (push (car null-require) feats-to-remove)))))
+    ;; Re-sort by ~:order~, to position reqirued features correctly.
+    (setq current-implementations
+          (sort current-implementations
+                (lambda (a b)
+                  (< (or (plist-get (cdr a) :order) 0)
+                     (or (plist-get (cdr b) :order) 0)))))
+    ;; * Processing ~:before~ and ~:after~
+    ;; To resolve dependency order, we will now perform a stable topological
+    ;; sort on any DAGs that exist within current-implementations.
+    (org-export--feature-implementation-toposort
+     current-implementations)))
+
+(defun org-export--feature-implementation-toposort (implementations)
+  "Perform a stable topological sort of IMPLEMENTATIONS.
+The sort is performed based on the :before and :after properties.
+
+See <https://en.wikipedia.org/wiki/Topological_sorting> for more information
+on what this entails."
+  (let ((feature-indicies
+         (cl-loop
+          for elt in implementations
+          and index from 0
+          collect (cons (car elt) index)))
+        resolved-implementations
+        adj-list node-stack)
+    ;; Build an adjacency list from :before and :after.
+    (dolist (impl implementations)
+      (push (list (car impl)) adj-list))
+    (dolist (impl implementations)
+      (let ((before (org-ensure-list (plist-get (cdr impl) :before)))
+            (after (org-ensure-list (plist-get (cdr impl) :after))))
+        (dolist (child before)
+          (push (car impl) (cdr (assq child adj-list))))
+        (when after
+          (setcdr (assq (car impl) adj-list)
+                  (nconc (cdr (assq (car impl) adj-list))
+                         after)))))
+    ;; Initialise the node stack with the first implementation.
+    (setq node-stack (list (car implementations))
+          ;; Make the order of adj-list match implementations.
+          adj-list
+          (mapcar
+           (lambda (entry)
+             (cons (car entry)
+                   ;; Sort edges according to accord with feature
+                   ;; order, to make the DFS stable.
+                   (sort (cdr entry)
+                         (lambda (a b)
+                           (< (or (alist-get a feature-indicies)
+                                  most-positive-fixnum)
+                              (or (alist-get b feature-indicies)
+                                  most-positive-fixnum))))))
+           (nreverse adj-list)))
+    (while adj-list
+      (let ((deps (alist-get (caar node-stack) adj-list))
+            new-dep-found)
+        ;; Look for any unresolved dependencies.
+        (while (and deps (not new-dep-found))
+          (if (not (assq (car deps) adj-list))
+              (setq deps (cdr deps))
+            ;; Check the unresolved dependency is not part of a cycle.
+            (when (assq (car deps) node-stack)
+              (signal 'org-circular-feature-dependency
+                      (format "Found a cycle in the feature dependency graph: %S"
+                              (cons (car deps)
+                                    (nreverse (memq (car deps)
+                                                    (nreverse
+                                                     (mapcar #'car node-stack))))))))
+            ;; Push the unresolved dependency to the top of the stack.
+            (push (assq (car deps) implementations)
+                  node-stack)
+            (setq new-dep-found t)))
+        (unless new-dep-found
+          ;; The top item of the stack has no unresolved dependencies.
+          ;; Move it to the resolved list, and remove its entry from
+          ;; adj-list to both mark it as such and ensure that
+          ;; node-stack will not be incremented to it when/if the
+          ;; stack is emptied.
+          (push (car node-stack) resolved-implementations)
+          (setq adj-list
+                (delq (assq (caar node-stack) adj-list) adj-list)
+                node-stack
+                (or (cdr node-stack)
+                    (list (assq (caar adj-list) implementations)))))))
+    (nreverse resolved-implementations)))
+
+(defun org-export-expand-feature-snippets (info &optional feature-implementations)
+  "Expand each of the feature :snippet keys in FEATURE-IMPLEMENTATIONS.
+FEATURE-IMPLEMENTATIONS is expected to be a list of implementation
+plists, if not provided explicitly it is extracted from the
+:feature-implementations key of INFO.
+
+Each implementation plist's :snippet value is expanded in order, in
+the following manner:
+- nil values are ignored
+- functions are called with INFO, and must produce a string or nil
+- variable symbols use the value, which must be a string or nil
+- strings are included verbatim
+- all other values throw an `error'."
+  (let (expanded-snippets snippet value)
+    (dolist (impl (or feature-implementations
+                      (plist-get info :feature-implementations)))
+      (setq snippet (plist-get (cdr impl) :snippet)
+            value (cond
+                   ((null snippet) nil)
+                   ((functionp snippet) (funcall snippet info))
+                   ((symbolp snippet) (symbol-value snippet))
+                   ((stringp snippet) snippet)
+                   (t (error "org-export: The %s feature snippet %S is invalid (must be either nil, a function/variable symbol, or a string)"
+                             (car impl) snippet))))
+      (cond
+       ((stringp value)
+        (push value expanded-snippets))
+       (value ; Non-string value, could come from function or variable.
+        (error "org-export: The %s feature snippet %s must give nil or a string, but instead gave %S"
+               (car impl)
+               (cond
+                ((and (functionp snippet) (symbolp snippet))
+                 (format "function (`%s')" snippet))
+                ((functionp snippet) "anonymous function")
+                (t (format "variable (`%s')" snippet)))
+               value))))
+    (nreverse expanded-snippets)))
+
+(defun org-export-process-features (info)
+  "Install feature conditions/implementations in INFO, and resolve them.
+See `org-export-detect-features' and `org-export-resolve-feature-implementations' for
+more information on what this entails."
+  (org-export-install-features info)
+  (let* ((exp-features (org-export-detect-features info))
+         (resolved-implementations
+          (org-export-resolve-feature-implementations info exp-features)))
+    (plist-put info :feature-implementations resolved-implementations)
+    (plist-put info :features (mapcar #'car resolved-implementations))))
+
+;;;###autoload
+(defmacro org-export-update-features (backend &rest feature-property-value-lists)
+  "For BACKEND's export spec, set each FEATURE's :PROPERTY to VALUE.
+
+The behaviour of this macro is best behaved with an example.
+For instance, to add some preamble content from the variable
+\"my-org-beamer-metropolis-tweaks\" when using the metropolis theme
+with beamer export:
+
+  (org-export-update-features \\='beamer
+    (beamer-metropolis
+     :condition (string-match-p \"metropolis$\" (plist-get info :beamer-theme))
+     :snippet my-org-beamer-metropolis-tweaks
+     :order 3))
+
+The modifies the beamer backend, either creating or updating the
+\"beamer-metropolis\" feature.  The :condition property adds a
+condition which detects the feature, and all other properties are
+applied to the feature's implementation plist.  Setting
+:condition to t means the feature will always be enabled, and
+conversely setting :condition to nil means the feature will never
+be enabled.
+
+When setting the :condition and :snippet properties, any sexp is
+is implicitly converted to,
+  (lambda (info) SEXPR)
+
+Each (FEATURE . (:PROPERTY VALUE)) form that is processed is
+taken from the single &rest argument
+FEATURE-PROPERTY-VALUE-LISTS.
+
+\(fn BACKEND &rest (FEATURE . (:PROPERTY VALUE)...)...)"
+  (declare (indent 1))
+  (org-with-gensyms (backend-struct the-entry the-features the-condition the-feat-impl cond-feat)
+    (let ((backend-expr
+           (if (and (eq (car-safe backend) 'quote)
+                    (symbolp (cadr backend))
+                    (not (cddr backend)))
+               `(org-export-get-backend ',(cadr backend))
+             `(if (symbolp ,backend)
+                  (org-export-get-backend ,backend)
+                backend)))
+          (backend-impls
+           (list 'aref backend-struct
+                 (cl-struct-slot-offset 'org-export-backend 'feature-implementations)))
+          (backend-conds
+           (list 'aref backend-struct
+                 (cl-struct-slot-offset 'org-export-backend 'feature-conditions)))
+          body condition-set-p implementation-set-p)
+      (dolist (feature-property-value-set feature-property-value-lists)
+        (when (eq (car feature-property-value-set) 'quote)
+          (pop feature-property-value-set))
+        (let ((features (car feature-property-value-set))
+              (property-value-pairs (cdr feature-property-value-set))
+              let-body property value)
+          (while property-value-pairs
+            (setq property (pop property-value-pairs)
+                  value (pop property-value-pairs))
+            (cond
+             ((consp value)
+              (unless (memq (car value) '(function quote))
+                (if (and (memq property '(:condition :snippet))
+                         (not (functionp value)))
+                    (setq value `(lambda (info) ,value))
+                  (setq value (list 'quote value)))))
+             ((memq value '(nil t))) ; Leave unmodified.
+             ((symbolp value)
+              (setq value (list 'quote value))))
+            (if (eq property :condition)
+                (progn
+                  (unless condition-set-p
+                    (setq condition-set-p t))
+                  (push
+                   (if value
+                       (let ((the-features (org-ensure-list features)))
+                         `(let* ((,the-condition ,value)
+                                 (,the-entry (assoc ,the-condition ,backend-conds)))
+                            (if ,the-entry
+                                (setcdr ,the-entry
+                                        (append ',the-features (cdr ,the-entry)))
+                              (push (cons ,the-condition ',the-features)
+                                    ,backend-conds))))
+                     (let ((single-feature
+                            (if (consp features)
+                                (intern (string-join (mapcar #'symbol-name features)
+                                                     "-and-"))
+                              features)))
+                       `(dolist (,cond-feat ,backend-conds)
+                          (cond
+                           ((equal (cdr ,cond-feat) (list ,single-feature))
+                            (setf ,backend-conds (delq ,cond-feat ,backend-conds)))
+                           ((memq ,single-feature (cdr ,cond-feat))
+                            (setcdr ,cond-feat
+                                    (delq ,single-feature (cdr ,cond-feat))))))))
+                   body))
+              (unless implementation-set-p
+                (setq implementation-set-p t))
+              (push
+               (if let-body
+                   `(plist-put (cdr ,the-feat-impl) ,property ,value)
+                 `(setcdr ,the-feat-impl
+                   (plist-put (cdr ,the-feat-impl) ,property ,value)))
+               let-body)))
+          (when let-body
+            (let ((the-feature
+                   (if (consp features)
+                       (intern (string-join (mapcar #'symbol-name features)
+                                            "-and-"))
+                     features)))
+              (when (consp features)
+                (push
+                 `(plist-put (cdr ,the-feat-impl) :when ',features)
+                 let-body))
+              (push
+               `(let ((,the-feat-impl
+                       (or (assoc ',the-feature ,backend-impls)
+                           (car (push (list ',the-feature ,property nil)
+                                      ,backend-impls)))))
+                  ,@(nreverse let-body))
+               body)))))
+      `(let ((,backend-struct ,backend-expr))
+         ,@(and (not (org-export-backend-p backend-expr))
+                `((unless (org-export-backend-p ,backend-struct)
+                    (error "`%s' is not a loaded export backend" ,backend))))
+         ,@(nreverse body)
+         nil))))
 
 \f
 ;;; The Filter System
@@ -3186,6 +3783,8 @@ (defun org-export--annotate-info (backend info &optional subtreep visible-only e
     ;; the output of the selected citation export processor.
     (org-cite-process-citations info)
     (org-cite-process-bibliography info)
+    ;; Install all the feature conditions and implementations.
+    (org-export-process-features info)
     info))
 
 ;;;###autoload
-- 
2.39.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-ox-latex-Apply-new-generated-preamble-to-export.patch --]
[-- Type: text/x-patch, Size: 22007 bytes --]

From 364d9f02a6d6b5e44e93a054a3da8c1328820061 Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Mon, 6 Feb 2023 00:01:26 +0800
Subject: [PATCH 3/6] ox-latex: Apply new generated preamble to export

* lisp/ox-latex.el (org-latex-template, org-latex-make-preamble,
org-latex-guess-polyglossia-language, org-latex-guess-babel-language,
org-latex-guess-inputenc, org-latex-generate-engraved-preamble):
Refactor to make use of the generated export, and add a few new bells
and whistles.

* lisp/ox-beamer.el (org-beamer-template): Adjust to account for the
changes in ox-latex.el.
---
 lisp/org.el       |  15 --
 lisp/ox-beamer.el |  10 +-
 lisp/ox-latex.el  | 345 ++++++++++++++++++++++++++++------------------
 3 files changed, 211 insertions(+), 159 deletions(-)

diff --git a/lisp/org.el b/lisp/org.el
index cc2c09e3a..559b94d97 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -3385,14 +3385,6 @@ (defun org-get-packages-alist (var)
 (defcustom org-latex-default-packages-alist
   '(("AUTO" "inputenc"  t ("pdflatex"))
     ("T1"   "fontenc"   t ("pdflatex"))
-    (""     "graphicx"  t)
-    (""     "longtable" nil)
-    (""     "wrapfig"   nil)
-    (""     "rotating"  nil)
-    ("normalem" "ulem"  t)
-    (""     "amsmath"   t)
-    (""     "amssymb"   t)
-    (""     "capt-of"   nil)
     (""     "hyperref"  nil))
   "Alist of default packages to be inserted in the header.
 
@@ -3403,15 +3395,8 @@ (defcustom org-latex-default-packages-alist
 Org mode to function properly:
 
 - inputenc, fontenc:  for basic font and character selection
-- graphicx: for including images
-- longtable: For multipage tables
 - wrapfig: for figure placement
 - rotating: for sideways figures and tables
-- ulem: for underline and strike-through
-- amsmath: for subscript and superscript and math environments
-- amssymb: for various symbols used for interpreting the entities
-  in `org-entities'.  You can skip some of this package if you don't
-  use any of the symbols.
 - capt-of: for captions outside of floats
 - hyperref: for cross references
 
diff --git a/lisp/ox-beamer.el b/lisp/ox-beamer.el
index 5df78d5a4..8924b412b 100644
--- a/lisp/ox-beamer.el
+++ b/lisp/ox-beamer.el
@@ -821,9 +821,7 @@ (defun org-beamer-template (contents info)
      ;; Time-stamp.
      (and (plist-get info :time-stamp-file)
 	  (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))
-     ;; LaTeX compiler
-     (org-latex--insert-compiler info)
-     ;; Document class and packages.
+     ;; Document class, packages, and some configuration.
      (org-latex-make-preamble info)
      ;; Insert themes.
      (let ((format-theme
@@ -872,12 +870,6 @@ (defun org-beamer-template (contents info)
      (let ((template (plist-get info :latex-hyperref-template)))
        (and (stringp template)
 	    (format-spec template (org-latex--format-spec info))))
-     ;; engrave-faces-latex preamble
-     (when (and (eq org-latex-src-block-backend 'engraved)
-                (org-element-map (plist-get info :parse-tree)
-                    '(src-block inline-src-block) #'identity
-                    info t))
-       (org-latex-generate-engraved-preamble info))
      ;; Document start.
      "\\begin{document}\n\n"
      ;; Title command.
diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index dc2062df5..a2bc72fc0 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -170,7 +170,78 @@ (org-export-define-backend 'latex
     (:latex-toc-command nil nil org-latex-toc-command)
     (:latex-compiler "LATEX_COMPILER" nil org-latex-compiler)
     ;; Redefine regular options.
-    (:date "DATE" nil "\\today" parse)))
+    (:date "DATE" nil "\\today" parse))
+  :feature-conditions-alist
+  `((t !announce-start !announce-end
+     !guess-pollyglossia !guess-babel !guess-inputenc)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            '(latex-fragment latex-environment) #'identity info t))
+     maths)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            'underline #'identity info t))
+     underline)
+    ("\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith"
+     underline)
+    (,(lambda (info)
+        (org-element-map (plist-get info :parse-tree)
+            'link
+          (lambda (link)
+            (and (member (org-element-property :type link)
+                         '("http" "https" "ftp" "file"))
+                 (file-name-extension (org-element-property :path link))
+                 (equal (downcase (file-name-extension
+                                   (org-element-property :path link)))
+                        "svg")))
+          info t))
+     svg)
+    (org-latex-tables-booktabs booktabs)
+    (,(lambda (info)
+        (equal (plist-get info :latex-default-table-environment)
+               "longtable"))
+     longtable)
+    ("^[ \t]*\\+attr_latex: .*:environment +longtable"
+     longtable)
+    (,(lambda (info)
+        (eq (plist-get info :latex-src-block-backend) 'engraved))
+     engraved-code)
+    ("^[ \t]*#\\+attr_latex: .*:float +wrap"
+     float-wrap)
+    ("^[ \t]*#\\+attr_latex: .*:float +sideways"
+     rotate)
+    ("^[ \t]*#\\+caption\\(?:\\[.*\\]\\)?:\\|\\\\caption{" caption))
+  :feature-implementations-alist
+  `((!announce-start
+     :snippet ,(lambda (info)
+                 (with-temp-buffer
+                   (setq-local left-margin 2)
+                   (insert (string-join
+                            (mapcar #'symbol-name
+                                    (plist-get info :features))
+                            ", ")
+                           ".")
+                   (fill-region-as-paragraph (point-min) (point-max))
+                   (goto-char (point-min))
+                   (insert "%% ox-latex features:\n% ")
+                   (while (search-forward "\n" nil t)
+                     (insert "%"))
+                   (buffer-string)))
+     :order -100)
+    (maths :snippet "\\usepackage{amsmath}\n\\usepackage{amssymb}" :order 0.2)
+    (underline :snippet "\\usepackage[normalem]{ulem}" :order 0.5)
+    (image :snippet "\\usepackage{graphicx}" :order 2)
+    (svg :snippet "\\usepackage[inkscapelatex=false]{svg}" :order 2 :when image)
+    (longtable :snippet "\\usepackage{longtable}" :when table :order 2)
+    (booktabs :snippet "\\usepackage{booktabs}" :when table :order 2)
+    (float-wrap :snippet "\\usepackage{wrapfig}" :order 2)
+    (rotate :snippet "\\usepackage{rotating}" :order 2)
+    (caption :snippet "\\usepackage{capt-of}")
+    (engraved-code :when code :snippet org-latex-generate-engraved-preamble)
+    (!guess-pollyglossia :snippet org-latex-guess-polyglossia-language)
+    (!guess-babel :snippet org-latex-guess-babel-language)
+    (!guess-inputenc :snippet org-latex-guess-inputenc)
+    (!announce-end :snippet "%% end ox-latex features\n" :order 100)))
 
 
 \f
@@ -1347,7 +1418,7 @@ (defun org-latex-generate-engraved-preamble (info)
              t t
              engraved-preamble)))
     (concat
-     "\n% Setup for code blocks [1/2]\n\n"
+     "% Setup for code blocks [1/2]\n\n"
      engraved-preamble
      "\n\n% Setup for code blocks [2/2]: syntax highlighting colors\n\n"
      (if (require 'engrave-faces-latex nil t)
@@ -1375,8 +1446,7 @@ (defun org-latex-generate-engraved-preamble (info)
                (t (funcall gen-theme-spec engraved-theme))))
            (funcall gen-theme-spec engraved-theme))
        (message "Cannot engrave source blocks. Consider installing `engrave-faces'.")
-       "% WARNING syntax highlighting unavailable as engrave-faces-latex was missing.\n")
-     "\n")))
+       "% WARNING syntax highlighting unavailable as engrave-faces-latex was missing."))))
 
 ;;;; Compilation
 
@@ -1620,29 +1690,29 @@ (defun org-latex--caption/label-string (element info)
 	      (org-trim label)
 	      (org-export-data main info))))))
 
-(defun org-latex-guess-inputenc (header)
+(defun org-latex-guess-inputenc (info)
   "Set the coding system in inputenc to what the buffer is.
 
-HEADER is the LaTeX header string.  This function only applies
-when specified inputenc option is \"AUTO\".
+INFO is the plist used as a communication channel.
+This function only applies when specified inputenc option is \"AUTO\".
 
 Return the new header, as a string."
-  (let* ((cs (or (ignore-errors
-		   (latexenc-coding-system-to-inputenc
-		    (or org-export-coding-system buffer-file-coding-system)))
-		 "utf8")))
-    (if (not cs) header
+  (let ((header (plist-get info :latex-full-header))
+        (cs (or (ignore-errors
+                  (latexenc-coding-system-to-inputenc
+                   (or org-export-coding-system buffer-file-coding-system)))
+                "utf8")))
+    (when (and cs (string-match "\\\\usepackage\\[\\(AUTO\\)\\]{inputenc}" header))
       ;; First translate if that is requested.
       (setq cs (or (cdr (assoc cs org-latex-inputenc-alist)) cs))
-      ;; Then find the \usepackage statement and replace the option.
-      (replace-regexp-in-string "\\\\usepackage\\[\\(AUTO\\)\\]{inputenc}"
-				cs header t nil 1))))
+      (plist-put info :latex-full-header
+                 (replace-match cs t t header 1))))
+  nil)
 
-(defun org-latex-guess-babel-language (header info)
+(defun org-latex-guess-babel-language (info)
   "Set Babel's language according to LANGUAGE keyword.
 
-HEADER is the LaTeX header string.  INFO is the plist used as
-a communication channel.
+INFO is the plist used as a communication channel.
 
 Insertion of guessed language only happens when Babel package has
 explicitly been loaded.  Then it is added to the rest of
@@ -1656,50 +1726,46 @@ (defun org-latex-guess-babel-language (header info)
 
 Return the new header."
   (let* ((language-code (plist-get info :language))
-	 (plist (cdr
-		 (assoc language-code org-latex-language-alist)))
-	 (language (plist-get plist :babel))
-	 (language-ini-only (plist-get plist :babel-ini-only))
-	 ;; If no language is set, or Babel package is not loaded, or
-	 ;; LANGUAGE keyword value is a language served by Babel
-	 ;; exclusively through ini files, return HEADER as-is.
-	 (header (if (or language-ini-only
-			 (not (stringp language-code))
-			 (not (string-match "\\\\usepackage\\[\\(.*\\)\\]{babel}" header)))
-		     header
-		   (let ((options (save-match-data
-				    (org-split-string (match-string 1 header) ",[ \t]*"))))
-		     ;; If LANGUAGE is already loaded, return header
-		     ;; without AUTO.  Otherwise, replace AUTO with language or
-		     ;; append language if AUTO is not present.  Languages that are
-		     ;; served in Babel exclusively through ini files are not added
-		     ;; to the babel argument, and must be loaded using
-		     ;; `\babelprovide'.
-		     (replace-match
-		      (mapconcat (lambda (option) (if (equal "AUTO" option) language option))
-				 (cond ((member language options) (delete "AUTO" options))
-				       ((member "AUTO" options) options)
-				       (t (append options (list language))))
-				 ", ")
-		      t nil header 1)))))
+         (plist (cdr (assoc language-code org-latex-language-alist)))
+         (language (plist-get plist :babel))
+         (header (plist-get info :latex-full-header))
+         (language-ini-only (plist-get plist :babel-ini-only))
+         (babel-header-options
+          ;; If no language is set, or Babel package is not loaded, or
+          ;; LANGUAGE keyword value is a language served by Babel
+          ;; exclusively through ini files, return HEADER as-is.
+          (and (not language-ini-only)
+               (stringp language-code)
+               (string-match "\\\\usepackage\\[\\(.*\\)\\]{babel}" header)
+               (let ((options (save-match-data
+                                (org-split-string (match-string 1 header) ",[ \t]*"))))
+                 (cond ((member language options) (delete "AUTO" options))
+                       ((member "AUTO" options) options)
+                       (t (append options (list language))))))))
+    (when babel-header-options
+      ;; If AUTO is present in the header options, replace it with `language'.
+      (setq header
+            (replace-match
+             (mapconcat (lambda (option) (if (equal "AUTO" option) language option))
+                        babel-header-options
+                        ", ")
+             t nil header 1)))
     ;; If `\babelprovide[args]{AUTO}' is present, AUTO is
     ;; replaced by LANGUAGE.
-    (if (not (string-match "\\\\babelprovide\\[.*\\]{\\(.+\\)}" header))
-	header
-      (let ((prov (match-string 1 header)))
-	(if (equal "AUTO" prov)
-	    (replace-regexp-in-string (format
-				       "\\(\\\\babelprovide\\[.*\\]\\)\\({\\)%s}" prov)
-				      (format "\\1\\2%s}"
-					      (or language language-ini-only))
-				      header t)
-	  header)))))
-
-(defun org-latex-guess-polyglossia-language (header info)
+    (when (string-match "\\\\babelprovide\\[.*\\]{AUTO}" header)
+      (setq header
+            (replace-regexp-in-string
+             (format
+              "\\(\\\\babelprovide\\[.*\\]\\)\\({\\)%s}" prov)
+             (format "\\1\\2%s}" (or language language-ini-only))
+             header t)))
+    (plist-put info :latex-full-header header))
+  nil)
+
+(defun org-latex-guess-polyglossia-language (info)
   "Set the Polyglossia language according to the LANGUAGE keyword.
 
-HEADER is the LaTeX header string.  INFO is the plist used as
-a communication channel.
+INFO is the plist used as a communication channel.
 
 Insertion of guessed language only happens when the Polyglossia
 package has been explicitly loaded.
@@ -1710,48 +1776,50 @@ (defun org-latex-guess-polyglossia-language (header info)
 using \setdefaultlanguage and not as an option to the package.
 
 Return the new header."
-  (let* ((language (plist-get info :language)))
+  (let ((header (plist-get info :latex-full-header))
+        (language (plist-get info :language)))
     ;; If no language is set or Polyglossia is not loaded, return
     ;; HEADER as-is.
-    (if (or (not (stringp language))
-	    (not (string-match
-		  "\\\\usepackage\\(?:\\[\\([^]]+?\\)\\]\\){polyglossia}\n"
-		  header)))
-	header
+    (when (and (stringp language)
+               (string-match
+                "\\\\usepackage\\(?:\\[\\([^]]+?\\)\\]\\){polyglossia}\n"
+                header))
       (let* ((options (org-string-nw-p (match-string 1 header)))
-	     (languages (and options
-			     ;; Reverse as the last loaded language is
-			     ;; the main language.
-			     (nreverse
-			      (delete-dups
-			       (save-match-data
-				 (org-split-string
-				  (replace-regexp-in-string
-				   "AUTO" language options t)
-				  ",[ \t]*"))))))
-	     (main-language-set
-	      (string-match-p "\\\\setmainlanguage{.*?}" header)))
-	(replace-match
-	 (concat "\\usepackage{polyglossia}\n"
-		 (mapconcat
-		  (lambda (l)
-		    (let* ((plist (cdr
-				   (assoc language org-latex-language-alist)))
-			   (polyglossia-variant (plist-get plist :polyglossia-variant))
-			   (polyglossia-lang (plist-get plist :polyglossia))
-			   (l (if (equal l language)
-				  polyglossia-lang
-				l)))
-		      (format (if main-language-set (format "\\setotherlanguage{%s}\n" l)
-				(setq main-language-set t)
-				"\\setmainlanguage%s{%s}\n")
-			      (if polyglossia-variant
-				  (format "[variant=%s]" polyglossia-variant)
-				"")
-			      l)))
-		  languages
-		  ""))
-	 t t header 0)))))
+             (languages (and options
+                             ;; Reverse as the last loaded language is
+                             ;; the main language.
+                             (nreverse
+                              (delete-dups
+                               (save-match-data
+                                 (org-split-string
+                                  (replace-regexp-in-string
+                                   "AUTO" language options t)
+                                  ",[ \t]*"))))))
+             (main-language-set
+              (string-match-p "\\\\setmainlanguage{.*?}" header))
+             (polyglossia-modified-header
+              (replace-match
+               (concat "\\usepackage{polyglossia}\n"
+                       (mapconcat
+                        (lambda (l)
+                          (let* ((plist (cdr (assoc language org-latex-language-alist)))
+                                 (polyglossia-variant (plist-get plist :polyglossia-variant))
+                                 (polyglossia-lang (plist-get plist :polyglossia))
+                                 (l (if (equal l language)
+                                        polyglossia-lang
+                                      l)))
+                            (format (if main-language-set (format "\\setotherlanguage{%s}\n" l)
+                                      (setq main-language-set t)
+                                      "\\setmainlanguage%s{%s}\n")
+                                    (if polyglossia-variant
+                                        (format "[variant=%s]" polyglossia-variant)
+                                      "")
+                                    l)))
+                        languages
+                        ""))
+               t t header 0)))
+        (plist-put info :latex-full-header polyglossia-modified-header))))
+  nil)
 
 (defun org-latex--remove-packages (pkg-alist info)
   "Remove packages based on the current LaTeX compiler.
@@ -1952,32 +2020,50 @@ (defun org-latex-make-preamble (info &optional template snippet?)
 specified in `org-latex-default-packages-alist' or
 `org-latex-packages-alist'."
   (let* ((class (plist-get info :latex-class))
-	 (class-template
-	  (or template
-	      (let* ((class-options (plist-get info :latex-class-options))
-		     (header (nth 1 (assoc class (plist-get info :latex-classes)))))
-		(and (stringp header)
-		     (if (not class-options) header
-		       (replace-regexp-in-string
-			"^[ \t]*\\\\documentclass\\(\\(\\[[^]]*\\]\\)?\\)"
-			class-options header t nil 1))))
-	      (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)))
-		    ""))))
-      info)
-     info)))
+         (class-template
+          (or template
+              (let* ((class-options (plist-get info :latex-class-options))
+                     (header (nth 1 (assoc class (plist-get info :latex-classes)))))
+                (and (stringp header)
+                     (if (not class-options) header
+                       (replace-regexp-in-string
+                        "^[ \t]*\\\\documentclass\\(\\(\\[[^]]*\\]\\)?\\)"
+                        class-options header t nil 1))))
+              (user-error "Unknown LaTeX class `%s'" class)))
+         generated-preamble)
+    (plist-put info :latex-full-header
+               (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)))
+                            ""))))
+    (setq generated-preamble
+          (if snippet?
+              (progn
+                (org-latex-guess-inputenc info)
+                (org-latex-guess-babel-language info)
+                (org-latex-guess-polyglossia-language info)
+                "\n% Generated preamble omitted for snippets.")
+            (concat
+             "\n"
+             (string-join
+              (org-export-expand-feature-snippets info)
+              "\n\n")
+             "\n")))
+    (concat
+     ;; Time-stamp.
+     (and (plist-get info :time-stamp-file)
+          (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))
+     ;; LaTeX compiler.
+     (org-latex--insert-compiler info)
+     (plist-get info :latex-full-header)
+     generated-preamble)))
 
 (defun org-latex-template (contents info)
   "Return complete document string after LaTeX conversion.
@@ -1986,12 +2072,7 @@ (defun org-latex-template (contents info)
   (let ((title (org-export-data (plist-get info :title) info))
 	(spec (org-latex--format-spec info)))
     (concat
-     ;; Time-stamp.
-     (and (plist-get info :time-stamp-file)
-	  (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))
-     ;; LaTeX compiler.
-     (org-latex--insert-compiler info)
-     ;; Document class and packages.
+     ;; Timestamp, compiler statement, document class and packages.
      (org-latex-make-preamble info)
      ;; Possibly limit depth for headline numbering.
      (let ((sec-num (plist-get info :section-numbers)))
@@ -2028,12 +2109,6 @@ (defun org-latex-template (contents info)
      (let ((template (plist-get info :latex-hyperref-template)))
        (and (stringp template)
             (format-spec template spec)))
-     ;; engrave-faces-latex preamble
-     (when (and (eq org-latex-src-block-backend 'engraved)
-                (org-element-map (plist-get info :parse-tree)
-                    '(src-block inline-src-block) #'identity
-                    info t))
-       (org-latex-generate-engraved-preamble info))
      ;; Document start.
      "\\begin{document}\n\n"
      ;; Title command.
-- 
2.39.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-oc-Make-use-of-conditional-preamble-for-export.patch --]
[-- Type: text/x-patch, Size: 9734 bytes --]

From e9816966e0a401be7abb642333532561e9192d1f Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Mon, 6 Feb 2023 00:01:41 +0800
Subject: [PATCH 4/6] oc-*: Make use of conditional preamble for export

* lisp/oc-natbib.el (org-cite-natbib-use-package): Refactor to make use
of the conditional/generated preamble.

* lisp/oc-csl.el (org-cite-csl-finalizer): Refactor to make use of
the conditional/generated preamble.

* lisp/oc-biblatex.el (org-cite-biblatex-prepare-preamble): Refactor to
make use of the conditional/generated preamble.
---
 lisp/oc-biblatex.el | 82 ++++++++++++++++++---------------------------
 lisp/oc-csl.el      | 16 ---------
 lisp/oc-natbib.el   | 33 +++++++-----------
 lisp/ox-latex.el    | 16 +++++++++
 4 files changed, 62 insertions(+), 85 deletions(-)

diff --git a/lisp/oc-biblatex.el b/lisp/oc-biblatex.el
index b2d31f0f6..e8e420891 100644
--- a/lisp/oc-biblatex.el
+++ b/lisp/oc-biblatex.el
@@ -375,61 +375,45 @@ (defun org-cite-biblatex-export-citation (citation style _ info)
        (other
         (user-error "Invalid entry %S in `org-cite-biblatex-styles'" other))))))
 
-(defun org-cite-biblatex-prepare-preamble (output _keys files style &rest _)
-  "Prepare document preamble for \"biblatex\" usage.
-
-OUTPUT is the final output of the export process.  FILES is the list of file
-names used as the bibliography.
-
-This function ensures \"biblatex\" package is required.  It also adds resources
-to the document, and set styles."
-  (with-temp-buffer
-    (save-excursion (insert output))
-    (when (search-forward "\\begin{document}" nil t)
-      ;; Ensure there is a \usepackage{biblatex} somewhere or add one.
-      ;; Then set options.
-      (goto-char (match-beginning 0))
-      (let ((re (rx "\\usepackage"
-                    (opt (group "[" (*? anything) "]"))
-                    "{biblatex}")))
-        (cond
-         ;; No "biblatex" package loaded.  Insert "usepackage" command
-         ;; with appropriate options, including style.
-         ((not (re-search-backward re nil t))
-          (save-excursion
-            (insert
-             (format "\\usepackage%s{biblatex}\n"
-                     (org-cite-biblatex--package-options
-                      org-cite-biblatex-options style)))))
-         ;; "biblatex" package loaded, but without any option.
-         ;; Include style only.
-         ((not (match-beginning 1))
-          (search-forward "{" nil t)
-          (insert (org-cite-biblatex--package-options nil style)))
-         ;; "biblatex" package loaded with some options set.  Override
-         ;; style-related options with ours.
-         (t
-          (replace-match
-           (save-match-data
-             (org-cite-biblatex--package-options (match-string 1) style))
-           nil nil nil 1))))
-      ;; Insert resources below.
-      (forward-line)
-      (insert (mapconcat (lambda (f)
-                           (format "\\addbibresource%s{%s}"
-                                   (if (org-url-p f) "[location=remote]" "")
-                                   f))
-                         files
-                         "\n")
-              "\n"))
-    (buffer-string)))
+(defun org-cite-biblatex--generate-latex-preamble (info)
+  "Ensure that the biblatex package is loaded, and the necessary resources.
+This is performed by extracting relevant information from the
+INFO export plist, and modifying any existing
+\\usepackage{biblatex} statement in the LaTeX header."
+  (let ((style (org-cite-bibliography-style info))
+        (files (plist-get info :bibliography))
+        (usepackage-rx (rx "\\usepackage"
+                           (opt (group "[" (*? anything) "]"))
+                           "{biblatex}")))
+    (concat
+     (if (string-match usepackage-rx (plist-get info :latex-full-header))
+         ;; "biblatex" package loaded, but with none (or different) options.
+         ;; Replace with style-including command.
+         (plist-put info :latex-full-header
+                    (replace-match
+                     (format "\\usepackage%s{biblatex}"
+                             (save-match-data
+                               (org-cite-biblatex--package-options nil style)))
+                     t t
+                     (plist-get info :latex-full-header)))
+       ;; No "biblatex" package loaded.  Insert "usepackage" command
+       ;; with appropriate options, including style.
+       (format "\\usepackage%s{biblatex}\n"
+               (org-cite-biblatex--package-options
+                org-cite-biblatex-options style)))
+     ;; Load resources.
+     (mapconcat (lambda (f)
+                  (format "\\addbibresource%s{%s}"
+                          (if (org-url-p f) "[location=remote]" "")
+                          f))
+                files
+                "\n"))))
 
 \f
 ;;; Register `biblatex' processor
 (org-cite-register-processor 'biblatex
   :export-bibliography #'org-cite-biblatex-export-bibliography
   :export-citation #'org-cite-biblatex-export-citation
-  :export-finalizer #'org-cite-biblatex-prepare-preamble
   :cite-styles #'org-cite-biblatex-list-styles)
 
 (provide 'oc-biblatex)
diff --git a/lisp/oc-csl.el b/lisp/oc-csl.el
index 94c2ed94c..77c758215 100644
--- a/lisp/oc-csl.el
+++ b/lisp/oc-csl.el
@@ -841,27 +841,11 @@ (defun org-cite-csl-render-bibliography (_keys _files _style props _backend info
        ;; process.
        (org-cite-parse-elements output)))))
 
-(defun org-cite-csl-finalizer (output _keys _files _style _backend info)
-  "Add \"hanging\" package if missing from LaTeX output.
-OUTPUT is the export document, as a string.  INFO is the export state, as a
-property list."
-  (org-cite-csl--barf-without-citeproc)
-  (if (not (eq 'org-latex (org-cite-csl--output-format info)))
-      output
-    (with-temp-buffer
-      (save-excursion (insert output))
-      (when (search-forward "\\begin{document}" nil t)
-	(goto-char (match-beginning 0))
-	;; Insert the CSL-specific parts of the LaTeX preamble.
-	(insert (org-cite-csl--generate-latex-preamble info)))
-      (buffer-string))))
-
 \f
 ;;; Register `csl' processor
 (org-cite-register-processor 'csl
   :export-citation #'org-cite-csl-render-citation
   :export-bibliography #'org-cite-csl-render-bibliography
-  :export-finalizer #'org-cite-csl-finalizer
   :cite-styles
   '((("author" "a") ("bare" "b") ("caps" "c") ("full" "f") ("bare-caps" "bc") ("caps-full" "cf") ("bare-caps-full" "bcf"))
     (("noauthor" "na") ("bare" "b") ("caps" "c") ("bare-caps" "bc"))
diff --git a/lisp/oc-natbib.el b/lisp/oc-natbib.el
index 9153afd86..1f1216b98 100644
--- a/lisp/oc-natbib.el
+++ b/lisp/oc-natbib.el
@@ -168,32 +168,25 @@ (defun org-cite-natbib-export-citation (citation style _ info)
           (org-cite-natbib--build-optional-arguments citation info)
           (org-cite-natbib--build-arguments citation)))
 
-(defun org-cite-natbib-use-package (output &rest _)
-  "Ensure output requires \"natbib\" package.
-OUTPUT is the final output of the export process."
-  (with-temp-buffer
-    (save-excursion (insert output))
-    (when (search-forward "\\begin{document}" nil t)
-      ;; Ensure there is a \usepackage{natbib} somewhere or add one.
-      (goto-char (match-beginning 0))
-      (let ((re (rx "\\usepackage" (opt "[" (*? nonl) "]") "{natbib}")))
-        (unless (re-search-backward re nil t)
-          (insert
-           (format "\\usepackage%s{natbib}\n"
-                   (if (null org-cite-natbib-options)
-                       ""
-                     (format "[%s]"
-                             (mapconcat #'symbol-name
-                                        org-cite-natbib-options
-                                        ","))))))))
-    (buffer-string)))
+(defun org-cite-natbib--generate-latex-preamble (info)
+  "Ensure that the \"natbib\" package is loaded.
+INFO is a plist used as a communication channel."
+  (and (not (string-match
+             (rx "\\usepackage" (opt "[" (*? nonl) "]") "{natbib}")
+             (plist-get info :latex-full-header)))
+       (format "\\usepackage%s{natbib}\n"
+               (if (null org-cite-natbib-options)
+                   ""
+                 (format "[%s]"
+                         (mapconcat #'symbol-name
+                                    org-cite-natbib-options
+                                    ","))))))
 
 \f
 ;;; Register `natbib' processor
 (org-cite-register-processor 'natbib
   :export-bibliography #'org-cite-natbib-export-bibliography
   :export-citation #'org-cite-natbib-export-citation
-  :export-finalizer #'org-cite-natbib-use-package
   :cite-styles
   '((("author" "a") ("caps" "a") ("full" "f"))
     (("noauthor" "na") ("bare" "b"))
diff --git a/lisp/ox-latex.el b/lisp/ox-latex.el
index a2bc72fc0..4be61a58d 100644
--- a/lisp/ox-latex.el
+++ b/lisp/ox-latex.el
@@ -1448,6 +1448,22 @@ (defun org-latex-generate-engraved-preamble (info)
        (message "Cannot engrave source blocks. Consider installing `engrave-faces'.")
        "% WARNING syntax highlighting unavailable as engrave-faces-latex was missing."))))
 
+;; Citation features
+
+(org-export-update-features 'latex
+  (bibliography-csl
+   :condition (eq (org-cite-processor info) 'csl)
+   :when bibliography
+   :snippet org-cite-csl--generate-latex-preamble)
+  (bibliography-biblatex
+   :condition (eq (org-cite-processor info) 'biblatex)
+   :when bibliography
+   :snippet org-cite-biblatex--generate-latex-preamble)
+  (bibliography-natbib
+   :condition (eq (org-cite-processor info) 'natbib)
+   :when bibliography
+   :snippet org-cite-natbib--generate-latex-preamble))
+
 ;;;; Compilation
 
 (defcustom org-latex-compiler-file-string "%% Intended LaTeX compiler: %s\n"
-- 
2.39.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-test-ox-Add-tests-for-export-feature-resolution.patch --]
[-- Type: text/x-patch, Size: 5451 bytes --]

From f77f8f2de0ed0119f5648a5b0397ff856833f483 Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Tue, 21 Feb 2023 01:26:42 +0800
Subject: [PATCH 5/6] test-ox: Add tests for export feature resolution

* testing/lisp/test-ox.el: Add a set of tests for
`org-export-resolve-feature-implementations'.
---
 testing/lisp/test-ox.el | 114 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 114 insertions(+)

diff --git a/testing/lisp/test-ox.el b/testing/lisp/test-ox.el
index 99f5c0f0f..918cb87d2 100644
--- a/testing/lisp/test-ox.el
+++ b/testing/lisp/test-ox.el
@@ -2133,6 +2133,120 @@ (ert-deftest test-org-export/data-with-backend ()
 	(bold . (lambda (bold contents info) (concat contents "!")))))
      '(:with-emphasize t)))))
 
+\f
+;;; Export features
+
+(ert-deftest test-org-export/feature-resolution ()
+  "Test the behaviour of `org-export-resolve-feature-implementations'"
+  ;; Check implementations for listed features are given.
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a) (b) (c)))
+          '((a) (b) (c))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a)))
+          '((a))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a) '((a) (b) (c)))
+          '((a))))
+  ;; Check depencency resolution.
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a) '((a :requires b) (b) (c)))
+          '((a :requires b) (b))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a) '((a :requires (b c)) (b) (c)))
+          '((a :requires (b c)) (b) (c))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a) '((a :requires b) (b :requires c) (c)))
+          '((a :requires b) (b :requires c) (c))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :prevents b) (b) (c)))
+          '((a :prevents b) (c))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :prevents (b c)) (b) (c)))
+          '((a :prevents (b c)))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c d) '((a :requires (b c)) (b) (c) (d :prevents b)))
+          '((a :requires (b c)) (c) (d :prevents b))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c d) '((a :requires (b c)) (b) (c :prevents b)))
+          '((a :requires (b c)) (c :prevents b))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a d) '((a :requires b) (b :requires c) (c) (d :prevents a)))
+          '((d :prevents a))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c d) '((a :requires b) (b :requires c) (c) (d :prevents a)))
+          '((b :requires c) (c) (d :prevents a))))
+  (should-error
+   (org-export-resolve-feature-implementations
+    nil '(a) '((a :requires b)))
+   :type 'org-missing-feature-dependency)
+  ;; Check application of the :when condition.
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :when b) (b)))
+          '((a :when b) (b))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :when (b c)) (b) (c)))
+          '((a :when (b c)) (b) (c))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a) (b) (c :when (a b))))
+          '((a) (b) (c :when (a b)))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :when b) (b :when c) (c)))
+          '((a :when b) (b :when c) (c))))
+  ;; Check simple ordering.
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :order 3) (b :order 1) (c :order 2)))
+          '((b :order 1) (c :order 2) (a :order 3))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :order 1) (b) (c :order -1)))
+          '((c :order -1) (b) (a :order 1))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a) '((a :order 1 :requires b) (b :requires c) (c :order -1)))
+          '((c :order -1) (b :requires c) (a :order 1 :requires b))))
+  ;; Check before/after ordering.
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :after (b c)) (b) (c)))
+          '((b) (c) (a :after (b c)))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a :after b) (b :after c) (c)))
+          '((c) (b :after c) (a :after b))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a) (b) (c :before (a b))))
+          '((c :before (a b)) (a) (b))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a) (b :before a) (c :before b)))
+          '((c :before b) (b :before a) (a))))
+  (should
+   (equal (org-export-resolve-feature-implementations
+           nil '(a b c) '((a) (b :after c :before a) (c)))
+          '((c) (b :after c :before a) (a))))
+  (should-error ; Circular dependency
+   (org-export-resolve-feature-implementations
+    nil '(a b) '((a :after b) (b :after a)))
+   :type 'org-circular-feature-dependency))
 
 \f
 ;;; Comments
-- 
2.39.1


[-- Attachment #7: 0006-org-manual-Document-export-features.patch --]
[-- Type: text/x-patch, Size: 16933 bytes --]

From 2096a4b03a57abb2dc03d986ebea1c4eb64cd967 Mon Sep 17 00:00:00 2001
From: TEC <git@tecosaur.net>
Date: Tue, 21 Feb 2023 01:26:20 +0800
Subject: [PATCH 6/6] org-manual: Document export features

* doc/org-manual.org (+*** Export features): Initial manual entry on
export features.
---
 doc/org-manual.org | 372 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 372 insertions(+)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index 5b6633417..4364d950d 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -16192,6 +16192,378 @@ *** Extending an existing back-end
 self-installing an item in the export dispatcher menu, and other
 user-friendly improvements.
 
+*** Export features
+**** The underlying idea
+
+Across export backends it is common to want to include certain chunks
+of content that are only relevant in particular situations.
+
+With static export templates, one is forced to choose between
+including everything that /might/ be wanted, or including very little
+by default and requiring common content to be manually added every
+time it is wanted.
+
+"Export features" allow for a third option, a much more sophisticated
+method of resolving this dilemma. At the start of the export process,
+the buffer being exported and the export communication plist (~info~)
+are scanned to determine which capabilities are relevant to the
+current export. During the construction of the final output this list
+of capabilities is used to produce snippets of content to be included.
+
+This can be thought of as the construction of a graph between conditions,
+features, and feature implementations. For example, say we have three conditions
+we want to support:
++ Say that images need some extra setup to be supported well, we can
+  just include it when image links are found in the buffer.
++ Say we can better support emojis by treating them as images in a
+  particular export backend. We could look for a signal in the buffer
+  that emojis should be handled as images, and then make use of some
+  "image support" and "emoji support" snippets.
++ Say that LaTeX maths requires some extra setup, we can just
+  do this when inline LaTeX fragments are found.
+This situation can be crudely drawn with the following graph:
+
+#+begin_example
+ condition            feature       implementation
+ =========            =======       ===============
+
+ [emoji] -----------> emoji ------> [emoji plist]
+         \
+          '---->----.
+                     \
+ [image link] ------> image ------> [image plist]
+
+ [inline LaTeX] ----> maths ------> [maths plist]
+
+ \____________________/   \__________________/
+        phase 1                 phase 2
+#+end_example
+
+In phase 1 "feature detection" the relevant features are determined, and in
+phase 2 "feature implementation" how those features can be provided is worked
+out.
+
+**** Feature detection
+
+After the expansion of =#+include= statements and the removal of
+comments, the export communication plist (~info~) is annotated. At the
+very end of the annotation process, ~org-export-detect-features~ is
+run to determine the list of capabilities relevant to the current export.
+
+This operates by merging the global feature condition alist
+(~org-export-conditional-features~) with the ~feature-conditions~ slot
+of the current backend and each of its parents. This produces the
+total feature conditions alist, which has the form:
+
+#+begin_example
+((condition . implied-features)
+ ...)
+#+end_example
+
+Where =condition= is a test that implies that =implied-features= are
+relevant. While =implied-features= is always a list of feature
+symbols, for convenience =condition= can take a number of forms,
+namely:
++ A regexp which is searched for in the export buffer.
++ A variable, if a string it is used as a regexp search, otherwise any
+  non-nil value is taken to imply =implied-features=.
++ A (unary) function, which is called with on the export communication
+  plist (~info~). A returned string is used as a regexp search,
+  otherwise any non-nil value is taken to imply =implied-features=.
+
+As an example, a feature conditions alist which checks whether any
+headings exist, and if the word "hello" appears could take the
+following form:
+
+#+begin_example
+(((lambda (info)
+   (org-element-map (plist-get info :parse-tree) 'heading
+    #'identity info t))
+  headlines)
+ ("hello" has-greeting))
+#+end_example
+
+Conditions are inherited from parent export backends and (for conditions general
+enough to apply across backends) the variable ~org-export-conditional-features~.
+
+#+begin_example
+                                                    ,--> beamer
+                                                   /
+  html <----.                      ,----> latex --'
+             \                    /
+        org-export-conditional-features
+             /                    \
+ ascii <----'                      '----> odt
+#+end_example
+
+**** Feature implementations
+
+The other half of the export feature system is of course producing
+snippets of content from the list of features. Export backends can do
+this at any point via ~org-export-expand-feature-snippets~. This
+operates on a /feature implementation alist/. The implementation alist
+is essentially a mirror of the condition alist, instead of =(condition
+. feature-list)= elements it takes =(feature . implementation-plist)=
+elements. This reversal of order may seem a bit odd, but it should
+help make the condition--feature--implementation graph more apparent.
+
+For example, considering this example condition alist and implementation alist
+would be represented as the earlier graph.
+
+#+begin_example
+;; The condition alist
+(([emoji predicate] emoji image)
+ ([image predicate] image)
+ ([inline LaTeX predicate] maths))
+;; The implementation alist
+((emoji [plist])
+ (image [plist])
+ (maths [plist])
+#+end_example
+
+The implementation plist recognises a number of keywords, but the
+primary keyword is ~:snippet~. The snippet value provides the snippet
+content used to provide the feature's capability. Much like
+~condition~, it accepts a number of forms for convenience, namely:
++ A string, which is passed on.
++ A variable symbol, the value of which must be a string.
++ A (unary) function, which is called on the export communication
+  plist (~info~), and must return a string.
+
+Note that no keys are mandatory in the implementation plist (not even
+~:snippet~).
+
+Like conditions, implementations are also inherited from parent backends, but
+there is no "root" global list of implementations, as they are always
+backend-specific.
+
+#+begin_example
+                                     ,--> beamer
+                                    /
+  html <---o         o---> latex --'
+
+ ascii <---o         o---> odt
+#+end_example
+
+**** Feature dependency and incompatibility
+
+While just connecting features with snippets satisfies most use cases,
+this system is designed to also allow for complex configurations of
+inter-dependent snippets.
+
+In slightly more complex examples, we may run across implementations
+which either (a) only make sense when another feature is active, or
+(b) require another implementation to be used in order to work. These
+two situations are covered by the ~:when~ and ~:requires~ keywords
+respectively. The accept either a single feature symbol or a list of
+feature symbols.
+
+For example, if an implementation contains ~:when featA~, it will only
+be used when =featA= is active. If the implementation contains ~:when
+(featA featB)~ it will require /both/ =featA= and =featB= to be
+active. The ~:requires~ keyword works in the same way, but
+unconditionally requires implementations instead of testing for them.
+
+Occasionally one implementation may be incompatible with another. For
+example, in LaTeX loading the same package with different options will
+often produce an "options clash" error. To ensure that incompatible
+implementations are not used, the ~:prevents~ keyword makes it as if
+the feature were never used in the first place.
+
+Circular ~:requires~ and ~:prevents~, or features that are
+simultaneously required and prevented result in undefined behaviour.
+Similarly, the behaviour of mutual ~:when~​s (e.g. ~(a :when b) (b
+:when a)~ is also undefined.
+
+**** Feature ordering
+
+In many scenarios it is not only /which/ snippets are included that
+matters, but the /order/ in which they are placed. A requirement for a
+certain snippet to appear before/after others can be specified through
+the ~:before~ and ~:after~ keywords. Like ~:when~, ~:requires~, and
+~:prevents~ they accept either a single feature symbol or a list of
+feature symbols.
+
+As an example, should an implementation plist contain ~:before featA
+:after (featB featC)~ it will be placed after =featA= but before
+=featB= and =featC=. It is possible to accidentally create circular
+dependencies, in which case an error will be raised.
+
+While ~:before~ and ~:after~ work well for specifying relative
+ordering, it can also be useful to specify the /absolute/ ordering,
+for instance to put something first or last. This can be controlled
+via the ~:order~ keyword. Each implementation has an ~:order~ of zero
+by default. Implementations with a higher ~:order~ come later.
+
+# REVIEW maybe give a convention on :order ranges?
+# Perhaps take inspiration from ~add-hook~.
+
+-----
+
+The overall ordering behaviour can be characterized as a ascending
+sort of ~:order~ followed by a stable [[https://en.wikipedia.org/wiki/Topological_sorting][topological sort]] based on
+~:before~ and ~:after~.
+
+**** Adding or editing export features
+
+The export features of a backend can be modified via the convenience
+macro ~org-export-update-features~. This is invoked with the following
+form:
+
+#+begin_example
+(org-export-update-features 'BACKEND
+ (FEATURE-NAME
+  :PROPERTY VALUE
+  ...)
+ ...)
+#+end_example
+
+For each feature mentioned, it sets the each =:PROPERTY= to =VALUE= in
+the implementation plist. The one exception to this is ~:condition~ in
+which case the backend's feature condition alist is modified so that
+the condition is taken to imply the feature.
+
+Setting ~:condition t~ will thus make the feature enabled by default. This is not
+a special case, but rather an instance of the general behaviour of obtaining the
+value of any non-function symbol provided, and as ~(symbol-value 't)~ is always
+non-nil, the associated features will always be considered active. Conversely
+setting ~:condition nil~ will make it so no conditions imply the feature. This is
+possible thanks to special behavior that removes the feature from all other
+conditions' associations when ~nil~ is given.
+
+Since having a anonymous function (lambda) is expected to be
+reasonably common with ~:condition~ and ~:snippet~, for those keywords
+and sexp given is implicitly wrapped with ~(lambda (info) SEXP)~.
+
+If the backend is not available at the time the feature update is run,
+an error will be raised.
+
+**** Custom export feature examples
+
+To make the usage of ~org-export-update-features~ and the capabilities
+of the export feature system clearer, here are a few examples
+~org-export-update-features~ invocations.
+
+Say you want to apply the [[https://ctan.org/pkg/chickenize][chickenize]] package to the word "wacky" when
+the title starts with "wacky". We can implement that by testing for
+the regexp =^#\\+title: Wacky= and including
+src_latex{\usepackage[chickenstring[1]='wacky']{chickenize}} when it is
+found.
+
+#+begin_example
+(org-export-update-features 'latex
+ (wacky-chicken
+  :condition "^#\\+title: Wacky"
+  :snippet "\\usepackage[chickenstring[1]='wacky']{chickenize}"))
+#+end_example
+
+However, if =#+title: Wacky= is placed inside an example block, this
+regexp will match even though the match isn't actually parsed as a
+keyword. To be more robust, we can inspect ~info~ instead.
+
+#+begin_example
+(org-export-update-features 'latex
+ (wacky-chicken
+  :condition (and (car (plist-get info :title))
+                  (string-match-p "^Wacky" (car (plist-get info :title))))
+  :snippet "\\usepackage[chickenstring[1]='wacky']{chickenize}"))
+#+end_example
+
+=chickenize= is a LuaLaTeX only package, and so trying to use this
+when compiling with pdfLaTeX or XeLaTeX will cause issues. This may
+well apply to other snippets too, so it could make sense to make a
+=lualatex= feature to indicate when LuaLaTeX is being used.
+
+#+begin_example
+(org-export-update-features 'latex
+ (lualatex
+  :condition (equal (plist-get info :latex-compiler) "lualatex")))
+#+end_example
+
+To only use the chickenize snippet with LuaLaTeX, we can now add
+=lualatex= as a ~:when~ clause.
+
+#+begin_example
+(org-export-update-features 'latex
+ (wacky-chicken
+  :when lualatex))
+#+end_example
+
+Hopefully this has given you an impression of how ~:condition~, ~:when~, and
+~:snippet~ can look in practice. To demonstrate ~:requires~, ~:prevents~, and
+~:after~ we will consider another LaTeX example.
+
+Say that you wanted a few named special blocks to automatically export nicely,
+such as =#+begin_warning=, =#+begin_info=, and =#+begin_note=. All three of
+these can be individually detected and handled appropriately. For the sake of
+simplicity, a crude regexp will be used here, however examining the parse tree
+would be a more robust solution.
+
+#+begin_example
+(org-export-update-features 'latex
+ (box-warning
+  :condition "^[ \t]*#\\+begin_warning"
+  :snippet "\\mysetupbox{warning}"
+  :requires mysetupbox
+  :after mysetupbox)
+ (box-info
+  :condition "^[ \t]*#\\+begin_info"
+  :snippet "\\mysetupbox{info}"
+  :requires mysetupbox
+  :after mysetupbox)
+ (box-note
+  :condition "^[ \t]*#\\+begin_note"
+  :snippet "\\mysetupbox{note}"
+  :requires mysetupbox
+  :after mysetupbox)
+ (mysetupbox
+  :snippet "\newcommand{\mysetupbox}...")
+#+end_example
+
+Here, all three box types make use of LaTeX command ~\mysetupbox~ which is
+provided by the =mysetupbox= feature implementation. By using a ~:requires~ for
+this, we can make sure it is availible that it is loaded once, and thanks to the
+~:after mysetupbox~ lines the specific box setup invocations will occur after
+the ~\newcommand{\mysetupbox}...~ definition.
+
+Say that when using beamer you use a package that defines its own
+warning/info/note environments and you'd like to use those. In that case one can
+make an on-by-default beamer feature that prevents the box features from being
+used.
+
+#+begin_example
+(org-export-update-features 'beamer
+ (no-custom-boxes
+  :condition t
+  :prevents (box-warning box-info box-note)))
+#+end_example
+
+**** Detailed explanation of the implementation resolution process
+
+The previous descriptions of the "export feature" behaviour should give a clear
+overview of how this feature works. In case more detail is wanted, or should
+there be any ambiguity, here is a more technical description of the overall
+feature implementation resolution process.
+
+1. The list of detected features is used to obtain all applicable corresponding
+   feature implementations. Detected feature symbols may have /no/ corresponding
+   implementation.
+2. The list of feature implementations is sorted according to ~:order~.
+3. Using a queue, all required features (~:requires~) are added to the list of feature
+ implementations, as are their requirements recursively. Should a feature that
+   has no implementation be required, an ~org-missing-feature-dependency~ error is raised.
+4. The implementations with a ~:when~ condition are scanned and marked as
+   confirmed when all of the ~:when~ conditions are known to be satisfied. This is
+   repeated until there is no change in the list of confirmed implementations,
+   at which point all non-confirmed implementations are removed.
+5. For each of the feature implementations in turn, prevented features
+   (~:prevents~) are removed from the list. Feature implementations that are only
+   present because of a feature that has now been removed are themselves
+   removed, recursively.
+6. The list of feature implementations is sorted according to ~:order~, again.
+7. A stable topological sort is performed using ~:before~ and ~:after~. Should any
+   circular dependencies be found, a ~org-circular-feature-dependency~ error is raised.
+
 ** Export in Foreign Buffers
 :PROPERTIES:
 :DESCRIPTION: Author tables and lists in Org syntax.
-- 
2.39.1


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-20 17:41 ` Timothy
@ 2023-02-24 12:51   ` Sébastien Miquel
  2023-02-24 12:59     ` Ihor Radchenko
  2023-02-25  3:15     ` Timothy
  0 siblings, 2 replies; 11+ messages in thread
From: Sébastien Miquel @ 2023-02-24 12:51 UTC (permalink / raw)
  To: Timothy; +Cc: emacs-orgmode

Hi,

Timothy writes:
> Notably, I’ve now got a draft manual entry (see the last patch attached), which
> should go a long way to better explaining what this is without asking you to
> wade through all the code comments 😄

Thank you for sharing your work Timothy, I've been using your export
features for a while.

I think it would be useful to support an easy way to toggle a feature
on and off. Either the manual should describe the best way to make a
feature depend on a user variable (this requires using the #+BIND
keyword, I guess), or I'd like org to support a new keyword, such as
  : #+org_export_features
that would take a list of features, and enable them. Here, I am
thinking of features such as the `chikenize` example in the manual.

I have not tested it, but after a quick look at the patch, enabled
features should be available to the export transcoder functions,
through their =info= argument, aren't they ? This seems very useful.

-- 
Sébastien Miquel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-24 12:51   ` Sébastien Miquel
@ 2023-02-24 12:59     ` Ihor Radchenko
  2023-02-24 21:47       ` Sébastien Miquel
  2023-02-25  3:15     ` Timothy
  1 sibling, 1 reply; 11+ messages in thread
From: Ihor Radchenko @ 2023-02-24 12:59 UTC (permalink / raw)
  To: sebastien.miquel; +Cc: Timothy, emacs-orgmode

Sébastien Miquel <sebastien.miquel@posteo.eu> writes:

> I think it would be useful to support an easy way to toggle a feature
> on and off. Either the manual should describe the best way to make a
> feature depend on a user variable (this requires using the #+BIND
> keyword, I guess), or I'd like org to support a new keyword, such as
>   : #+org_export_features
> that would take a list of features, and enable them. Here, I am
> thinking of features such as the `chikenize` example in the manual.

The traditional user-facing approach for toggling staff in export is
setting export options.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-24 12:59     ` Ihor Radchenko
@ 2023-02-24 21:47       ` Sébastien Miquel
  2023-02-26 12:19         ` Ihor Radchenko
  0 siblings, 1 reply; 11+ messages in thread
From: Sébastien Miquel @ 2023-02-24 21:47 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode

Ihor Radchenko writes:
>> I think it would be useful to support an easy way to toggle a feature
>> on and off. Either the manual should describe the best way to make a
>> feature depend on a user variable (this requires using the #+BIND
>> keyword, I guess), or I'd like org to support a new keyword, such as
>>    : #+org_export_features
>> that would take a list of features, and enable them. Here, I am
>> thinking of features such as the `chikenize` example in the manual.
> The traditional user-facing approach for toggling staff in export is
> setting export options.

Indeed. Then I suggest that such use be described in the manual.
Having the user create a whole new option in order to toggle a
`chikenize` package seems a bit unwieldy, however.

-- 
Sébastien Miquel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-24 12:51   ` Sébastien Miquel
  2023-02-24 12:59     ` Ihor Radchenko
@ 2023-02-25  3:15     ` Timothy
  1 sibling, 0 replies; 11+ messages in thread
From: Timothy @ 2023-02-25  3:15 UTC (permalink / raw)
  To: sebastien.miquel; +Cc: emacs-orgmode

[-- Attachment #1: Type: text/plain, Size: 892 bytes --]

Hi Sebastien,

> I think it would be useful to support an easy way to toggle a feature
> on and off. Either the manual should describe the best way to make a
> feature depend on a user variable (this requires using the #+BIND
> keyword, I guess), or I’d like org to support a new keyword, such as
>  : #+org_export_features
> that would take a list of features, and enable them. Here, I am
> thinking of features such as the `chikenize` example in the manual.

A large part of the idea here is that these “export features” will automatically
 be used when relevant, without having to be manually enabled/disabled.

All the best,
Timothy

-- 
Timothy (‘tecosaur’/‘TEC’), Org mode contributor.
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/tec>.

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-24 21:47       ` Sébastien Miquel
@ 2023-02-26 12:19         ` Ihor Radchenko
  2023-02-26 13:04           ` Sébastien Miquel
  0 siblings, 1 reply; 11+ messages in thread
From: Ihor Radchenko @ 2023-02-26 12:19 UTC (permalink / raw)
  To: sebastien.miquel; +Cc: emacs-orgmode

Sébastien Miquel <sebastien.miquel@posteo.eu> writes:

>> The traditional user-facing approach for toggling staff in export is
>> setting export options.
>
> Indeed. Then I suggest that such use be described in the manual.
> Having the user create a whole new option in order to toggle a
> `chikenize` package seems a bit unwieldy, however.

What do you suggest instead of creating a new option?

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-26 12:19         ` Ihor Radchenko
@ 2023-02-26 13:04           ` Sébastien Miquel
  2023-02-27 19:05             ` Ihor Radchenko
  0 siblings, 1 reply; 11+ messages in thread
From: Sébastien Miquel @ 2023-02-26 13:04 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, Timothy


Ihor Radchenko writes:
>>> The traditional user-facing approach for toggling staff in export is
>>> setting export options.
>> Indeed. Then I suggest that such use be described in the manual.
>> Having the user create a whole new option in order to toggle a
>> `chikenize` package seems a bit unwieldy, however.
> What do you suggest instead of creating a new option?

Timothy's patch supports having a feature depend on a variable. I was
thinking here of a user variable (instead of an already defined org
variable) that could be toggled per document.

It is fairly orthogonal to the original purpose of the patch, but it
does scratch an annoying itch of mine: there's very little support in
org to minutely tweak the export on a per document basis, beyond the
default options.

One way to do this (easier than setting up a new option), is to define
any variable, make the feature depend on it, and set it using the
`#+bind` keyword. Combined with these export features, this could be
used to easily tweak the LaTeX preamble, per document.

If we find this use case to be legitimate and useful, I suggested
earlier making it even easier with a `#+org_export_features` keyword.
It would only take a list of feature names, and not require any
variable tied to the feature.

-- 
Sébastien Miquel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH] Introduce "export features"
  2023-02-26 13:04           ` Sébastien Miquel
@ 2023-02-27 19:05             ` Ihor Radchenko
  0 siblings, 0 replies; 11+ messages in thread
From: Ihor Radchenko @ 2023-02-27 19:05 UTC (permalink / raw)
  To: sebastien.miquel; +Cc: emacs-orgmode, Timothy

Sébastien Miquel <sebastien.miquel@posteo.eu> writes:

> Ihor Radchenko writes:
>>>> The traditional user-facing approach for toggling staff in export is
>>>> setting export options.
>>> Indeed. Then I suggest that such use be described in the manual.
>>> Having the user create a whole new option in order to toggle a
>>> `chikenize` package seems a bit unwieldy, however.
>> What do you suggest instead of creating a new option?
>
> Timothy's patch supports having a feature depend on a variable. I was
> thinking here of a user variable (instead of an already defined org
> variable) that could be toggled per document.
>
> It is fairly orthogonal to the original purpose of the patch, but it
> does scratch an annoying itch of mine: there's very little support in
> org to minutely tweak the export on a per document basis, beyond the
> default options.

Defining a new option is easy:

(org-export-define-derived-backend 'my-html 'html
  :options-alist
  '((:my-option "MY_OPTION_KEYWORD" nil "my-option" my-option-variable)))

Then, you can either

#+:BIND: my-option-variable t
or
#+OPTIONS: my-option:t
or
#+MY_OPTION_KEYWORD: t

Of course, you will also need to plug support of :my-option as needed
via filters or altering transcoders.

Timothy, since you are going to provide a macro for altering the
existing backends, it would also make sense to generalize it for
altering other aspects of the backends, like option-alist,
filters-alist, and menu-entry.

> One way to do this (easier than setting up a new option), is to define
> any variable, make the feature depend on it, and set it using the
> `#+bind` keyword. Combined with these export features, this could be
> used to easily tweak the LaTeX preamble, per document.
>
> If we find this use case to be legitimate and useful, I suggested
> earlier making it even easier with a `#+org_export_features` keyword.
> It would only take a list of feature names, and not require any
> variable tied to the feature.

I do not see much point compared to option alist, which is more
flexible. And you can always define a feature with condition to match
against "#\\+org_export_features.+my-feature".

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2023-02-27 19:06 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
     [not found] <mailman.13.1676134175.1258.emacs-orgmode@gnu.org>
2023-02-11 17:03 ` [PATCH] Introduce "export features" No Wayman
2023-02-10 17:20 Timothy
2023-02-11 11:37 ` Ihor Radchenko
2023-02-20 17:41 ` Timothy
2023-02-24 12:51   ` Sébastien Miquel
2023-02-24 12:59     ` Ihor Radchenko
2023-02-24 21:47       ` Sébastien Miquel
2023-02-26 12:19         ` Ihor Radchenko
2023-02-26 13:04           ` Sébastien Miquel
2023-02-27 19:05             ` Ihor Radchenko
2023-02-25  3:15     ` Timothy

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).