Org: re-implement conditional preamble generation

It's *extra* nice now
This commit is contained in:
TEC 2021-03-18 02:44:44 +08:00
parent 0cc34f64bc
commit 32ab25b884
Signed by: tec
GPG Key ID: 779591AFDB81F06C
2 changed files with 263 additions and 55 deletions

View File

@ -7046,8 +7046,6 @@ Let's consider some other content we only want in certain situations.
#+name: org-latex-checkbox-preamble
#+begin_src LaTeX
\\usepackage{pifont}
\\usepackage{amssymb} % for \square
\\newcommand{\\checkboxUnchecked}{$\\square$}
\\newcommand{\\checkboxTransitive}{\\rlap{\\raisebox{-0.1ex}{\\hspace{0.35ex}\\Large\\textbf -}}$\\square$}
\\newcommand{\\checkboxChecked}{\\rlap{\\raisebox{0.2ex}{\\hspace{0.35ex}\\scriptsize \\ding{52}}}$\\square$}
@ -7056,7 +7054,6 @@ Let's consider some other content we only want in certain situations.
#+name: org-latex-box-preamble
#+begin_src LaTeX
% args = #1 Name, #2 Colour, #3 Ding, #4 Label
\\usepackage{pifont}
\\newcommand{\\defsimplebox}[4]{%
\\definecolor{#1}{HTML}{#2}
\\newenvironment{#1}[1][]
@ -7070,10 +7067,6 @@ Let's consider some other content we only want in certain situations.
\\vspace{-0.5\\baselineskip}
}%
}
\\defsimplebox{warning}{e66100}{\\ding{68}}{Warning}
\\defsimplebox{info}{3584e4}{\\ding{68}}{Information}
\\defsimplebox{success}{26a269}{\\ding{68}}{\\vspace{-\\baselineskip}}
\\defsimplebox{error}{c01c28}{\\ding{68}}{Important}
#+end_src
Lastly, we will pass this content into some global variables we for ease of
@ -7152,64 +7145,192 @@ exists.
"\n"))
#+end_src
***** Implementation
#+name: org-latex-conditional-preamble
#+begin_src emacs-lisp
(defvar org-latex-conditional-preambles
'((t . org-latex-universal-preamble)
("^[ ]*#\\+begin_src" . org-latex-embed-tangled-files)
("\\[\\[file:\\(?:[^\\]]+?|\\\\\\]\\)\\.svg\\]\\]" . "\\usepackage{svg}")
("\\[\\[file:\\(?:[^]]\\|\\\\\\]\\)+\\.\\(?:eps\\|pdf\\|png\\|jpeg\\|jpg\\|jbig2\\)\\]\\]" . "\\usepackage{graphicx}")
("^[ ]*|" . "\\usepackage{longtable}\n\\usepackage{booktabs}")
("\\\\(\\|\\\\\\[\\|\\\\begin{\\(?:math\\|displaymath\\|equation\\|align\\|flalign\\|multiline\\|gather\\)[a-z]*\\*?}"
. "\\usepackage{bmc-maths}")
("cref:\\|\\cref{" . "\\usepackage{cleveref}")
("\\+[^ ].*[^ ]\\+\\|_[^ ].*[^ ]_\\|\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith"
. "\\usepackage[normalem]{ulem}")
(":float wrap" . "\\usepackage{wrapfig}")
(":float sideways" . "\\usepackage{rotating}")
("^[ ]*#\\+caption:\\|\\\\caption" . org-latex-caption-preamble)
("^[ ]*\\(?:[-+*]\\|[0-9]+[.)]\\|[A-Za-z]+[.)]\\) \\[[ -X]\\]" . org-latex-checkbox-preamble)
("^[ ]*#\\+begin_\\(?:warning\\|info\\|success\\|error\\)\\|\\\\begin{\\(?:warning\\|info\\|success\\|error\\)}"
. org-latex-box-preamble))
"Snippets which are conditionally included in the preamble of a LaTeX export.
***** Content-feature-preamble association
Initially this idea was implemented with an alist that associated a construct
that would search the current Org file for an indication that some feature was
needed, with a LaTeX snippet to be inserted in the preamble which would provide
that feature.
This is all well and good when there is a bijection between detected features
and the LaTeX code needed to support those features, but in many cases this
relation is not injective.
Alist where when the car results in a non-nil value, the cdr is inserted in
the preamble. The car may be a:
To better model the reality of the situation, I add an extra layer to this
process where each detected feature gives a list of required "feature flags".
Simply be merging the lists of feature flags we no longer have to require
injectivity to avoid LaTeX duplication. Then the extra layer forms a bijection
between there feature flags and a specification which can be used to implement
the feature.
This model also provides a number of nice secondary benefits, such as a simple
implementation of feature dependency.
#+begin_src dot :file misc/org-latex-clever-preamble.svg :exports none
digraph {
graph [bgcolor="transparent"];
node [shape="underline" penwidth="2" width="1.3" style="rounded,filled" fillcolor="#efefef" color="#c9c9c9" fontcolor="#000000" fontname="overpass"];
edge [arrowhead=none color="#aaaaaa" penwidth="1.2"]
rankdir=LR
"Checkboxes" [color="#2ec27e"]
"Fancy blocks" [color="#2ec27e"]
"Images" [color="#2ec27e"]
"Dingbats" [color="#f5c211"]
"Graphics" [color="#f5c211"]
"pifont" [color="#813d9c"]
"graphicx" [color="#813d9c"]
"Checkboxes" -> "Dingbats" -> "pifont"
"Fancy blocks" -> "Dingbats"
"Images" -> "Graphics" -> "graphicx"
}
#+end_src
#+caption: Association between Org features, feature flags, and LaTeX snippets required.
#+attr_html: :class invertible :alt Graph of possible Emacs task integrations :style max-width:min(24em,100%)
[[file:misc/org-latex-clever-preamble.svg]]
#+begin_src emacs-lisp
(defvar org-latex-conditional-features
'((t . universal)
("^[ ]*#\\+begin_src" . embed-tangled)
("\\[\\[file:\\(?:[^]]+?\\|\\\\\\]\\)\\.svg\\]\\]" . svg)
("\\[\\[file:\\(?:[^]]\\|\\\\\\]\\)+?\\.\\(?:eps\\|pdf\\|png\\|jpeg\\|jpg\\|jbig2\\)\\]\\]" . image)
("\\\\(\\|\\\\\\[\\|\\\\begin{\\(?:math\\|displaymath\\|equation\\|align\\|flalign\\|multiline\\|gather\\)[a-z]*\\*?}" . maths)
("^[ ]*|" . table)
("cref:\\|\\cref{" . cleveref)
("\\+[^ ].*[^ ]\\+\\|_[^ ].*[^ ]_\\|\\\\uu?line\\|\\\\uwave\\|\\\\sout\\|\\\\xout\\|\\\\dashuline\\|\\dotuline\\|\\markoverwith" . underline)
(":float wrap" . float-wrap)
(":float sideways" . rotate)
("^[ ]*#\\+caption:\\|\\\\caption" . caption)
("^[ ]*\\(?:[-+*]\\|[0-9]+[.)]\\|[A-Za-z]+[.)]\\) \\[[ -X]\\]" . checkbox)
("^[ ]*#\\+begin_warning\\|\\\\begin{warning}" . box-warning)
("^[ ]*#\\+begin_info\\|\\\\begin{info}" . box-info)
("^[ ]*#\\+begin_success\\|\\\\begin{success}" . box-success)
("^[ ]*#\\+begin_error\\|\\\\begin{error}" . box-error))
"Org feature tests and associated LaTeX feature flags.
Alist where the car is the feature test and may be a:
- string, which is used as a regex search in the buffer
- symbol, the value of which used
- function, the result of the function is used
- list, which passed to eval
The cdr may be a:
- string, which is inserted without processing
- symbol, the value of which is inserted
- function, the result of which is inserted")
The cdr is either a single feature symbol or list of feature symbols.")
#+end_src
#+begin_src emacs-lisp
(defvar org-latex-feature-implementations
'((universal :snippet org-latex-universal-preamble)
(embed-tangled :snippet (org-latex-embed-tangled-files))
(image :snippet "\\usepackage{graphicx}")
(svg :snippet "\\usepackage{svg}")
(maths :snippet "\\usepackage{bmc-maths}" :priority 0)
(table :snippet "\\usepackage{longtable}\n\\usepackage{booktabs}")
(cleveref :snippet "\\usepackage{cleveref}" :priority -1)
(underline :snippet "\\usepackage[normalem]{ulem}")
(float-wrap :snippet "\\usepackage{wrapfig}")
(rotate :snippet "\\usepackage{rotating}")
(caption :snippet org-latex-caption-preamble)
(pifont :snippet "\\usepackage{pifont}")
(checkbox :requires pifont :snippet (concat (unless (memq 'maths features)
"\\usepackage{amssymb} % provides \\square")
org-latex-checkbox-preamble))
(fancy-box :requires pifont :snippet org-latex-box-preamble)
(box-warning :requires fancy-box :snippet "\\defsimplebox{warning}{e66100}{\\ding{68}}{Warning}")
(box-info :requires fancy-box :snippet "\\defsimplebox{info}{3584e4}{\\ding{68}}{Information}")
(box-success :requires fancy-box :snippet "\\defsimplebox{success}{26a269}{\\ding{68}}{\\vspace{-\\baselineskip}}")
(box-error :requires fancy-box :snippet "\\defsimplebox{error}{c01c28}{\\ding{68}}{Important}"))
"LaTeX features and details required to implement them.
List where the car is the feature symbol, and the rest forms a plist with the
following keys:
- :snippet, which may be either
- a string which should be included in the preamble
- a symbol, the value of which is included in the preamble
- a function, which is evaluated with the list of feature flags as its
single argument. The result of which is included in the preamble
- a list, which is passed to `eval', with a list of feature flags available
as \"features\".
- :requires, a feature or list of features that must be available
- :priority, for when ordering is important. Highest priority features are
considered first. The default priority is 0.")
#+end_src
***** Feature determination
Now that we have ~org-latex-conditional-features~ defined, we need to use it to
extract a list of features found in an Org buffer.
#+begin_src emacs-lisp
(defun org-latex-detect-features (&optional buffer)
"List features from `org-latex-conditional-features' detected in BUFFER."
(with-current-buffer (or buffer (current-buffer))
(delete-dups
(apply #'append
(mapcar (lambda (construct-feature)
(when (pcase (car construct-feature)
((pred stringp) (save-excursion
(goto-char (point-min))
(search-forward-regexp (car construct-feature) nil t)))
((pred functionp) (funcall (car construct-feature)))
((pred listp) (eval (car construct-feature)))
((pred symbolp) (symbol-value (car construct-feature)))
(_ (user-error "org-latex-conditional-features key %s unable to be used" (car construct-feature))))
(if (listp (cdr construct-feature)) (cdr construct-feature) (list (cdr construct-feature)))))
org-latex-conditional-features)))))
#+end_src
***** Preamble generation
Once a list of required features has been determined, we want to use
~org-latex-feature-implementations~ to generate the LaTeX which should be inserted
into the preamble to provide those features.
This is done in stages.
+ The dependencies for each listed feature are added to feature list (=:requires=).
+ The feature list is scrubbed of duplicates
+ The feature list is sorted by =:priority=
+ The value of each feature's =:snippet= parameter is concatenated
#+begin_src emacs-lisp
(defun org-latex-generate-features-preamble (features)
"Generate the LaTeX preamble content required to provide FEATURES.
This is done according to `org-latex-feature-implementations'"
(dolist (feature features)
(unless (assoc feature org-latex-feature-implementations)
(error "Feature %s not provided in org-latex-feature-implementations" feature))
(when-let ((requirements (plist-get (cdr (assoc feature org-latex-feature-implementations)) :requires)))
(setf features (append (if (listp requirements) requirements (list requirements)) features))))
(mapconcat (lambda (feature)
(when-let ((snippet (plist-get (cdr (assoc feature org-latex-feature-implementations)) :snippet)))
(concat
(pcase snippet
((pred stringp) snippet)
((pred functionp) (funcall snippet features))
((pred listp) (eval `(let ((features ',features)) (,@snippet))))
((pred symbolp) (symbol-value snippet))
(_ (user-error "org-latex-feature-implementations :snippet value %s unable to be used" snippet)))
"\n")))
(sort (delete-dups features)
(lambda (feat1 feat2)
(if (> (or (plist-get (cdr (assoc feat1 org-latex-feature-implementations)) :priority) 0)
(or (plist-get (cdr (assoc feat2 org-latex-feature-implementations)) :priority) 0))
t nil)))
""))
#+end_src
Then Org needs to be advised to actually use this generated preamble content.
#+begin_src emacs-lisp
(defadvice! org-latex-header-smart-preamble (orig-fn tpl def-pkg pkg snippets-p &optional extra)
"Dynamically insert preamble content based on `org-latex-conditional-preambles'."
:around #'org-splice-latex-header
(let ((header (funcall orig-fn tpl def-pkg pkg snippets-p extra)))
(if snippets-p header
(concat header
(mapconcat (lambda (term-preamble)
(when (pcase (car term-preamble)
((pred stringp) (save-excursion
(goto-char (point-min))
(search-forward-regexp (car term-preamble) nil t)))
((pred functionp) (funcall (car term-preamble)))
((pred symbolp) (symbol-value (car term-preamble)))
(_ (user-error "org-latex-conditional-preambles key %s unable to be used" (car term-preamble))))
(concat
(pcase (cdr term-preamble)
((pred stringp) (cdr term-preamble))
((pred functionp) (funcall (cdr term-preamble)))
((pred symbolp) (symbol-value (cdr term-preamble)))
(_ (user-error "org-latex-conditional-preambles value %s unable to be used" (cdr term-preamble))))
"\n")))
org-latex-conditional-preambles
"") "\n"))))
(org-latex-generate-features-preamble (org-latex-detect-features))
"\n"))))
#+end_src
***** Reduce default packages
Thanks to our additions, we can remove a few packages from
@ -7237,7 +7358,7 @@ exporting that as LaTeX commands.
(setq org-latex-listings 'engraved) ; NOTE non-standard value
#+end_src
Thanks to ~org-latex-conditional-preambles~ and some copy-paste with the =minted=
Thanks to ~org-latex-conditional-features~ and some copy-paste with the =minted=
entry in ~org-latex-scr-block~ we can easily add this as a recognised
~org-latex-listings~ value.
@ -7260,8 +7381,9 @@ entry in ~org-latex-scr-block~ we can easily add this as a recognised
<<org-latex-engraved-code-preamble>>
")
(add-to-list 'org-latex-conditional-preambles '("^[ ]*#\\+BEGIN_SRC\\|#\\+begin_src" . org-latex-engraved-code-preamble) t)
(add-to-list 'org-latex-conditional-preambles '("^[ ]*#\\+BEGIN_SRC\\|#\\+begin_src" . engrave-faces-latex-gen-preamble) t)
(add-to-list 'org-latex-feature-implementations '(engraved-code-setup :snippet org-latex-engraved-code-preamble :priority -2))
(add-to-list 'org-latex-feature-implementations '(engraved-code :requires engraved-code-setup :snippet (engrave-faces-latex-gen-preamble) :priority -2.1))
(add-to-list 'org-latex-conditional-features '("^[ ]*#\\+BEGIN_SRC\\|^[ ]*#\\+begin_src\\|src_[A-Za-z]" . engraved-code))
(defun org-latex-scr-block--engraved (src-block contents info)
(let* ((lang (org-element-property :language src-block))

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.42.3 (0)
-->
<!-- Title: %3 Pages: 1 -->
<svg width="365pt" height="152pt"
viewBox="0.00 0.00 365.00 152.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 148)">
<title>%3</title>
<!-- Checkboxes -->
<g id="node1" class="node">
<title>Checkboxes</title>
<polygon fill="#efefef" stroke="transparent" stroke-width="2" points="95.5,-90 1.5,-90 1.5,-54 95.5,-54 95.5,-90"/>
<polyline fill="none" stroke="#2ec27e" stroke-width="2" points="1.5,-54 95.5,-54 "/>
<text text-anchor="middle" x="48.5" y="-68.3" font-family="overpass" font-size="14.00" fill="#000000">Checkboxes</text>
</g>
<!-- Dingbats -->
<g id="node4" class="node">
<title>Dingbats</title>
<polygon fill="#efefef" stroke="transparent" stroke-width="2" points="227,-63 133,-63 133,-27 227,-27 227,-63"/>
<polyline fill="none" stroke="#f5c211" stroke-width="2" points="133,-27 227,-27 "/>
<text text-anchor="middle" x="180" y="-41.3" font-family="overpass" font-size="14.00" fill="#000000">Dingbats</text>
</g>
<!-- Checkboxes&#45;&gt;Dingbats -->
<g id="edge1" class="edge">
<title>Checkboxes&#45;&gt;Dingbats</title>
<path fill="none" stroke="#aaaaaa" stroke-width="1.2" d="M95.52,-62.4C107.59,-59.89 120.61,-57.17 132.69,-54.66"/>
</g>
<!-- Fancy blocks -->
<g id="node2" class="node">
<title>Fancy blocks</title>
<polygon fill="#efefef" stroke="transparent" stroke-width="2" points="97,-36 0,-36 0,0 97,0 97,-36"/>
<polyline fill="none" stroke="#2ec27e" stroke-width="2" points="0,0 97,0 "/>
<text text-anchor="middle" x="48.5" y="-14.3" font-family="overpass" font-size="14.00" fill="#000000">Fancy blocks</text>
</g>
<!-- Fancy blocks&#45;&gt;Dingbats -->
<g id="edge3" class="edge">
<title>Fancy blocks&#45;&gt;Dingbats</title>
<path fill="none" stroke="#aaaaaa" stroke-width="1.2" d="M97.35,-27.98C108.88,-30.38 121.18,-32.94 132.64,-35.33"/>
</g>
<!-- Images -->
<g id="node3" class="node">
<title>Images</title>
<polygon fill="#efefef" stroke="transparent" stroke-width="2" points="95.5,-144 1.5,-144 1.5,-108 95.5,-108 95.5,-144"/>
<polyline fill="none" stroke="#2ec27e" stroke-width="2" points="1.5,-108 95.5,-108 "/>
<text text-anchor="middle" x="48.5" y="-122.3" font-family="overpass" font-size="14.00" fill="#000000">Images</text>
</g>
<!-- Graphics -->
<g id="node5" class="node">
<title>Graphics</title>
<polygon fill="#efefef" stroke="transparent" stroke-width="2" points="227,-144 133,-144 133,-108 227,-108 227,-144"/>
<polyline fill="none" stroke="#f5c211" stroke-width="2" points="133,-108 227,-108 "/>
<text text-anchor="middle" x="180" y="-122.3" font-family="overpass" font-size="14.00" fill="#000000">Graphics</text>
</g>
<!-- Images&#45;&gt;Graphics -->
<g id="edge4" class="edge">
<title>Images&#45;&gt;Graphics</title>
<path fill="none" stroke="#aaaaaa" stroke-width="1.2" d="M95.52,-126C107.59,-126 120.61,-126 132.69,-126"/>
</g>
<!-- pifont -->
<g id="node6" class="node">
<title>pifont</title>
<polygon fill="#efefef" stroke="transparent" stroke-width="2" points="357,-63 263,-63 263,-27 357,-27 357,-63"/>
<polyline fill="none" stroke="#813d9c" stroke-width="2" points="263,-27 357,-27 "/>
<text text-anchor="middle" x="310" y="-41.3" font-family="overpass" font-size="14.00" fill="#000000">pifont</text>
</g>
<!-- Dingbats&#45;&gt;pifont -->
<g id="edge2" class="edge">
<title>Dingbats&#45;&gt;pifont</title>
<path fill="none" stroke="#aaaaaa" stroke-width="1.2" d="M227.21,-45C238.81,-45 251.26,-45 262.86,-45"/>
</g>
<!-- graphicx -->
<g id="node7" class="node">
<title>graphicx</title>
<polygon fill="#efefef" stroke="transparent" stroke-width="2" points="357,-144 263,-144 263,-108 357,-108 357,-144"/>
<polyline fill="none" stroke="#813d9c" stroke-width="2" points="263,-108 357,-108 "/>
<text text-anchor="middle" x="310" y="-122.3" font-family="overpass" font-size="14.00" fill="#000000">graphicx</text>
</g>
<!-- Graphics&#45;&gt;graphicx -->
<g id="edge5" class="edge">
<title>Graphics&#45;&gt;graphicx</title>
<path fill="none" stroke="#aaaaaa" stroke-width="1.2" d="M227.21,-126C238.81,-126 251.26,-126 262.86,-126"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB