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
next prev 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).