emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
From: Timothy <orgmode@tec.tecosaur.net>
To: emacs-orgmode@gnu.org
Subject: Re: [PATCH] Introduce "export features"
Date: Tue, 21 Feb 2023 01:41:40 +0800	[thread overview]
Message-ID: <87ilfwnsf4.fsf@tec.tecosaur.net> (raw)
In-Reply-To: <875yc95rxp.fsf@tec.tecosaur.net>

[-- 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


  parent reply	other threads:[~2023-02-20 17:45 UTC|newest]

Thread overview: 28+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-02-10 17:20 [PATCH] Introduce "export features" Timothy
2023-02-11 11:37 ` Ihor Radchenko
2023-02-20 17:41 ` Timothy [this message]
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
2023-02-21 14:22 ` [POLL] Naming of " Timothy
2023-02-22  1:46   ` Dr. Arne Babenhauserheide
2023-02-22  2:40     ` Timothy
2023-02-23 15:55       ` No Wayman
2023-02-23 16:17         ` No Wayman
2023-02-22 12:23   ` Ihor Radchenko
2023-02-23 15:31     ` No Wayman
2023-02-23 16:04     ` Bruce D'Arcus
2023-02-23 19:04       ` Ihor Radchenko
2023-02-23 19:55     ` Sébastien Miquel
2023-02-24 10:27       ` Ihor Radchenko
2023-02-24 12:46         ` Sébastien Miquel
2023-02-24 13:03           ` Ihor Radchenko
2023-02-24 21:38             ` Sébastien Miquel
2023-02-26 12:28               ` Ihor Radchenko
2023-02-26 14:06                 ` Sébastien Miquel
2023-02-27 19:32                   ` Ihor Radchenko
     [not found] <mailman.13.1676134175.1258.emacs-orgmode@gnu.org>
2023-02-11 17:03 ` [PATCH] Introduce " No Wayman

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.orgmode.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=87ilfwnsf4.fsf@tec.tecosaur.net \
    --to=orgmode@tec.tecosaur.net \
    --cc=emacs-orgmode@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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).