From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp11.migadu.com ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms5.migadu.com with LMTPS id iN3MCZ2x82PwKQAAbAwnHQ (envelope-from ) for ; Mon, 20 Feb 2023 18:45:01 +0100 Received: from aspmx1.migadu.com ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp11.migadu.com with LMTPS id CDTJCZ2x82PeFAEA9RJhRA (envelope-from ) for ; Mon, 20 Feb 2023 18:45:01 +0100 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 52BD029C2A for ; Mon, 20 Feb 2023 18:44:59 +0100 (CET) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1pUAD6-0004c0-SF; Mon, 20 Feb 2023 12:44:20 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pUAD5-0004bj-Lu for emacs-orgmode@gnu.org; Mon, 20 Feb 2023 12:44:19 -0500 Received: from out5-smtp.messagingengine.com ([66.111.4.29]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1pUACy-0001kd-DL for emacs-orgmode@gnu.org; Mon, 20 Feb 2023 12:44:19 -0500 Received: from compute2.internal (compute2.nyi.internal [10.202.2.46]) by mailout.nyi.internal (Postfix) with ESMTP id E06805C01E6 for ; Mon, 20 Feb 2023 12:44:08 -0500 (EST) Received: from mailfrontend1 ([10.202.2.162]) by compute2.internal (MEProxy); Mon, 20 Feb 2023 12:44:08 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tecosaur.net; h= cc:content-type:date:date:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:sender:subject :subject:to:to; s=fm3; t=1676915048; x=1677001448; bh=wawAqNrGLd V43X3z8PChJJheV4e1VWO+l5OLaqBbpZc=; b=ZXqbt4Dp15ROkMZ+eV3Stu5XGm fjdedlMX3w5Jr7pQ9cr37blCNO9rte26X1SBUhacwVge7rmLgTbX3S5ByuRx2ocr iiO5QhYt6+ZVKJKG+UtkRojm1Rp0kenvwKgQF3p3PdNb0GQDSu7dqwt26dFcNLav /W54Qcb1UvFKGMqZ0i3G2GHPXJbH3QlEaNFkakFS2+wt7pbcegIwltBgucvqF8xH T2ITqbOn6IQujlKuK7opDeh4eK8Q4GrmBZ/JWhheJFY9IfzSPKpaehWVMvUAOFWA exMYXRJg1oPvT17/kkZ/MFYJKOCRroetJV5uVuMwA3d3iTDyyyHAO/vH831g== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-type:date:date:feedback-id :feedback-id:from:from:in-reply-to:in-reply-to:message-id :mime-version:references:reply-to:sender:subject:subject:to:to :x-me-proxy:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s= fm1; t=1676915048; x=1677001448; bh=wawAqNrGLdV43X3z8PChJJheV4e1 VWO+l5OLaqBbpZc=; b=KrFHbO9GhbSz8skk2FDCTf58JlXZAMHi4k6D0e3FI9Rq +fytLOXiayZDPCDVsCsy7tSCQ5NJOx+Yao62Ibtupi13jE7UwWSX17VG2tm7d0eZ ZgTdT812pM1PBeVcI4wuaGuGbpCjJA+1zUHDXN/P5mxEx+vGv40hRk1AvTPkCkjG QorBQBgj5twI43s2msoStIuJ74L2cvedqQlsR+3e0ShDKNfNkgjslyvT+/egshni z8Xh3zG9SPjzTkk/fdn7Kukl2hCsSrwbuvQQlRLHGrnBa8fopq/o16Mk/g3eUP8S 5YbR58XvscFnaiVldps/KKfCGloQ9LYPiPnghE+41Q== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvhedrudejhedgleekucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen uceurghilhhouhhtmecufedttdenucenucfjughrpefhvffuffhffggjkfggtgesmhdtre ertderjeenucfhrhhomhepvfhimhhothhhhicuoehorhhgmhhouggvsehtvggtrdhtvggt ohhsrghurhdrnhgvtheqnecuggftrfgrthhtvghrnhephfdvudellefffeefkedvteevhe euudejfedthfeiiedvkeejhfefteeilefgfeetnecuffhomhgrihhnpehorhhgmhhouggv rdhorhhgpdhlihgsvghrrghprgihrdgtohhmnecuvehluhhsthgvrhfuihiivgeptdenuc frrghrrghmpehmrghilhhfrhhomhepohhrghhmohguvgesthgvtgdrthgvtghoshgruhhr rdhnvght X-ME-Proxy: Feedback-ID: iecf94634:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA for ; Mon, 20 Feb 2023 12:44:07 -0500 (EST) From: Timothy To: emacs-orgmode@gnu.org Subject: Re: [PATCH] Introduce "export features" Date: Tue, 21 Feb 2023 01:41:40 +0800 References: <875yc95rxp.fsf@tec.tecosaur.net> User-agent: mu4e 1.8.13; emacs 28.2.50 In-reply-to: <875yc95rxp.fsf@tec.tecosaur.net> Message-ID: <87ilfwnsf4.fsf@tec.tecosaur.net> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Received-SPF: none client-ip=66.111.4.29; envelope-from=orgmode@tec.tecosaur.net; helo=out5-smtp.messagingengine.com X-Spam_score_int: -26 X-Spam_score: -2.7 X-Spam_bar: -- X-Spam_report: (-2.7 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_LOW=-0.7, SPF_HELO_PASS=-0.001, SPF_NONE=0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-orgmode@gnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "General discussions about Org-mode." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-orgmode-bounces+larch=yhetil.org@gnu.org Sender: emacs-orgmode-bounces+larch=yhetil.org@gnu.org X-Migadu-Country: US X-Migadu-Flow: FLOW_IN ARC-Seal: i=1; s=key1; d=yhetil.org; t=1676915100; a=rsa-sha256; cv=none; b=uFmGZnFzEEqmk1TtwD9LtTRoscKBM4ei2m8U3zqhOE1hzmamuz/RR9k6i4Bc9tzUxgocTr m29oZPNsGyxssUOUvXIO1NOOF+Y0/0a1+frjJOCEaYn7hC4QfoCnGEQe9/0lP0gUGKRWuV 29LGE4ZZiQUsJsG5Q9ZsRH8hzQoKrX5ROgLMJu1E2id8MCloQvgYJtkjEY1Epi6GjQ2+rX 2wtC0r903Ed/NqQsN6rRze1sFk9MC6Fz/bCOPIEfY3B6ODAcquwUkWWkW1ULwelReKNvcF jFyTIjOChRpUBMs0Aow2RTQQk0SHsAWVZHVnuHwuzT3cwJhqOIa9PoCFM04AIQ== ARC-Authentication-Results: i=1; aspmx1.migadu.com; dkim=fail ("body hash did not verify") header.d=tecosaur.net header.s=fm3 header.b=ZXqbt4Dp; dkim=fail ("body hash did not verify") header.d=messagingengine.com header.s=fm1 header.b=KrFHbO9G; dmarc=none; spf=pass (aspmx1.migadu.com: domain of "emacs-orgmode-bounces+larch=yhetil.org@gnu.org" designates 209.51.188.17 as permitted sender) smtp.mailfrom="emacs-orgmode-bounces+larch=yhetil.org@gnu.org" ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=yhetil.org; s=key1; t=1676915100; h=from:from:sender:sender:reply-to:subject:subject:date:date: message-id:message-id:to:to:cc:mime-version:mime-version: content-type:content-type:in-reply-to:in-reply-to: references:references:list-id:list-help:list-unsubscribe: list-subscribe:list-post:dkim-signature; bh=/J0WjuRGdqjJP0yEqVKLLaR7UQ0RG0R4lkFM4+EiYwo=; b=WxS3uTOwnawKi2AnoGU800TimeSJMXiXlE1mnNxBEcjQhLe/bInafaMho0xzpz1yJ0Rdgr FWlzU4A50Y85g2GoPEKmXHqMa+19NAt/BSqDMa7kfCfuZCMLvUOhFXHItAx20US/k8CY2T SGHaWOSfR7r5DChCweC1XGgfPQhCBgx0psY2c6HmzUH2TcAGyOuPj+NU9yp0p+Aj2HqOKb n2RJqO4MMDbz2CvbVjgeUM3liyJJfpNwWyfXa30kxU7Bp+Qi2zYEpCfcVRorxmQ2rv1Edd A36olH2OLGRoRBNKbWc/krkk9ueswYjw85p5eQMHOthKPQP+SihDJWdr/fJqSQ== Authentication-Results: aspmx1.migadu.com; dkim=fail ("body hash did not verify") header.d=tecosaur.net header.s=fm3 header.b=ZXqbt4Dp; dkim=fail ("body hash did not verify") header.d=messagingengine.com header.s=fm1 header.b=KrFHbO9G; dmarc=none; spf=pass (aspmx1.migadu.com: domain of "emacs-orgmode-bounces+larch=yhetil.org@gnu.org" designates 209.51.188.17 as permitted sender) smtp.mailfrom="emacs-orgmode-bounces+larch=yhetil.org@gnu.org" X-Migadu-Scanner: scn1.migadu.com X-Migadu-Spam-Score: -0.45 X-Spam-Score: -0.45 X-Migadu-Queue-Id: 52BD029C2A X-TUID: C/e28CzJ1Gu+ --=-=-= Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: quoted-printable Hi All, Following some feedback I=E2=80=99ve received from a few people (including = off-list), here=E2=80=99s a v2 set of patches. Notably, I=E2=80=99ve now got a draft manual entry (see the last patch atta= ched), 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 --=20 Timothy (=E2=80=98tecosaur=E2=80=99/=E2=80=98TEC=E2=80=99), Org mode contri= butor. Learn more about Org mode at . Support Org development at , or support my work at . --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-org-compat-Add-ensure-list-as-org-ensure-list.patch >From a8ed768515e4cf305ba52566f372e096f8ed449a Mon Sep 17 00:00:00 2001 From: TEC 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)))) + ;;; Emacs < 27.1 compatibility -- 2.39.1 --=-=-= Content-Type: text/x-patch; charset=utf-8 Content-Disposition: attachment; filename=0002-ox-Introduce-conditional-generated-preamble.patch Content-Transfer-Encoding: quoted-printable >From 3a1d9eeb4e77a15f1466bc2377838b38d97d5b22 Mon Sep 17 00:00:00 2001 From: TEC 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 =20 (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 f= eature-implementations) =20 ;;;###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))) =20 +(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 backe= nd)) + (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-implementat= ions backend)) + (unless (assq (car implementation) implementations) + (push implementation implementations)))) + implementations))) + +(defun org-export-install-features (info) + "Install feature conditions and implementations in the communication cha= nnel. +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. =20 @@ -1247,20 +1303,24 @@ (defun org-export-define-backend (backend transcode= rs &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-implementati= ons) (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-implement= ations)))) =20 (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 paren= t &rest body) =20 (org-export-to-buffer \\=3D'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 paren= t &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 paren= t &rest body) :transcoders transcoders :options options :filters filters - :menu menu-entry)))) + :menu menu-entry + :feature-conditions feature-conditions + :feature-implementations feature-implement= ations)))) =20 =20 @@ -2030,6 +2094,539 @@ (defun org-export-expand (blob contents &optional w= ith-affiliated) (funcall (intern (format "org-element-%s-interpreter" type)) blob contents)))) =20 + +;;; Conditional/Generated Features +;; +;; Many formats have some version of a preamble, whether it be HTML's +;; ... 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 =3D + (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 use= d" + condition features))) + for active-features =3D + (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 =E2=80=94 the export in= fo, + 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 implementa= tion + ;; is used. It takes the form of an alist with feature symbols a= s the + ;; keys, and a list of features that ask for that feature as valu= es. + ;; 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-implementatio= ns + ;; 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) :requi= res))) + (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 coul= d 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 condition= s. + ;; 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) :prev= ents))) + (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 topologic= al + ;; 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 for more informati= on +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 gra= ph: %S" + (cons (car deps) + (nreverse (memq (car deps) + (nreverse + (mapcar #'car node-st= ack)))))))) + ;; 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-implemen= tations) + "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 inv= alid (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 s= tring, 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-implement= ations' 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 \\=3D'beamer + (beamer-metropolis + :condition (string-match-p \"metropolis$\" (plist-get info :beamer-th= eme)) + :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 t= he-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-imple= mentations))) + (backend-conds + (list 'aref backend-struct + (cl-struct-slot-offset 'org-export-backend 'feature-condi= tions))) + 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 ,backen= d-conds))) + (if ,the-entry + (setcdr ,the-entry + (append ',the-features (cdr ,the-e= ntry))) + (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)))) =20 ;;; The Filter System @@ -3186,6 +3783,8 @@ (defun org-export--annotate-info (backend info &optio= nal 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)) =20 ;;;###autoload --=20 2.39.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0003-ox-latex-Apply-new-generated-preamble-to-export.patch >From 364d9f02a6d6b5e44e93a054a3da8c1328820061 Mon Sep 17 00:00:00 2001 From: TEC 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))) @@ -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 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0004-oc-Make-use-of-conditional-preamble-for-export.patch >From e9816966e0a401be7abb642333532561e9192d1f Mon Sep 17 00:00:00 2001 From: TEC 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")))) ;;; 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)))) - ;;; 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 + ",")))))) ;;; 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 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0005-test-ox-Add-tests-for-export-feature-resolution.patch >From f77f8f2de0ed0119f5648a5b0397ff856833f483 Mon Sep 17 00:00:00 2001 From: TEC 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))))) + +;;; 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)) ;;; Comments -- 2.39.1 --=-=-= Content-Type: text/x-patch; charset=utf-8 Content-Disposition: attachment; filename=0006-org-manual-Document-export-features.patch Content-Transfer-Encoding: quoted-printable >From 2096a4b03a57abb2dc03d986ebea1c4eb64cd967 Mon Sep 17 00:00:00 2001 From: TEC 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. =20 +*** 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 cond= itions +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 + =3D=3D=3D=3D=3D=3D=3D=3D=3D =3D=3D=3D=3D=3D=3D=3D =3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + + [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 wor= ked +out. + +**** Feature detection + +After the expansion of =3D#+include=3D 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 =3Dcondition=3D is a test that implies that =3Dimplied-features=3D a= re +relevant. While =3Dimplied-features=3D is always a list of feature +symbols, for convenience =3Dcondition=3D 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 =3Dimplied-features=3D. ++ 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 =3Dimplied-features=3D. + +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 g= eneral +enough to apply across backends) the variable ~org-export-conditional-feat= ures~. + +#+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 =3D(condition +. feature-list)=3D elements it takes =3D(feature . implementation-plist)=3D +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 a= list +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 =3DfeatA=3D is active. If the implementation contains ~:when +(featA featB)~ it will require /both/ =3DfeatA=3D and =3DfeatB=3D 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~=E2=80=8Bs (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 =3DfeatA=3D but before +=3DfeatB=3D and =3DfeatC=3D. 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/Topo= logical_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 =3D:PROPERTY=3D to =3DVALUE= =3D 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 obtaini= ng the +value of any non-function symbol provided, and as ~(symbol-value 't)~ is a= lways +non-nil, the associated features will always be considered active. Convers= ely +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 =3D^#\\+title: Wacky=3D and including +src_latex{\usepackage[chickenstring[1]=3D'wacky']{chickenize}} when it is +found. + +#+begin_example +(org-export-update-features 'latex + (wacky-chicken + :condition "^#\\+title: Wacky" + :snippet "\\usepackage[chickenstring[1]=3D'wacky']{chickenize}")) +#+end_example + +However, if =3D#+title: Wacky=3D 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]=3D'wacky']{chickenize}")) +#+end_example + +=3Dchickenize=3D 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 +=3Dlualatex=3D 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 +=3Dlualatex=3D 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~, a= nd +~: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 nic= ely, +such as =3D#+begin_warning=3D, =3D#+begin_info=3D, and =3D#+begin_note=3D.= 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 =3Dmysetupbox=3D feature implementation. By using a ~:requ= ires~ 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 af= ter +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 o= ne can +make an on-by-default beamer feature that prevents the box features from b= eing +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 shou= ld +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 correspo= nding + feature implementations. Detected feature symbols may have /no/ corresp= onding + implementation. +2. The list of feature implementations is sorted according to ~:order~. +3. Using a queue, all required features (~:requires~) are added to the lis= t 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 implementati= ons, + 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 a= re 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~, ag= ain. +7. A stable topological sort is performed using ~:before~ and ~:after~. Sh= ould any + circular dependencies be found, a ~org-circular-feature-dependency~ err= or is raised. + ** Export in Foreign Buffers :PROPERTIES: :DESCRIPTION: Author tables and lists in Org syntax. --=20 2.39.1 --=-=-=--