For some inline formulas, such as , the default baseline
186 | vertical alignment is not ideal. You can adjust it manually, using a
187 | negative value to lower the image below the baseline: . In this case, I’ve specified
192 | a -0.5em value, which is about half a baseline down.
193 |
194 |
To check that the filter processes elements of arbitrary depth, we’ve
195 | placed the next bit within a dummy Div block.
196 |
197 |
The display formula below is not explicitly marked to be imagified.
198 | However, it will be imagified in the filter’s scope option
199 | is set to all:
202 |
203 |
This next formula is imagified with options provided for elements of
204 | a custom class, highlightme: . They display
207 | the formula as an inline instead of a block and add a red border. They
208 | also specify a large zoom (4) but we’ve overridden it and locally
209 | specified a zoom of 1.
210 |
211 |
The filter automatically recognizes TikZ pictures and loads the TikZ
212 | package with the tikz option for the
213 | standalone. When dvisvgm is used for
214 | conversion to SVG, the required dvisvgm option is set
215 | too:
216 |
252 |
253 |
We can also use separate .tex and .tikz
254 | files as sources for images. The filter converts them to PDF (for
255 | LaTeX/PDF output) or SVG as required. That is useful to create
256 | cross-referencable figures with Pandoc-Crossref and Quarto.
257 |
258 |
261 | Figure 1 is a separate TikZ
262 | file
263 |
264 |
265 |
268 | Figure 2 is a separate LaTeX
269 | file
270 |
271 |
Currently, these should not contain a LaTeX preamble or
272 | \begin{document}. There is no difference between
273 | .tikz and .tex sources here. A TikZ picture in
274 | a .tikz file should still have
275 | \begin{tikzpicture} or \tikz commands.
276 |
277 |
We can also use LaTeX packages that are provided in the document’s
278 | folder, here fitch.sty (a package not available on
279 | CTAN):
280 |
294 |
295 |
296 |
297 |
--------------------------------------------------------------------------------
/example-pandoc/figure1.tikz:
--------------------------------------------------------------------------------
1 | \usetikzlibrary {arrows.meta,graphs,shapes.misc}
2 | \tikz [>={Stealth[round]}, black!50, text=black, thick,
3 | every new ->/.style = {shorten >=1pt},
4 | graphs/every graph/.style = {edges=rounded corners},
5 | skip loop/.style = {to path={-- ++(0,#1) -| (\tikztotarget)}},
6 | hv path/.style = {to path={-| (\tikztotarget)}},
7 | vh path/.style = {to path={|- (\tikztotarget)}},
8 | nonterminal/.style = {
9 | rectangle, minimum size=6mm, very thick, draw=red!50!black!50, top color=white,
10 | bottom color=red!50!black!20, font=\itshape, text height=1.5ex,text depth=.25ex},
11 | terminal/.style = {
12 | rounded rectangle, minimum size=6mm, very thick, draw=black!50, top color=white,
13 | bottom color=black!20, font=\ttfamily, text height=1.5ex, text depth=.25ex},
14 | shape = coordinate
15 | ]
16 | \graph [grow right sep, branch down=7mm, simple] {
17 | / -> unsigned integer[nonterminal] -- p1 -> "." [terminal] -- p2 -> digit[terminal] -- p3 -- p4 -- p5 -> E[terminal] -- q1 ->[vh path]
18 | {[nodes={yshift=7mm}]
19 | "+"[terminal], q2, "-"[terminal]
20 | } -> [hv path]
21 | q3 -- /unsigned integer [nonterminal] -- p6 -> /;
22 | p1 ->[skip loop=5mm] p4;
23 | p3 ->[skip loop=-5mm] p2;
24 | p5 ->[skip loop=-11mm] p6;
25 |
26 | q1 -- q2 -- q3; % make these edges plain
27 | };
--------------------------------------------------------------------------------
/example-pandoc/figure2.tex:
--------------------------------------------------------------------------------
1 | $\displaystyle
2 | \left|\int_a^b fg\right| \leq \left(\int_a^b
3 | f^2\right)^{1/2}\left(\int_a^b g^2\right)^{1/2}
4 | $
--------------------------------------------------------------------------------
/example-pandoc/fitch.sty:
--------------------------------------------------------------------------------
1 | % Macros for Fitch-style natural deduction.
2 | % Author: Peter Selinger, University of Ottawa
3 | % Created: Jan 14, 2002
4 | % Modified: Feb 8, 2005
5 | % Version: 0.5
6 | % Copyright: (C) 2002-2005 Peter Selinger
7 | % Filename: fitch.sty
8 | % Documentation: fitchdoc.tex
9 | % URL: http://quasar.mathstat.uottawa.ca/~selinger/fitch/
10 | % new URL: https://www.mathstat.dal.ca/~selinger/fitch/
11 |
12 | % License:
13 | %
14 | % This program is free software; you can redistribute it and/or modify
15 | % it under the terms of the GNU General Public License as published by
16 | % the Free Software Foundation; either version 2, or (at your option)
17 | % any later version.
18 | %
19 | % This program is distributed in the hope that it will be useful, but
20 | % WITHOUT ANY WARRANTY; without even the implied warranty of
21 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 | % General Public License for more details.
23 | %
24 | % You should have received a copy of the GNU General Public License
25 | % along with this program; if not, write to the Free Software Foundation,
26 | % Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
27 |
28 | % USAGE EXAMPLE:
29 | %
30 | % The following is a simple example illustrating the usage of this
31 | % package. For detailed instructions and additional functionality, see
32 | % the user guide, which can be found in the file fitchdoc.tex.
33 | %
34 | % \[
35 | % \begin{nd}
36 | % \hypo{1} {P\vee Q}
37 | % \hypo{2} {\neg Q}
38 | % \open
39 | % \hypo{3a} {P}
40 | % \have{3b} {P} \r{3a}
41 | % \close
42 | % \open
43 | % \hypo{4a} {Q}
44 | % \have{4b} {\neg Q} \r{2}
45 | % \have{4c} {\bot} \ne{4a,4b}
46 | % \have{4d} {P} \be{4c}
47 | % \close
48 | % \have{5} {P} \oe{1,3a-3b,4a-4d}
49 | % \end{nd}
50 | % \]
51 |
52 | {\chardef\x=\catcode`\*
53 | \catcode`\*=11
54 | \global\let\nd*astcode\x}
55 | \catcode`\*=11
56 |
57 | % References
58 |
59 | \newcount\nd*ctr
60 | \def\nd*render{\expandafter\ifx\expandafter\nd*x\nd*base\nd*x\the\nd*ctr\else\nd*base\ifnum\nd*ctr<0\the\nd*ctr\else\ifnum\nd*ctr>0+\the\nd*ctr\fi\fi\fi}
61 | \expandafter\def\csname nd*-\endcsname{}
62 |
63 | \def\nd*num#1{\nd*numo{\nd*render}{#1}\global\advance\nd*ctr1}
64 | \def\nd*numopt#1#2{\nd*numo{$#1$}{#2}}
65 | \def\nd*numo#1#2{\edef\x{#1}\mbox{$\x$}\expandafter\global\expandafter\let\csname nd*-#2\endcsname\x}
66 | \def\nd*ref#1{\expandafter\let\expandafter\x\csname nd*-#1\endcsname\ifx\x\relax%
67 | \errmessage{Undefined natdeduction reference: #1}\else\mbox{$\x$}\fi}
68 | \def\nd*noop{}
69 | \def\nd*set#1#2{\ifx\relax#1\nd*noop\else\global\def\nd*base{#1}\fi\ifx\relax#2\relax\else\global\nd*ctr=#2\fi}
70 | \def\nd*reset{\nd*set{}{1}}
71 | \def\nd*refa#1{\nd*ref{#1}}
72 | \def\nd*aux#1#2{\ifx#2-\nd*refa{#1}--\def\nd*c{\nd*aux{}}%
73 | \else\ifx#2,\nd*refa{#1}, \def\nd*c{\nd*aux{}}%
74 | \else\ifx#2;\nd*refa{#1}; \def\nd*c{\nd*aux{}}%
75 | \else\ifx#2.\nd*refa{#1}. \def\nd*c{\nd*aux{}}%
76 | \else\ifx#2)\nd*refa{#1})\def\nd*c{\nd*aux{}}%
77 | \else\ifx#2(\nd*refa{#1}(\def\nd*c{\nd*aux{}}%
78 | \else\ifx#2\nd*end\nd*refa{#1}\def\nd*c{}%
79 | \else\def\nd*c{\nd*aux{#1#2}}%
80 | \fi\fi\fi\fi\fi\fi\fi\nd*c}
81 | \def\ndref#1{\nd*aux{}#1\nd*end}
82 |
83 | % Layer A
84 |
85 | % define various dimensions (explained in fitchdoc.tex):
86 | \newlength{\nd*dim}
87 | \newdimen\nd*depthdim
88 | \newdimen\nd*hsep
89 | \newdimen\ndindent
90 | \ndindent=1em
91 | % user command to redefine dimensions
92 | \def\nddim#1#2#3#4#5#6#7#8{\nd*depthdim=#3\relax\nd*hsep=#6\relax%
93 | \def\nd*height{#1}\def\nd*thickness{#8}\def\nd*initheight{#2}%
94 | \def\nd*indent{#5}\def\nd*labelsep{#4}\def\nd*justsep{#7}}
95 | % set initial dimensions
96 | \nddim{4.5ex}{3.5ex}{1.5ex}{1em}{1.6em}{.5em}{2.5em}{.2mm}
97 |
98 | \def\nd*v{\rule[-\nd*depthdim]{\nd*thickness}{\nd*height}}
99 | \def\nd*t{\rule[-\nd*depthdim]{0mm}{\nd*height}\rule[-\nd*depthdim]{\nd*thickness}{\nd*initheight}}
100 | \def\nd*i{\hspace{\nd*indent}}
101 | \def\nd*s{\hspace{\nd*hsep}}
102 | \def\nd*g#1{\nd*f{\makebox[\nd*indent][c]{$#1$}}}
103 | \def\nd*f#1{\raisebox{0pt}[0pt][0pt]{$#1$}}
104 | \def\nd*u#1{\makebox[0pt][l]{\settowidth{\nd*dim}{\nd*f{#1}}%
105 | \addtolength{\nd*dim}{2\nd*hsep}\hspace{-\nd*hsep}\rule[-\nd*depthdim]{\nd*dim}{\nd*thickness}}\nd*f{#1}}
106 |
107 | % Lists
108 |
109 | \def\nd*push#1#2{\expandafter\gdef\expandafter#1\expandafter%
110 | {\expandafter\nd*cons\expandafter{#1}{#2}}}
111 | \def\nd*pop#1{{\def\nd*nil{\gdef#1{\nd*nil}}\def\nd*cons##1##2%
112 | {\gdef#1{##1}}#1}}
113 | \def\nd*iter#1#2{{\def\nd*nil{}\def\nd*cons##1##2{##1#2{##2}}#1}}
114 | \def\nd*modify#1#2#3{{\def\nd*nil{\gdef#1{\nd*nil}}\def\nd*cons##1##2%
115 | {\advance#2-1 ##1\advance#2 1 \ifnum#2=1\nd*push#1{#3}\else%
116 | \nd*push#1{##2}\fi}#1}}
117 |
118 | \def\nd*cont#1{{\def\nd*t{\nd*v}\def\nd*v{\nd*v}\def\nd*g##1{\nd*i}%
119 | \def\nd*i{\nd*i}\def\nd*nil{\gdef#1{\nd*nil}}\def\nd*cons##1##2%
120 | {##1\expandafter\nd*push\expandafter#1\expandafter{##2}}#1}}
121 |
122 | % Layer B
123 |
124 | \newcount\nd*n
125 | \def\nd*beginb{\begingroup\nd*reset\gdef\nd*stack{\nd*nil}\nd*push\nd*stack{\nd*t}%
126 | \begin{array}{l@{\hspace{\nd*labelsep}}l@{\hspace{\nd*justsep}}l}}
127 | \def\nd*resumeb{\begingroup\begin{array}{l@{\hspace{\nd*labelsep}}l@{\hspace{\nd*justsep}}l}}
128 | \def\nd*endb{\end{array}\endgroup}
129 | \def\nd*hypob#1#2{\nd*f{\nd*num{#1}}&\nd*iter\nd*stack\relax\nd*cont\nd*stack\nd*s\nd*u{#2}&}
130 | \def\nd*haveb#1#2{\nd*f{\nd*num{#1}}&\nd*iter\nd*stack\relax\nd*cont\nd*stack\nd*s\nd*f{#2}&}
131 | \def\nd*havecontb#1#2{&\nd*iter\nd*stack\relax\nd*cont\nd*stack\nd*s\nd*f{\hspace{\ndindent}#2}&}
132 | \def\nd*hypocontb#1#2{&\nd*iter\nd*stack\relax\nd*cont\nd*stack\nd*s\nd*u{\hspace{\ndindent}#2}&}
133 |
134 | \def\nd*openb{\nd*push\nd*stack{\nd*i}\nd*push\nd*stack{\nd*t}}
135 | \def\nd*closeb{\nd*pop\nd*stack\nd*pop\nd*stack}
136 | \def\nd*guardb#1#2{\nd*n=#1\multiply\nd*n by 2 \nd*modify\nd*stack\nd*n{\nd*g{#2}}}
137 |
138 | % Layer C
139 |
140 | \def\nd*clr{\gdef\nd*cmd{}\gdef\nd*typ{\relax}}
141 | \def\nd*sto#1#2#3{\gdef\nd*typ{#1}\gdef\nd*byt{}%
142 | \gdef\nd*cmd{\nd*typ{#2}{#3}\nd*byt\\}}
143 | \def\nd*chtyp{\expandafter\ifx\nd*typ\nd*hypocontb\def\nd*typ{\nd*havecontb}\else\def\nd*typ{\nd*haveb}\fi}
144 | \def\nd*hypoc#1#2{\nd*chtyp\nd*cmd\nd*sto{\nd*hypob}{#1}{#2}}
145 | \def\nd*havec#1#2{\nd*cmd\nd*sto{\nd*haveb}{#1}{#2}}
146 | \def\nd*hypocontc#1{\nd*chtyp\nd*cmd\nd*sto{\nd*hypocontb}{}{#1}}
147 | \def\nd*havecontc#1{\nd*cmd\nd*sto{\nd*havecontb}{}{#1}}
148 | \def\nd*by#1#2{\ifx\nd*x#2\nd*x\gdef\nd*byt{\mbox{#1}}\else\gdef\nd*byt{\mbox{#1, \ndref{#2}}}\fi}
149 |
150 | % multi-line macros
151 | \def\nd*mhypoc#1#2{\nd*mhypocA{#1}#2\\\nd*stop\\}
152 | \def\nd*mhypocA#1#2\\{\nd*hypoc{#1}{#2}\nd*mhypocB}
153 | \def\nd*mhypocB#1\\{\ifx\nd*stop#1\else\nd*hypocontc{#1}\expandafter\nd*mhypocB\fi}
154 | \def\nd*mhavec#1#2{\nd*mhavecA{#1}#2\\\nd*stop\\}
155 | \def\nd*mhavecA#1#2\\{\nd*havec{#1}{#2}\nd*mhavecB}
156 | \def\nd*mhavecB#1\\{\ifx\nd*stop#1\else\nd*havecontc{#1}\expandafter\nd*mhavecB\fi}
157 | \def\nd*mhypocontc#1{\nd*mhypocB#1\\\nd*stop\\}
158 | \def\nd*mhavecontc#1{\nd*mhavecB#1\\\nd*stop\\}
159 |
160 | \def\nd*beginc{\nd*beginb\nd*clr}
161 | \def\nd*resumec{\nd*resumeb\nd*clr}
162 | \def\nd*endc{\nd*cmd\nd*endb}
163 | \def\nd*openc{\nd*cmd\nd*clr\nd*openb}
164 | \def\nd*closec{\nd*cmd\nd*clr\nd*closeb}
165 | \let\nd*guardc\nd*guardb
166 |
167 | % Layer D
168 |
169 | % macros with optional arguments spelled-out
170 | \def\nd*hypod[#1][#2]#3[#4]#5{\ifx\relax#4\relax\else\nd*guardb{1}{#4}\fi\nd*mhypoc{#3}{#5}\nd*set{#1}{#2}}
171 | \def\nd*haved[#1][#2]#3[#4]#5{\ifx\relax#4\relax\else\nd*guardb{1}{#4}\fi\nd*mhavec{#3}{#5}\nd*set{#1}{#2}}
172 | \def\nd*havecont#1{\nd*mhavecontc{#1}}
173 | \def\nd*hypocont#1{\nd*mhypocontc{#1}}
174 | \def\nd*base{undefined}
175 | \def\nd*opend[#1]#2{\nd*cmd\nd*clr\nd*openb\nd*guard{#1}#2}
176 | \def\nd*close{\nd*cmd\nd*clr\nd*closeb}
177 | \def\nd*guardd[#1]#2{\nd*guardb{#1}{#2}}
178 |
179 | % Handling of optional arguments.
180 |
181 | \def\nd*optarg#1#2#3{\ifx[#3\def\nd*c{#2#3}\else\def\nd*c{#2[#1]{#3}}\fi\nd*c}
182 | \def\nd*optargg#1#2#3{\ifx[#3\def\nd*c{#1#3}\else\def\nd*c{#2{#3}}\fi\nd*c}
183 |
184 | \def\nd*five#1{\nd*optargg{\nd*four{#1}}{\nd*two{#1}}}
185 | \def\nd*four#1[#2]{\nd*optarg{0}{\nd*three{#1}[#2]}}
186 | \def\nd*three#1[#2][#3]#4{\nd*optarg{}{#1[#2][#3]{#4}}}
187 | \def\nd*two#1{\nd*three{#1}[\relax][]}
188 |
189 | \def\nd*have{\nd*five{\nd*haved}}
190 | \def\nd*hypo{\nd*five{\nd*hypod}}
191 | \def\nd*open{\nd*optarg{}{\nd*opend}}
192 | \def\nd*guard{\nd*optarg{1}{\nd*guardd}}
193 |
194 | \def\nd*init{%
195 | \let\open\nd*open%
196 | \let\close\nd*close%
197 | \let\hypo\nd*hypo%
198 | \let\have\nd*have%
199 | \let\hypocont\nd*hypocont%
200 | \let\havecont\nd*havecont%
201 | \let\by\nd*by%
202 | \let\guard\nd*guard%
203 | \def\ii{\by{$\Rightarrow$I}}%
204 | \def\ie{\by{$\Rightarrow$E}}%
205 | \def\Ai{\by{$\forall$I}}%
206 | \def\Ae{\by{$\forall$E}}%
207 | \def\Ei{\by{$\exists$I}}%
208 | \def\Ee{\by{$\exists$E}}%
209 | \def\ai{\by{$\wedge$I}}%
210 | \def\ae{\by{$\wedge$E}}%
211 | \def\ai{\by{$\wedge$I}}%
212 | \def\ae{\by{$\wedge$E}}%
213 | \def\oi{\by{$\vee$I}}%
214 | \def\oe{\by{$\vee$E}}%
215 | \def\ni{\by{$\neg$I}}%
216 | \def\ne{\by{$\neg$E}}%
217 | \def\be{\by{$\bot$E}}%
218 | \def\nne{\by{$\neg\neg$E}}%
219 | \def\r{\by{R}}%
220 | }
221 |
222 | \newenvironment{nd}{\begingroup\nd*init\nd*beginc}{\nd*endc\endgroup}
223 | \newenvironment{ndresume}{\begingroup\nd*init\nd*resumec}{\nd*endc\endgroup}
224 |
225 | \catcode`\*=\nd*astcode
226 |
227 | % End of file fitch.sty
228 |
229 |
--------------------------------------------------------------------------------
/example-pandoc/website_defaults.yaml:
--------------------------------------------------------------------------------
1 | # Pandoc defaults for generating docs example output
2 |
3 | # Needed to set imagify/output-folder to _site
4 | # in website_medata.yaml
5 |
6 | verbosity: ERROR
7 | input-files:
8 | - ${.}/example.md
9 | standalone: true
10 | filters:
11 | - {type: lua, path: imagify.lua}
12 | # Metadata must be provided in a separate file to be parsed
13 | # as Markdown
14 | metadata-file: ${.}/website_meta.yaml
15 | # Resource path needed to find `.tex`/`.tikz` figures in this subfolder
16 | resource-path:
17 | - ${.}
18 |
--------------------------------------------------------------------------------
/example-pandoc/website_meta.yaml:
--------------------------------------------------------------------------------
1 | imagify:
2 | scope: all
3 | embed: false
4 | lazy: false
5 | output-folder: _site/_imagify_files/
6 | pdf-engine: latex
7 | zoom: 1.5
8 | imagify-classes:
9 | highlightme:
10 | zoom: 4 # will show if it's not overriden
11 | block-style: "border: 1px solid red;"
12 | debug: false
13 | fitch:
14 | header-includes: \usepackage{fitch}
15 | debug: false
16 |
--------------------------------------------------------------------------------
/example-quarto/example.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Imagify Example"
3 | filters:
4 | - ../imagify.lua
5 | imagify:
6 | scope: all
7 | embed: false
8 | lazy: true
9 | output-folder: _imagify_files
10 | pdf-engine: latex
11 | zoom: 1.5
12 | imagify-classes:
13 | highlightme:
14 | zoom: 4 # will show if it's not overriden
15 | block-style: "border: 1px solid red;"
16 | debug: false
17 | fitch:
18 | header-includes: \usepackage{fitch}
19 | debug: false
20 |
21 | format:
22 | html:
23 | toc: true
24 | pdf:
25 | include-in-header: |
26 | \usepackage{tikz}
27 | \usepackage{example-pandoc/fitch}
28 |
29 | ---
30 |
31 | Imagify the following span: [the formula $E = mc^2$]{.imagify}.
32 |
33 |
34 | ::: imagify
35 |
36 | For some inline formulas, such as
37 | $x=\frac{-b\pm\sqrt[]{b^2-4ac}}{2a}$, the default `baseline` vertical
38 | alignment is not ideal. You can adjust it manually, using a negative
39 | value to lower the image below the baseline:
40 | [$x=\frac{-b\pm\sqrt[]{b^2-4ac}}{2a}$]{.imagify
41 | vertical-align="-.5em"}. In this case, I've specified a `-0.5em`
42 | value, which is about half a baseline down.
43 |
44 | :::
45 |
46 | To check that the filter processes elements of arbitrary depth, we've
47 | placed the next bit within a dummy Div block.
48 |
49 | :::: arbitraryDiv
50 |
51 | The display formula below is not explicitly marked to be imagified.
52 | However, it will be imagified if the filter's `scope` option is set
53 | to `all`:
54 | $$P = \frac{T}{V}$$
55 |
56 | ::: {.highlightme zoom='1'}
57 |
58 | This next formula is imagified with options provided for elements
59 | of a custom class, `highlightme`:
60 | $$P = \frac{T}{V}$$.
61 | They display the formula as an inline instead of a block and
62 | add a red border. They also specify a large zoom (4) but we've
63 | overridden it and locally specified a zoom of 1.
64 |
65 | :::
66 |
67 | The filter automatically recognizes TikZ pictures and loads the TikZ
68 | package with the `tikz` option for the `standalone`. When `dvisvgm` is
69 | used for conversion to SVG, the required `dvisvgm` option is set too:
70 |
71 | \usetikzlibrary{intersections}
72 | \begin{tikzpicture}[scale=3,line cap=round,
73 | % Styles
74 | axes/.style=,
75 | important line/.style={very thick}]
76 |
77 | % Colors
78 | \colorlet{anglecolor}{green!50!black}
79 | \colorlet{sincolor}{red}
80 | \colorlet{tancolor}{orange!80!black}
81 | \colorlet{coscolor}{blue}
82 |
83 | % The graphic
84 | \draw[help lines,step=0.5cm] (-1.4,-1.4) grid (1.4,1.4);
85 | \draw (0,0) circle [radius=1cm];
86 | \begin{scope}[axes]
87 | \draw[->] (-1.5,0) -- (1.5,0) node[right] {$x$} coordinate(x axis);
88 | \draw[->] (0,-1.5) -- (0,1.5) node[above] {$y$} coordinate(y axis);
89 | \foreach \x/\xtext in {-1, -.5/-\frac{1}{2}, 1}
90 | \draw[xshift=\x cm] (0pt,1pt) -- (0pt,-1pt) node[below,fill=white] {$\xtext$};
91 | \foreach \y/\ytext in {-1, -.5/-\frac{1}{2}, .5/\frac{1}{2}, 1}
92 | \draw[yshift=\y cm] (1pt,0pt) -- (-1pt,0pt) node[left,fill=white] {$\ytext$};
93 | \end{scope}
94 |
95 | \filldraw[fill=green!20,draw=anglecolor] (0,0) -- (3mm,0pt) arc [start angle=0, end angle=30, radius=3mm];
96 | \draw (15:2mm) node[anglecolor] {$\alpha$};
97 | \draw[important line,sincolor] (30:1cm) -- node[left=1pt,fill=white] {$\sin \alpha$} (30:1cm |- x axis); \draw[important line,coscolor] (30:1cm |- x axis) -- node[below=2pt,fill=white] {$\cos \alpha$} (0,0);
98 |
99 | \path [name path=upward line] (1,0) -- (1,1);
100 | \path [name path=sloped line] (0,0) -- (30:1.5cm);
101 |
102 | \draw [name intersections={of=upward line and sloped line, by=t}] [very thick,orange] (1,0) -- node [right=1pt,fill=white] {$\displaystyle \tan \alpha \color{black}=\frac{{\color{red}\sin \alpha}}{\color{blue}\cos \alpha}$} (t);
103 | \draw (0,0) -- (t);
104 | \end{tikzpicture}
105 |
106 | ::::
107 |
108 | We can also use separate `.tex` and `.tikz` files as sources for images. The
109 | filter converts them to PDF (for LaTeX/PDF output) or SVG as required.
110 | That is useful to create cross-referencable figures
111 | with Pandoc-Crossref and Quarto.
112 |
113 | 
114 |
115 | 
116 |
117 | Currently, these should not contain a LaTeX preamble or `\begin{document}`.
118 | There is no difference between `.tikz` and `.tex` sources here. A TikZ
119 | picture in a `.tikz` file should still have `\begin{tikzpicture}` or `\tikz` commands.
120 |
121 | ::: {.fitch}
122 |
123 | We can also use LaTeX packages that are provided in the document's folder,
124 | here `fitch.sty` (a package not available on CTAN):
125 |
126 | $$\begin{nd}
127 | \hypo[~] {1} {A \lor B}
128 | \open
129 | \hypo[~] {2} {A}
130 | \have[~] {3} {C}
131 | \close
132 | \open
133 | \hypo[~] {4} {B}
134 | \have[~] {5} {D}
135 | \close
136 | \have[~] {6} {C \lor D}
137 | \end{nd}$$
138 |
139 | :::
140 |
--------------------------------------------------------------------------------
/example-quarto/figure1.tikz:
--------------------------------------------------------------------------------
1 | \usetikzlibrary {arrows.meta,graphs,shapes.misc}
2 | \tikz [>={Stealth[round]}, black!50, text=black, thick,
3 | every new ->/.style = {shorten >=1pt},
4 | graphs/every graph/.style = {edges=rounded corners},
5 | skip loop/.style = {to path={-- ++(0,#1) -| (\tikztotarget)}},
6 | hv path/.style = {to path={-| (\tikztotarget)}},
7 | vh path/.style = {to path={|- (\tikztotarget)}},
8 | nonterminal/.style = {
9 | rectangle, minimum size=6mm, very thick, draw=red!50!black!50, top color=white,
10 | bottom color=red!50!black!20, font=\itshape, text height=1.5ex,text depth=.25ex},
11 | terminal/.style = {
12 | rounded rectangle, minimum size=6mm, very thick, draw=black!50, top color=white,
13 | bottom color=black!20, font=\ttfamily, text height=1.5ex, text depth=.25ex},
14 | shape = coordinate
15 | ]
16 | \graph [grow right sep, branch down=7mm, simple] {
17 | / -> unsigned integer[nonterminal] -- p1 -> "." [terminal] -- p2 -> digit[terminal] -- p3 -- p4 -- p5 -> E[terminal] -- q1 ->[vh path]
18 | {[nodes={yshift=7mm}]
19 | "+"[terminal], q2, "-"[terminal]
20 | } -> [hv path]
21 | q3 -- /unsigned integer [nonterminal] -- p6 -> /;
22 | p1 ->[skip loop=5mm] p4;
23 | p3 ->[skip loop=-5mm] p2;
24 | p5 ->[skip loop=-11mm] p6;
25 |
26 | q1 -- q2 -- q3; % make these edges plain
27 | };
--------------------------------------------------------------------------------
/example-quarto/figure2.tex:
--------------------------------------------------------------------------------
1 | $\displaystyle
2 | \left|\int_a^b fg\right| \leq \left(\int_a^b
3 | f^2\right)^{1/2}\left(\int_a^b g^2\right)^{1/2}
4 | $
--------------------------------------------------------------------------------
/example-quarto/fitch.sty:
--------------------------------------------------------------------------------
1 | % Macros for Fitch-style natural deduction.
2 | % Author: Peter Selinger, University of Ottawa
3 | % Created: Jan 14, 2002
4 | % Modified: Feb 8, 2005
5 | % Version: 0.5
6 | % Copyright: (C) 2002-2005 Peter Selinger
7 | % Filename: fitch.sty
8 | % Documentation: fitchdoc.tex
9 | % URL: http://quasar.mathstat.uottawa.ca/~selinger/fitch/
10 | % new URL: https://www.mathstat.dal.ca/~selinger/fitch/
11 |
12 | % License:
13 | %
14 | % This program is free software; you can redistribute it and/or modify
15 | % it under the terms of the GNU General Public License as published by
16 | % the Free Software Foundation; either version 2, or (at your option)
17 | % any later version.
18 | %
19 | % This program is distributed in the hope that it will be useful, but
20 | % WITHOUT ANY WARRANTY; without even the implied warranty of
21 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 | % General Public License for more details.
23 | %
24 | % You should have received a copy of the GNU General Public License
25 | % along with this program; if not, write to the Free Software Foundation,
26 | % Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
27 |
28 | % USAGE EXAMPLE:
29 | %
30 | % The following is a simple example illustrating the usage of this
31 | % package. For detailed instructions and additional functionality, see
32 | % the user guide, which can be found in the file fitchdoc.tex.
33 | %
34 | % \[
35 | % \begin{nd}
36 | % \hypo{1} {P\vee Q}
37 | % \hypo{2} {\neg Q}
38 | % \open
39 | % \hypo{3a} {P}
40 | % \have{3b} {P} \r{3a}
41 | % \close
42 | % \open
43 | % \hypo{4a} {Q}
44 | % \have{4b} {\neg Q} \r{2}
45 | % \have{4c} {\bot} \ne{4a,4b}
46 | % \have{4d} {P} \be{4c}
47 | % \close
48 | % \have{5} {P} \oe{1,3a-3b,4a-4d}
49 | % \end{nd}
50 | % \]
51 |
52 | {\chardef\x=\catcode`\*
53 | \catcode`\*=11
54 | \global\let\nd*astcode\x}
55 | \catcode`\*=11
56 |
57 | % References
58 |
59 | \newcount\nd*ctr
60 | \def\nd*render{\expandafter\ifx\expandafter\nd*x\nd*base\nd*x\the\nd*ctr\else\nd*base\ifnum\nd*ctr<0\the\nd*ctr\else\ifnum\nd*ctr>0+\the\nd*ctr\fi\fi\fi}
61 | \expandafter\def\csname nd*-\endcsname{}
62 |
63 | \def\nd*num#1{\nd*numo{\nd*render}{#1}\global\advance\nd*ctr1}
64 | \def\nd*numopt#1#2{\nd*numo{$#1$}{#2}}
65 | \def\nd*numo#1#2{\edef\x{#1}\mbox{$\x$}\expandafter\global\expandafter\let\csname nd*-#2\endcsname\x}
66 | \def\nd*ref#1{\expandafter\let\expandafter\x\csname nd*-#1\endcsname\ifx\x\relax%
67 | \errmessage{Undefined natdeduction reference: #1}\else\mbox{$\x$}\fi}
68 | \def\nd*noop{}
69 | \def\nd*set#1#2{\ifx\relax#1\nd*noop\else\global\def\nd*base{#1}\fi\ifx\relax#2\relax\else\global\nd*ctr=#2\fi}
70 | \def\nd*reset{\nd*set{}{1}}
71 | \def\nd*refa#1{\nd*ref{#1}}
72 | \def\nd*aux#1#2{\ifx#2-\nd*refa{#1}--\def\nd*c{\nd*aux{}}%
73 | \else\ifx#2,\nd*refa{#1}, \def\nd*c{\nd*aux{}}%
74 | \else\ifx#2;\nd*refa{#1}; \def\nd*c{\nd*aux{}}%
75 | \else\ifx#2.\nd*refa{#1}. \def\nd*c{\nd*aux{}}%
76 | \else\ifx#2)\nd*refa{#1})\def\nd*c{\nd*aux{}}%
77 | \else\ifx#2(\nd*refa{#1}(\def\nd*c{\nd*aux{}}%
78 | \else\ifx#2\nd*end\nd*refa{#1}\def\nd*c{}%
79 | \else\def\nd*c{\nd*aux{#1#2}}%
80 | \fi\fi\fi\fi\fi\fi\fi\nd*c}
81 | \def\ndref#1{\nd*aux{}#1\nd*end}
82 |
83 | % Layer A
84 |
85 | % define various dimensions (explained in fitchdoc.tex):
86 | \newlength{\nd*dim}
87 | \newdimen\nd*depthdim
88 | \newdimen\nd*hsep
89 | \newdimen\ndindent
90 | \ndindent=1em
91 | % user command to redefine dimensions
92 | \def\nddim#1#2#3#4#5#6#7#8{\nd*depthdim=#3\relax\nd*hsep=#6\relax%
93 | \def\nd*height{#1}\def\nd*thickness{#8}\def\nd*initheight{#2}%
94 | \def\nd*indent{#5}\def\nd*labelsep{#4}\def\nd*justsep{#7}}
95 | % set initial dimensions
96 | \nddim{4.5ex}{3.5ex}{1.5ex}{1em}{1.6em}{.5em}{2.5em}{.2mm}
97 |
98 | \def\nd*v{\rule[-\nd*depthdim]{\nd*thickness}{\nd*height}}
99 | \def\nd*t{\rule[-\nd*depthdim]{0mm}{\nd*height}\rule[-\nd*depthdim]{\nd*thickness}{\nd*initheight}}
100 | \def\nd*i{\hspace{\nd*indent}}
101 | \def\nd*s{\hspace{\nd*hsep}}
102 | \def\nd*g#1{\nd*f{\makebox[\nd*indent][c]{$#1$}}}
103 | \def\nd*f#1{\raisebox{0pt}[0pt][0pt]{$#1$}}
104 | \def\nd*u#1{\makebox[0pt][l]{\settowidth{\nd*dim}{\nd*f{#1}}%
105 | \addtolength{\nd*dim}{2\nd*hsep}\hspace{-\nd*hsep}\rule[-\nd*depthdim]{\nd*dim}{\nd*thickness}}\nd*f{#1}}
106 |
107 | % Lists
108 |
109 | \def\nd*push#1#2{\expandafter\gdef\expandafter#1\expandafter%
110 | {\expandafter\nd*cons\expandafter{#1}{#2}}}
111 | \def\nd*pop#1{{\def\nd*nil{\gdef#1{\nd*nil}}\def\nd*cons##1##2%
112 | {\gdef#1{##1}}#1}}
113 | \def\nd*iter#1#2{{\def\nd*nil{}\def\nd*cons##1##2{##1#2{##2}}#1}}
114 | \def\nd*modify#1#2#3{{\def\nd*nil{\gdef#1{\nd*nil}}\def\nd*cons##1##2%
115 | {\advance#2-1 ##1\advance#2 1 \ifnum#2=1\nd*push#1{#3}\else%
116 | \nd*push#1{##2}\fi}#1}}
117 |
118 | \def\nd*cont#1{{\def\nd*t{\nd*v}\def\nd*v{\nd*v}\def\nd*g##1{\nd*i}%
119 | \def\nd*i{\nd*i}\def\nd*nil{\gdef#1{\nd*nil}}\def\nd*cons##1##2%
120 | {##1\expandafter\nd*push\expandafter#1\expandafter{##2}}#1}}
121 |
122 | % Layer B
123 |
124 | \newcount\nd*n
125 | \def\nd*beginb{\begingroup\nd*reset\gdef\nd*stack{\nd*nil}\nd*push\nd*stack{\nd*t}%
126 | \begin{array}{l@{\hspace{\nd*labelsep}}l@{\hspace{\nd*justsep}}l}}
127 | \def\nd*resumeb{\begingroup\begin{array}{l@{\hspace{\nd*labelsep}}l@{\hspace{\nd*justsep}}l}}
128 | \def\nd*endb{\end{array}\endgroup}
129 | \def\nd*hypob#1#2{\nd*f{\nd*num{#1}}&\nd*iter\nd*stack\relax\nd*cont\nd*stack\nd*s\nd*u{#2}&}
130 | \def\nd*haveb#1#2{\nd*f{\nd*num{#1}}&\nd*iter\nd*stack\relax\nd*cont\nd*stack\nd*s\nd*f{#2}&}
131 | \def\nd*havecontb#1#2{&\nd*iter\nd*stack\relax\nd*cont\nd*stack\nd*s\nd*f{\hspace{\ndindent}#2}&}
132 | \def\nd*hypocontb#1#2{&\nd*iter\nd*stack\relax\nd*cont\nd*stack\nd*s\nd*u{\hspace{\ndindent}#2}&}
133 |
134 | \def\nd*openb{\nd*push\nd*stack{\nd*i}\nd*push\nd*stack{\nd*t}}
135 | \def\nd*closeb{\nd*pop\nd*stack\nd*pop\nd*stack}
136 | \def\nd*guardb#1#2{\nd*n=#1\multiply\nd*n by 2 \nd*modify\nd*stack\nd*n{\nd*g{#2}}}
137 |
138 | % Layer C
139 |
140 | \def\nd*clr{\gdef\nd*cmd{}\gdef\nd*typ{\relax}}
141 | \def\nd*sto#1#2#3{\gdef\nd*typ{#1}\gdef\nd*byt{}%
142 | \gdef\nd*cmd{\nd*typ{#2}{#3}\nd*byt\\}}
143 | \def\nd*chtyp{\expandafter\ifx\nd*typ\nd*hypocontb\def\nd*typ{\nd*havecontb}\else\def\nd*typ{\nd*haveb}\fi}
144 | \def\nd*hypoc#1#2{\nd*chtyp\nd*cmd\nd*sto{\nd*hypob}{#1}{#2}}
145 | \def\nd*havec#1#2{\nd*cmd\nd*sto{\nd*haveb}{#1}{#2}}
146 | \def\nd*hypocontc#1{\nd*chtyp\nd*cmd\nd*sto{\nd*hypocontb}{}{#1}}
147 | \def\nd*havecontc#1{\nd*cmd\nd*sto{\nd*havecontb}{}{#1}}
148 | \def\nd*by#1#2{\ifx\nd*x#2\nd*x\gdef\nd*byt{\mbox{#1}}\else\gdef\nd*byt{\mbox{#1, \ndref{#2}}}\fi}
149 |
150 | % multi-line macros
151 | \def\nd*mhypoc#1#2{\nd*mhypocA{#1}#2\\\nd*stop\\}
152 | \def\nd*mhypocA#1#2\\{\nd*hypoc{#1}{#2}\nd*mhypocB}
153 | \def\nd*mhypocB#1\\{\ifx\nd*stop#1\else\nd*hypocontc{#1}\expandafter\nd*mhypocB\fi}
154 | \def\nd*mhavec#1#2{\nd*mhavecA{#1}#2\\\nd*stop\\}
155 | \def\nd*mhavecA#1#2\\{\nd*havec{#1}{#2}\nd*mhavecB}
156 | \def\nd*mhavecB#1\\{\ifx\nd*stop#1\else\nd*havecontc{#1}\expandafter\nd*mhavecB\fi}
157 | \def\nd*mhypocontc#1{\nd*mhypocB#1\\\nd*stop\\}
158 | \def\nd*mhavecontc#1{\nd*mhavecB#1\\\nd*stop\\}
159 |
160 | \def\nd*beginc{\nd*beginb\nd*clr}
161 | \def\nd*resumec{\nd*resumeb\nd*clr}
162 | \def\nd*endc{\nd*cmd\nd*endb}
163 | \def\nd*openc{\nd*cmd\nd*clr\nd*openb}
164 | \def\nd*closec{\nd*cmd\nd*clr\nd*closeb}
165 | \let\nd*guardc\nd*guardb
166 |
167 | % Layer D
168 |
169 | % macros with optional arguments spelled-out
170 | \def\nd*hypod[#1][#2]#3[#4]#5{\ifx\relax#4\relax\else\nd*guardb{1}{#4}\fi\nd*mhypoc{#3}{#5}\nd*set{#1}{#2}}
171 | \def\nd*haved[#1][#2]#3[#4]#5{\ifx\relax#4\relax\else\nd*guardb{1}{#4}\fi\nd*mhavec{#3}{#5}\nd*set{#1}{#2}}
172 | \def\nd*havecont#1{\nd*mhavecontc{#1}}
173 | \def\nd*hypocont#1{\nd*mhypocontc{#1}}
174 | \def\nd*base{undefined}
175 | \def\nd*opend[#1]#2{\nd*cmd\nd*clr\nd*openb\nd*guard{#1}#2}
176 | \def\nd*close{\nd*cmd\nd*clr\nd*closeb}
177 | \def\nd*guardd[#1]#2{\nd*guardb{#1}{#2}}
178 |
179 | % Handling of optional arguments.
180 |
181 | \def\nd*optarg#1#2#3{\ifx[#3\def\nd*c{#2#3}\else\def\nd*c{#2[#1]{#3}}\fi\nd*c}
182 | \def\nd*optargg#1#2#3{\ifx[#3\def\nd*c{#1#3}\else\def\nd*c{#2{#3}}\fi\nd*c}
183 |
184 | \def\nd*five#1{\nd*optargg{\nd*four{#1}}{\nd*two{#1}}}
185 | \def\nd*four#1[#2]{\nd*optarg{0}{\nd*three{#1}[#2]}}
186 | \def\nd*three#1[#2][#3]#4{\nd*optarg{}{#1[#2][#3]{#4}}}
187 | \def\nd*two#1{\nd*three{#1}[\relax][]}
188 |
189 | \def\nd*have{\nd*five{\nd*haved}}
190 | \def\nd*hypo{\nd*five{\nd*hypod}}
191 | \def\nd*open{\nd*optarg{}{\nd*opend}}
192 | \def\nd*guard{\nd*optarg{1}{\nd*guardd}}
193 |
194 | \def\nd*init{%
195 | \let\open\nd*open%
196 | \let\close\nd*close%
197 | \let\hypo\nd*hypo%
198 | \let\have\nd*have%
199 | \let\hypocont\nd*hypocont%
200 | \let\havecont\nd*havecont%
201 | \let\by\nd*by%
202 | \let\guard\nd*guard%
203 | \def\ii{\by{$\Rightarrow$I}}%
204 | \def\ie{\by{$\Rightarrow$E}}%
205 | \def\Ai{\by{$\forall$I}}%
206 | \def\Ae{\by{$\forall$E}}%
207 | \def\Ei{\by{$\exists$I}}%
208 | \def\Ee{\by{$\exists$E}}%
209 | \def\ai{\by{$\wedge$I}}%
210 | \def\ae{\by{$\wedge$E}}%
211 | \def\ai{\by{$\wedge$I}}%
212 | \def\ae{\by{$\wedge$E}}%
213 | \def\oi{\by{$\vee$I}}%
214 | \def\oe{\by{$\vee$E}}%
215 | \def\ni{\by{$\neg$I}}%
216 | \def\ne{\by{$\neg$E}}%
217 | \def\be{\by{$\bot$E}}%
218 | \def\nne{\by{$\neg\neg$E}}%
219 | \def\r{\by{R}}%
220 | }
221 |
222 | \newenvironment{nd}{\begingroup\nd*init\nd*beginc}{\nd*endc\endgroup}
223 | \newenvironment{ndresume}{\begingroup\nd*init\nd*resumec}{\nd*endc\endgroup}
224 |
225 | \catcode`\*=\nd*astcode
226 |
227 | % End of file fitch.sty
228 |
229 |
--------------------------------------------------------------------------------
/imagify.lua:
--------------------------------------------------------------------------------
1 | _extensions/imagify/imagify.lua
--------------------------------------------------------------------------------
/src/common.lua:
--------------------------------------------------------------------------------
1 | ---message: send message to std_error
2 | ---comment
3 | ---@param type 'INFO'|'WARNING'|'ERROR'
4 | ---@param text string error message
5 | function message (type, text)
6 | local level = {INFO = 0, WARNING = 1, ERROR = 2}
7 | if level[type] == nil then type = 'ERROR' end
8 | if level[PANDOC_STATE.verbosity] <= level[type] then
9 | io.stderr:write('[' .. type .. '] Imagify: '
10 | .. text .. '\n')
11 | end
12 | end
13 |
14 | ---tfind: finds a value in an array
15 | ---comment
16 | ---@param tbl table
17 | ---@return number|false result
18 | function tfind(tbl, needle)
19 | local i = 0
20 | for _,v in ipairs(tbl) do
21 | i = i + 1
22 | if v == needle then
23 | return i
24 | end
25 | end
26 | return false
27 | end
28 |
29 | ---concatStrings: concatenate a list of strings into one.
30 | ---@param list string[] list of strings
31 | ---@param separator string separator (optional)
32 | ---@return string result
33 | function concatStrings(list, separator)
34 | separator = separator and separator or ''
35 | local result = ''
36 | for _,str in ipairs(list) do
37 | result = result..separator..str
38 | end
39 | return result
40 | end
41 |
42 | ---mergeMapInto: returns a new map resulting from merging a new one
43 | -- into an old one.
44 | ---@param new table|nil map with overriding values
45 | ---@param old table|nil map with original values
46 | ---@return table result new map with merged values
47 | function mergeMapInto(new,old)
48 | local result = {} -- we need to clone
49 | if type(old) == 'table' then
50 | for k,v in pairs(old) do result[k] = v end
51 | end
52 | if type(new) == 'table' then
53 | for k,v in pairs(new) do result[k] = v end
54 | end
55 | return result
56 | end
57 |
--------------------------------------------------------------------------------
/src/file.lua:
--------------------------------------------------------------------------------
1 | -- ## File functions
2 |
3 | local system = pandoc.system
4 | local path = pandoc.path
5 |
6 | ---fileExists: checks whether a file exists
7 | function fileExists(filepath)
8 | local f = io.open(filepath, 'r')
9 | if f ~= nil then
10 | io.close(f)
11 | return true
12 | else
13 | return false
14 | end
15 | end
16 |
17 | ---makeAbsolute: make filepath absolute
18 | ---@param filepath string file path
19 | ---@param root string|nil if relative, use this as root (default working dir)
20 | function makeAbsolute(filepath, root)
21 | root = root or system.get_working_directory()
22 | return path.is_absolute(filepath) and filepath
23 | or path.join{ root, filepath}
24 | end
25 |
26 | ---folderExists: checks whether a folder exists
27 | function folderExists(folderpath)
28 |
29 | -- the empty path always exists
30 | if folderpath == '' then return true end
31 |
32 | -- normalize folderpath
33 | folderpath = folderpath:gsub('[/\\]$','')..path.separator
34 | local ok, err, code = os.rename(folderpath, folderpath)
35 | -- err = 13 permission denied
36 | return ok or err == 13 or false
37 | end
38 |
39 | ---ensureFolderExists: create a folder if needed
40 | function ensureFolderExists(folderpath)
41 | local ok, err, code = true, nil, nil
42 |
43 | -- the empty path always exists
44 | if folderpath == '' then return true, nil, nil end
45 |
46 | -- normalize folderpath
47 | folderpath = folderpath:gsub('[/\\]$','')
48 |
49 | if not folderExists(folderpath) then
50 | ok, err, code = os.execute('mkdir '..folderpath)
51 | end
52 |
53 | return ok, err, code
54 | end
55 |
56 | ---writeToFile: write string to file.
57 | ---@param contents string file contents
58 | ---@param filepath string file path
59 | ---@param mode string 'b' for binary, any other value text mode
60 | ---@return nil | string status error message
61 | function writeToFile(contents, filepath, mode)
62 | local mode = mode == 'b' and 'wb' or 'w'
63 | local f = io.open(filepath, mode)
64 | if f then
65 | f:write(contents)
66 | f:close()
67 | else
68 | return 'File not found'
69 | end
70 | end
71 |
72 | ---readFile: read file as string (default) or binary.
73 | ---@param filepath string file path
74 | ---@param mode string 'b' for binary, any other value text mode
75 | ---@return string contents or empty string if failure
76 | function readFile(filepath, mode)
77 | local mode = mode == 'b' and 'rb' or 'r'
78 | local contents
79 | local f = io.open(filepath, mode)
80 | if f then
81 | contents = f:read('a')
82 | f:close()
83 | end
84 | return contents or ''
85 | end
86 |
87 | ---copyFile: copy file from source to destination
88 | ---Lua's os.rename doesn't work across volumes. This is a
89 | ---problem when Pandoc is run within a docker container:
90 | ---the temp files are in the container, the output typically
91 | ---in a shared volume mounted separately.
92 | ---We use copyFile to avoid this issue.
93 | ---@param source string file path
94 | ---@param destination string file path
95 | function copyFile(source, destination, mode)
96 | local mode = mode == 'b' and 'b' or ''
97 | writeToFile(readFile(source, mode), destination, mode)
98 | end
99 |
100 | -- stripExtension: strip filepath of the filename's extension
101 | ---@param filepath string file path
102 | ---@param extensions string[] list of extensions, e.g. {'tex', 'latex'}
103 | --- if not provided, any alphanumeric extension is stripped
104 | ---@return string filepath revised filepath
105 | function stripExtension(filepath, extensions)
106 | local name, ext = path.split_extension(filepath)
107 | ext = ext:match('^%.(.*)')
108 |
109 | if extensions then
110 | extensions = pandoc.List(extensions)
111 | return extensions:find(ext) and name
112 | or filepath
113 | else
114 | return name
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/src/main.lua:
--------------------------------------------------------------------------------
1 | --[[-- # Imagify - Pandoc / Quarto filter to convert selected
2 | LaTeX elements into images.
3 |
4 | @author Julien Dutant
5 | @copyright 2021-2023 Philosophie.ch
6 | @license MIT - see LICENSE file for details.
7 | @release 0.3.0
8 |
9 | Converts some or all LaTeX code in a document into
10 | images.
11 |
12 | @todo reader user templates from metadata
13 |
14 | @note Rendering options are provided in the doc's metadata (global),
15 | as Div / Span attribute (regional), on a RawBlock/Inline (local).
16 | They need to be kept track of, then merged before imagifying.
17 | The more local ones override the global ones.
18 | @note LaTeX Raw elements may be tagged as `tex` or `latex`. LaTeX code
19 | directly inserted in markdown (without $...$ or ```....``` wrappers)
20 | is parsed by Pandoc as Raw element with tag `tex` or `latex`.
21 | ]]
22 |
23 | PANDOC_VERSION:must_be_at_least(
24 | '2.19.0',
25 | 'The Imagify filter requires Pandoc version >= 2.19'
26 | )
27 |
28 | -- # Modules
29 |
30 | require 'common'
31 | require 'file'
32 |
33 | -- # Global variables
34 |
35 | local stringify = pandoc.utils.stringify
36 | local pandoctype = pandoc.utils.type
37 | local system = pandoc.system
38 | local path = pandoc.path
39 |
40 | --- renderOptions type
41 | --- Contains the fields below plus a number of Pandoc metadata
42 | ---keys like header-includes, fontenc, colorlinks etc.
43 | ---See getRenderOptions() for details.
44 | ---@alias ro_force boolean imagify even when targeting LaTeX
45 | ---@alias ro_embed boolean whether to embed (if possible) or output as file
46 | ---@alias ro_debug boolean debug mode (keep .tex source, crash on fail)
47 | ---@alias ro_template string identifier of a Pandoc template (default 'default')
48 | ---@alias ro_pdf_engine 'latex'|'pdflatex'|'xelatex'|'lualatex' latex engine to be used
49 | ---@alias ro_svg_converter 'dvisvgm' pdf/dvi to svg converter (default 'dvisvgm')
50 | ---@alias ro_zoom string to apply when converting pdf/dvi to svg
51 | ---@alias ro_vertical_align string vertical align value (HTML output)
52 | ---@alias ro_block_style string style to apply to blockish elements (DisplayMath, RawBlock)
53 | ---@alias renderOptsType {force: ro_force, embed: ro_embed, debug: ro_debug, template: ro_template, pdf_engine: ro_pdf_engine, svg_converter: ro_svg_converter, zoom: ro_zoom, vertical_align: ro_vertical_align, block_style: ro_block_style, }
54 | ---@type renderOptsType
55 | local globalRenderOptions = {
56 | force = false,
57 | embed = true,
58 | debug = false,
59 | template = 'default',
60 | pdf_engine = 'latex',
61 | svg_converter = 'dvisvgm',
62 | zoom = '1.5',
63 | vertical_align = 'baseline',
64 | block_style = 'display:block; margin: .5em auto;'
65 | }
66 |
67 | ---@alias fo_scope 'manual'|'all'|'images'|'none', # imagify scope
68 | ---@alias fo_lazy boolean, # do not regenerate existing image files
69 | ---@alias fo_no_html_embed boolean, # prohibit html embedding
70 | ---@alias fo_output_folder string, # path for outputs
71 | ---@alias fo_output_folder_exists boolean, # Internal var to avoid repeat checks
72 | ---@alias fo_libgs_path string|nil, # path to Ghostscript lib
73 | ---@alias fo_optionsForClass { string: renderOptsType}, # renderOptions for imagify classes
74 | ---@alias fo_extensionForOutput { default: string, string: string }, # map of image formats (svg|pdf) for some output formats
75 | ---@alias filterOptsType { scope : fo_scope, lazy: fo_lazy, no_html_embed : fo_no_html_embed, output_folder: fo_output_folder, output_folder_exists: fo_output_folder_exists, libgs_path: fo_libgs_path, optionsForClass: fo_optionsForClass, extensionForOutput: fo_extensionForOutput }
76 | ---@type filterOptsType
77 | local filterOptions = {
78 | scope = 'manual',
79 | lazy = true,
80 | no_html_embed = false,
81 | libgs_path = nil,
82 | output_folder = '_imagify',
83 | output_folder_exists = false,
84 | optionsForClass = {},
85 | extensionForOutput = {
86 | default = 'svg',
87 | html = 'svg',
88 | html4 = 'svg',
89 | html5 = 'svg',
90 | latex = 'pdf',
91 | beamer = 'pdf',
92 | docx = 'pdf',
93 | }
94 | }
95 |
96 | ---@alias tplId string template identifier, 'default' reserved for Pandoc's default template
97 | ---@alias to_source string template source code
98 | ---@alias to_template pandoc.Template compiled template
99 | ---@alias templateOptsType { default: table, string: { source: to_source, compiled: to_template}}
100 | ---@type templateOptsType
101 | local Templates = {
102 | default = {},
103 | }
104 |
105 | -- ## Pandoc AST functions
106 |
107 | --outputIsLaTeX: checks whether the target output is in LaTeX
108 | ---@return boolean
109 | local function outputIsLaTeX()
110 | return FORMAT:match('latex') or FORMAT:match('beamer') or false
111 | end
112 |
113 | --- ensureList: ensures an object is a pandoc.List.
114 | ---@param obj any|nil
115 | local function ensureList(obj)
116 |
117 | return pandoctype(obj) == 'List' and obj
118 | or pandoc.List:new{obj}
119 |
120 | end
121 |
122 | ---imagifyType: whether an element is imagifiable LaTeX and which type
123 | ---@alias imagifyType nil|'InlineMath'|'DisplayMath'|'RawBlock'|'RawInline'|'TexImage'|'TikzImage'
124 | ---@param elem pandoc.Math|pandoc.RawBlock|pandoc.RawInline|pandoc.Image element
125 | ---@return imagifyType elemType to imagify or nil
126 | function imagifyType(elem)
127 | return elem.t == 'Image' and (
128 | elem.src:match('%.tex$') and 'TexImage'
129 | or elem.src:match('%.tikz') and 'TikzImage'
130 | )
131 | or elem.mathtype == 'InlineMath' and 'InlineMath'
132 | or elem.mathtype == 'DisplayMath' and 'DisplayMath'
133 | or (elem.format == 'tex' or elem.format == 'latex')
134 | and (
135 | elem.t == 'RawBlock' and 'RawBlock'
136 | or elem.t == 'RawInline' and 'RawInline'
137 | )
138 | or nil
139 | end
140 |
141 | -- ## Smart imagifying functions
142 |
143 | ---usesTikZ: tell whether a source contains a TikZ picture
144 | ---@param source string LaTeX source
145 | ---@return boolean result
146 | local function usesTikZ(source)
147 | return (source:match('\\begin{tikzpicture}')
148 | or source:match('\\tikz')) and true
149 | or false
150 | end
151 |
152 | -- ## Converter functions
153 |
154 | local function dvisvgmVerbosity()
155 | return PANDOC_STATE.verbosity == 'ERROR' and '1'
156 | or PANDOC_STATE.verbosity == 'WARNING' and '2'
157 | or PANDOC_STATE.verbosity == 'INFO' and '4'
158 | or '2'
159 | end
160 |
161 | ---getCodeFromFile: get source code from a file
162 | ---uses Pandoc's resource paths if needed
163 | ---@param src string source file name/path
164 | ---@return string|nil result file contents or nil if not found
165 | function getCodeFromFile(src)
166 | local result
167 |
168 | if fileExists(src) then
169 | result = readFile(src)
170 | else
171 | for _,item in ipairs(PANDOC_STATE.resource_path) do
172 | if fileExists(path.join{item, src}) then
173 | result = readFile(path.join{item, src})
174 | break
175 | end
176 | end
177 | end
178 |
179 | return result
180 |
181 | end
182 |
183 | ---runLaTeX: runs latex engine on file
184 | ---@param source string filepath of the source file
185 | ---@param options table options
186 | -- format = output format, 'dvi' or 'pdf',
187 | -- pdf_engine = pdf engine, 'latex', 'xelatex', 'xetex', '' etc.
188 | -- texinputs = value for export TEXINPUTS
189 | ---@return boolean success, string result result is filepath or LaTeX log if failed
190 | local function runLaTeX(source, options)
191 | options = options or {}
192 | local format = options.format or 'pdf'
193 | local pdf_engine = options.pdf_engine or 'latex'
194 | local outfile = stripExtension(source, {'tex','latex'})
195 | local ext = pdf_engine == 'xelatex' and format == 'dvi' and '.xdv'
196 | or '.'..format
197 | local texinputs = options.texinputs or nil
198 | -- Latexmk: extra options come *after* - and *before*
199 | local latex_args = pandoc.List:new{ '--interaction=nonstopmode' }
200 | local latexmk_args = pandoc.List:new{ '-'..pdf_engine }
201 | -- Export the TEXINPUTS variable
202 | local env = texinputs and 'export TEXINPUTS='..texinputs..'; '
203 | or ''
204 | -- latex command run, for debug purposes
205 | local cmd
206 |
207 | -- @TODO implement verbosity in latex
208 | -- latexmk silent mode
209 | if PANDOC_STATE.verbosity == 'ERROR' then
210 | latexmk_args:insert('-silent')
211 | end
212 |
213 | -- xelatex doesn't accept `output-format`,
214 | -- generates both .pdf and .xdv
215 | if pdf_engine ~= 'xelatex' then
216 | latex_args:insert('--output-format='..format)
217 | end
218 |
219 |
220 | -- try Latexmk first, latex engine second
221 | -- two runs of latex engine
222 | cmd = env..'latexmk '..concatStrings(latexmk_args..latex_args, ' ')
223 | ..' '..source
224 | local success, err, code = os.execute(cmd)
225 |
226 | if not success and code == 127 then
227 | cmd = pdf_engine..' '
228 | ..concatStrings(latex_args, ' ')
229 | ..' '..source..' 2>&1 > /dev/null '..'; '
230 | cmd = cmd..cmd -- two runs needed
231 | success = os.execute(env..cmd)
232 | end
233 |
234 | if success then
235 |
236 | return true, outfile..ext
237 |
238 | else
239 |
240 | local result = 'LaTeX compilation failed.\n'
241 | ..'Command used: '..cmd..'\n'
242 | local src_code = readFile(source)
243 | if src_code then
244 | result = result..'LaTeX source code:\n'
245 | result = result..src_code
246 | end
247 | local log = readFile(outfile..'.log')
248 | if log then
249 | result = result..'LaTeX log:\n'..log
250 | end
251 | return false, result
252 |
253 | end
254 |
255 | end
256 |
257 | ---toSVG: convert latex output to SVG.
258 | ---Ghostcript library required to convert PDF files.
259 | -- See divsvgm manual for more details.
260 | -- Options:
261 | -- *output*: string output filepath (directory must exist),
262 | -- *zoom*: string zoom factor, e.g. 1.5.
263 | ---@param source string filepath of dvi, xdv or svg file
264 | ---@param options { output : string, zoom: string} options
265 | ---@return success boolean, result string filepath
266 | local function toSVG(source, options)
267 | if source == nil then return nil end
268 | local options = options or {}
269 | local outfile = options.output
270 | or stripExtension(source, {'pdf', 'svg', 'xdv'})..'.svg'
271 | local source_format = source:match('%.pdf$') and 'pdf'
272 | or source:match('%.dvi$') and 'dvi'
273 | or source:match('%.xdv$') and 'dvi'
274 | local cmd_opts = pandoc.List:new({'--optimize',
275 | '--verbosity='..dvisvgmVerbosity(),
276 | -- '--relative',
277 | -- '--no-fonts',
278 | '--font-format=WOFF',
279 | source
280 | })
281 |
282 | -- @TODO doesn't work on my machine, why?
283 | if filterOptions.libgs_path and filterOptions.libgs_path ~= '' then
284 | cmd_opts:insert('--libgs='..filterOptions.libgs_path)
285 | end
286 |
287 | -- note "Ghostcript required to process PDF files"
288 | if source_format == 'pdf' then
289 | cmd_opts:insert('--pdf')
290 | end
291 |
292 | if options.zoom then
293 | cmd_opts:insert('--zoom='..options.zoom)
294 | end
295 |
296 | cmd_opts:insert('--output='..outfile)
297 |
298 | success = os.execute('dvisvgm'
299 | ..' '..concatStrings(cmd_opts, ' ')
300 | )
301 |
302 | if success then
303 |
304 | return success, outfile
305 |
306 | else
307 |
308 | return success, 'DVI/PDF to SVG conversion failed\n'
309 |
310 | end
311 |
312 | end
313 |
314 | --- getSVGFromFile: extract svg tag (with contents) from a SVG file.
315 | -- Assumes the file only contains one SVG tag.
316 | -- @param filepath string file path
317 | local function getSVGFromFile(filepath)
318 | local contents = readFile(filepath)
319 |
320 | return contents and contents:match('')
321 |
322 | end
323 |
324 |
325 | --- urlEncode: URL-encodes a string
326 | -- See
327 | -- Modified to handle UTF-8: %w matches UTF-8 starting bytes, which should
328 | -- be encoded. We specify safe alphanumeric chars explicitly instead.
329 | -- @param str string
330 | local function urlEncode(str)
331 |
332 | --Ensure all newlines are in CRLF form
333 | str = string.gsub (str, "\r?\n", "\r\n")
334 |
335 | --Percent-encode all chars other than unreserved
336 | --as per RFC 3986, Section 2.3
337 | --
338 | str = str:gsub("[^0-9a-zA-Z%-._~]",
339 | function (c) return string.format ("%%%02X", string.byte(c)) end)
340 |
341 | return str
342 |
343 | end
344 |
345 | -- # Main filter functions
346 |
347 | -- ## Functions to read options
348 |
349 | ---getFilterOptions: read render options
350 | ---returns a map:
351 | --- scope: fo_scope
352 | --- libgs_path: string
353 | --- output_folder: string
354 | ---@param opts table options map from meta.imagify
355 | ---@return table result map of options
356 | local function getFilterOptions(opts)
357 | local stringKeys = {'scope', 'libgs-path', 'output-folder'}
358 | local boolKeys = {'lazy'}
359 | local result = {}
360 |
361 | for _,key in ipairs(boolKeys) do
362 | if opts[key] ~= nil and pandoctype(opts[key]) == 'boolean' then
363 | result[key] = opts[key]
364 | end
365 | end
366 |
367 | for _,key in ipairs(stringKeys) do
368 | opts[key] = opts[key] and stringify(opts[key]) or nil
369 | end
370 |
371 | result.scope = opts.scope and (
372 | opts.scope == 'all' and 'all'
373 | or (opts.scope == 'selected' or opts.scope == 'manual') and 'manual'
374 | or opts.scope == 'images' and 'images'
375 | or opts.scope == 'none' and 'none'
376 | ) or nil
377 |
378 | result.libgs_path = opts['libgs-path'] and opts['libgs-path'] or nil
379 |
380 | result.output_folder = opts['output-folder']
381 | and opts['output-folder'] or nil
382 |
383 | return result
384 |
385 | end
386 |
387 | ---getRenderOptions: read render options
388 | ---@param opts table options map, from doc metadata or elem attributes
389 | ---@return table result renderOptions map of options
390 | local function getRenderOptions(opts)
391 | local result = {}
392 | local renderBooleanlKeys = {
393 | 'force',
394 | 'embed',
395 | 'debug',
396 | }
397 | local renderStringKeys = {
398 | 'pdf-engine',
399 | 'svg-converter',
400 | 'zoom',
401 | 'vertical-align',
402 | 'block-style',
403 | }
404 | local renderListKeys = {
405 | 'classoption',
406 | }
407 | -- Pandoc metadata variables used by the LaTeX template
408 | local renderMetaKeys = {
409 | 'header-includes',
410 | 'mathspec',
411 | 'fontenc',
412 | 'fontfamily',
413 | 'fontfamilyoptions',
414 | 'fontsize',
415 | 'mainfont', 'sansfont', 'monofont', 'mathfont', 'CJKmainfont',
416 | 'mainfontoptions', 'sansfontoptions', 'monofontoptions',
417 | 'mathfontoptions', 'CJKoptions',
418 | 'microtypeoptions',
419 | 'colorlinks',
420 | 'boxlinks',
421 | 'linkcolor', 'filecolor', 'citecolor', 'urlcolor', 'toccolor',
422 | -- 'links-as-note': not visible in standalone LaTeX class
423 | 'urlstyle',
424 | }
425 | checks = {
426 | pdf_engine = {'latex', 'xelatex', 'lualatex'},
427 | svg_converter = {'dvisvgm'},
428 | }
429 |
430 | -- boolean values
431 | -- @TODO these may be passed as strings in Div attributes
432 | -- convert "xx-yy" to "xx_yy" keys
433 | for _,key in ipairs(renderBooleanlKeys) do
434 | if opts[key] ~= nil then
435 | if pandoctype(opts[key]) == 'boolean' then
436 | result[key:gsub('-','_')] = opts[key]
437 | elseif pandoctype(opts[key]) == 'string' then
438 | if opts[key] == 'false' or opts[key] == 'no' then
439 | result[key:gsub('-','_')] = false
440 | else
441 | result[key:gsub('-','_')] = true
442 | end
443 | end
444 | end
445 | end
446 |
447 | -- string values
448 | -- convert "xx-yy" to "xx_yy" keys
449 | for _,key in ipairs(renderStringKeys) do
450 | if opts[key] then
451 | result[key:gsub('-','_')] = stringify(opts[key])
452 | end
453 | end
454 |
455 | -- list values
456 | for _,key in ipairs(renderListKeys) do
457 | if opts[key] then
458 | result[key:gsub('-','_')] = ensureList(opts[key])
459 | end
460 | end
461 |
462 | -- meta values
463 | -- do not change the key names
464 | for _,key in ipairs(renderMetaKeys) do
465 | if opts[key] then
466 | result[key] = opts[key]
467 | end
468 | end
469 |
470 | -- apply checks
471 | for key, accepted_vals in pairs(checks) do
472 | if result[key] and not tfind(accepted_vals, result[key]) then
473 | message('WARNING', 'Option '..key..'has an invalid value: '
474 | ..result[key]..". I'm ignoring it."
475 | )
476 | result[key] = nil
477 | end
478 | end
479 |
480 | -- Special cases
481 | -- `embed` not possible with `extract-media` on
482 | if result.embed and filterOptions.no_html_embed then
483 | result.embed = nil
484 | end
485 |
486 | return result
487 |
488 | end
489 |
490 | ---readImagifyClasses: read user's specification of custom classes
491 | -- This can be a string (single class), a pandoc.List of strings
492 | -- or a map { class = renderOptionsForClass }.
493 | -- We update `filterOptions.classes` accordingly.
494 | ---@param opts pandoc.List|pandoc.MetaMap|string
495 | local function readImagifyClasses(opts)
496 | -- ensure it's a list or table
497 | if pandoctype(opts) ~= 'List' and pandoctype(opts) ~= 'table' then
498 | opts = pandoc.List:new({ opts })
499 | end
500 |
501 | if pandoctype(opts) == 'List' then
502 | for _, val in ipairs(opts) do
503 | local class = stringify(val)
504 | filterOptions.optionsForClass[class] = {}
505 | end
506 | elseif pandoctype(opts) == 'table' then
507 | for key, val in pairs(opts) do
508 | local class = stringify(key)
509 | filterOptions.optionsForClass[class] = getRenderOptions(val)
510 | end
511 | end
512 |
513 | end
514 |
515 | ---init: read metadata options.
516 | -- Classes in `imagify-classes:` override those in `imagify: classes:`
517 | -- If `meta.imagify` isn't a map assume it's a `scope` value
518 | -- Special cases:
519 | -- filterOptions.no_html_embed: Pandoc can't handle URL-embedded images when extract-media is on
520 | ---@param meta pandoc.Meta doc's metadata
521 | local function init(meta)
522 | local userOptions = meta.imagify
523 | and (pandoctype(meta.imagify) == 'table' and meta.imagify
524 | or {scope = stringify(meta.imagify)}
525 | )
526 | or {}
527 | local userClasses = meta['imagify-classes']
528 | and pandoctype(meta['imagify-classes'] ) == 'table'
529 | and meta['imagify-classes']
530 | or nil
531 | local rootKeysUsed = {
532 | 'header-includes',
533 | 'mathspec',
534 | 'fontenc',
535 | 'fontfamily',
536 | 'fontfamilyoptions',
537 | 'fontsize',
538 | 'mainfont', 'sansfont', 'monofont', 'mathfont', 'CJKmainfont',
539 | 'mainfontoptions', 'sansfontoptions', 'monofontoptions',
540 | 'mathfontoptions', 'CJKoptions',
541 | 'microtypeoptions',
542 | 'colorlinks',
543 | 'boxlinks',
544 | 'linkcolor', 'filecolor', 'citecolor', 'urlcolor', 'toccolor',
545 | -- 'links-as-note': no footnotes in standalone LaTeX class
546 | 'urlstyle',
547 | }
548 |
549 | -- pass relevant root options unless overriden in meta.imagify
550 | for _,key in ipairs(rootKeysUsed) do
551 | if meta[key] and not userOptions[key] then
552 | userOptions[key] = meta[key]
553 | end
554 | end
555 |
556 | filterOptions = mergeMapInto(
557 | getFilterOptions(userOptions),
558 | filterOptions
559 | )
560 |
561 | if meta['extract-media'] and FORMAT:match('html') then
562 | filterOptions.no_html_embed = true
563 | end
564 |
565 | globalRenderOptions = mergeMapInto(
566 | getRenderOptions(userOptions),
567 | globalRenderOptions
568 | )
569 |
570 | if userOptions.classes then
571 | filterOptions.classes = readImagifyClasses(userOptions.classes)
572 | end
573 |
574 | if userClasses then
575 | filterOptions.classes = readImagifyClasses(userClasses)
576 | end
577 |
578 | end
579 |
580 | -- ## Functions to convert images
581 |
582 | ---getTemplate: get a compiled template
583 | ---@param id string template identifier (key of Templates)
584 | ---@return pandoc.Template|nil tpl result
585 | local function getTemplate(id)
586 | if not Templates[id] then
587 | return nil
588 | end
589 |
590 | -- ensure there's a non-empty source, otherwise return nil
591 | -- special case: default template, fill in source from Pandoc
592 | if id == 'default' and not Templates[id].source then
593 | Templates[id].source = pandoc.template.default('latex')
594 | end
595 |
596 | if not Templates[id].source or Templates[id].source == '' then
597 | return nil
598 | end
599 |
600 | -- compile if needed and return
601 |
602 | if not Templates[id].compiled then
603 | Templates[id].compiled = pandoc.template.compile(
604 | Templates[id].source)
605 | end
606 |
607 | return Templates[id].compiled
608 |
609 | end
610 |
611 | ---buildTeXDoc: turns LaTeX element into a LaTeX doc source.
612 | ---@param code string LaTeX code
613 | ---@param renderOptions table render options
614 | ---@param elemType string 'InlineMath', 'DisplayMath', 'RawInline', 'RawBlock'
615 | local function buildTeXDoc(code, renderOptions, elemType)
616 | local endFormat = filterOptions.extensionForOutput[FORMAT]
617 | or filterOptions.extensionForOutput.default
618 | elemType = elemType and elemType or 'InlineMath'
619 | code = code or ''
620 | renderOptions = renderOptions or {}
621 | local template = renderOptions.template or 'default'
622 | local svg_converter = renderOptions.svg_converter or 'dvisvgm'
623 | local doc = nil
624 |
625 | -- wrap DisplayMath and InlineMath in math mode
626 | -- for display math we must use \displaystyle
627 | -- see
628 | if elemType == 'DisplayMath' then
629 | code = '$\\displaystyle\n'..code..'$'
630 | elseif elemType == 'InlineMath' then
631 | code = '$'..code..'$'
632 | end
633 |
634 | doc = pandoc.Pandoc(
635 | pandoc.RawBlock('latex', code),
636 | pandoc.Meta(renderOptions)
637 | )
638 |
639 | -- modify the doc's meta values as required
640 | --@TODO set class option class=...
641 | --Standalone tikz needs \standaloneenv{tikzpicture}
642 | local headinc = ensureList(doc.meta['header-includes'])
643 | local classopt = ensureList(doc.meta['classoption'])
644 |
645 | -- Standalone class `dvisvgm` option: make output file
646 | -- dvisvgm-friendly (esp TikZ images).
647 | -- Not compatible with pdflatex
648 | if endFormat == 'svg' and svg_converter == 'dvisvgm' then
649 | classopt:insert(pandoc.Str('dvisvgm'))
650 | end
651 |
652 | -- The standalone class option `tikz` needs to be activated
653 | -- to avoid an empty page of output.
654 | if usesTikZ(code) then
655 | headinc:insert(pandoc.RawBlock('latex', '\\usepackage{tikz}'))
656 | classopt:insert{
657 | pandoc.Str('tikz')
658 | }
659 | end
660 |
661 | doc.meta['header-includes'] = #headinc > 0 and headinc or nil
662 | doc.meta.classoption = #classopt > 0 and classopt or nil
663 | doc.meta.documentclass = 'standalone'
664 |
665 | return pandoc.write(doc, 'latex', {
666 | template = getTemplate(template),
667 | })
668 |
669 | end
670 |
671 | ---createUniqueName: return unique identifier for an image source.
672 | ---Combines LaTeX sources and rendering options.
673 | ---@param source string LaTeX source for the image
674 | ---@param renderOptions table render options
675 | ---@return string filename without extension
676 | local function createUniqueName(source, renderOptions)
677 | return pandoc.sha1(source ..
678 | '|Zoom:'..renderOptions.zoom)
679 | end
680 |
681 | ---latexToImage: convert LaTeX to image.
682 | -- The image can be exported as SVG string or as a SVG or PDF file.
683 | ---@param source string LaTeX source document
684 | ---@param renderOptions table rendering options
685 | ---@return success boolean, string result result is file contents or filepath or error message.
686 | local function latexToImage(source, renderOptions)
687 | local renderOptions = renderOptions or {}
688 | local ext = filterOptions.extensionForOutput[FORMAT]
689 | or filterOptions.extensionForOutput.default
690 | local lazy = filterOptions.lazy
691 | local embed = renderOptions.embed
692 | and ext == 'svg' and FORMAT:match('html') and true
693 | or false
694 | local pdf_engine = renderOptions.pdf_engine or 'latex'
695 | local latex_out_format = ext == 'svg' and 'dvi' or 'pdf'
696 | local debug = renderOptions.debug or false
697 | local folder = filterOptions.output_folder or ''
698 | local jobOutFolder = makeAbsolute(PANDOC_STATE.output_file
699 | and path.directory(PANDOC_STATE.output_file) ~= '.'
700 | and path.directory(PANDOC_STATE.output_file) or '')
701 | local texinputs = renderOptions.texinputs or nil
702 | -- to be created
703 | local folderAbs, file, fileAbs, texfileAbs = '', '', '', ''
704 | local fileRelativeToJob = ''
705 | local success, result
706 |
707 | -- default texinputs: all sources folders and output folder
708 | -- and directory folder?
709 | if not texinputs then
710 | texinputs = system.get_working_directory()..'//:'
711 | for _,filepath in ipairs(PANDOC_STATE.input_files) do
712 | texinputs = texinputs
713 | .. makeAbsolute(filepath and path.directory(filepath) or '')
714 | .. '//:'
715 | end
716 | texinputs = texinputs.. jobOutFolder .. '//:'
717 | end
718 |
719 | -- if we output files prepare folder and file names
720 | -- we need absolute paths to move things out of the temp dir
721 | if not embed or debug then
722 | folderAbs = makeAbsolute(folder)
723 | filename = createUniqueName(source, renderOptions)
724 | fileAbs = path.join{folderAbs, filename..'.'..ext}
725 | file = path.join{folder, filename..'.'..ext}
726 | texfileAbs = path.join{folderAbs, filename..'.tex'}
727 |
728 | -- ensure the output folder exists (only once)
729 | if not filterOptions.output_folder_exists then
730 | ensureFolderExists(folderAbs)
731 | filterOptions.output_folder_exists = true
732 | end
733 |
734 | -- path to the image relative to document output
735 | fileRelativeToJob = path.make_relative(fileAbs, jobOutFolder)
736 |
737 | -- if lazy, don't regenerate files that already exist
738 | if not embed and lazy and fileExists(fileAbs) then
739 | success, result = true, fileRelativeToJob
740 | return success, result
741 | end
742 |
743 | end
744 |
745 | system.with_temporary_directory('imagify', function (tmpdir)
746 | system.with_working_directory(tmpdir, function()
747 |
748 | writeToFile(source, 'source.tex')
749 |
750 | -- debug: copy before, LaTeX may crash
751 | if debug then
752 | writeToFile(source, texfileAbs)
753 | end
754 |
755 | -- result = 'source.dvi'|'source.xdv'|'source.pdf'|nil
756 | success, result = runLaTeX('source.tex', {
757 | format = latex_out_format,
758 | pdf_engine = pdf_engine,
759 | texinputs = texinputs
760 | })
761 |
762 | -- further conversions of dvi/pdf?
763 |
764 | if success and ext == 'svg' then
765 |
766 | success, result = toSVG(result, {
767 | zoom = renderOptions.zoom,
768 | })
769 |
770 | end
771 |
772 | -- embed or save
773 |
774 | if success then
775 |
776 | if embed and ext == 'svg' then
777 |
778 | -- read svg contents and cleanup
779 | result = "\n"
780 | .. getSVGFromFile(result)
781 |
782 | -- URL encode
783 | result = 'data:image/svg+xml,'..urlEncode(result)
784 |
785 | else
786 |
787 | --- File copy
788 | --- not os.rename, which doesn't work across volumes
789 | --- binary in case the output is PDF
790 | copyFile(result, fileAbs, 'b')
791 | result = fileRelativeToJob
792 |
793 | end
794 |
795 | end
796 |
797 | end)
798 | end)
799 |
800 | return success, result
801 |
802 | end
803 |
804 | ---createImageElemFrom(src, renderOptions, elemType)
805 | ---@param text string source code for the image
806 | ---@param src string URL (possibly URL encoded data)
807 | ---@param renderOptions table render Options
808 | ---@param elemType string 'InlineMath', 'DisplayMath', 'RawInline', 'RawBlock'
809 | ---@return pandoc.Image img
810 | local function createImageElemFrom(text, src, renderOptions, elemType)
811 | local title = text or ''
812 | local caption = '' -- for future implementation (Raw elems attribute?)
813 | local block = elemType == 'DisplayMath' or elemType == 'RawBlock'
814 | local style = ''
815 | local block_style = renderOptions.block_style
816 | or 'display: block; margin: .5em auto; '
817 | local vertical_align = renderOptions.vertical_align
818 | or 'baseline'
819 |
820 | if block then
821 | style = style .. block_style
822 | else
823 | style = style .. 'vertical-align: '..vertical_align..'; '
824 | end
825 |
826 | return pandoc.Image(caption, src, title, { style = style })
827 |
828 | end
829 |
830 | ---toImage: convert to pandoc.Image using specified rendering options.
831 | ---Return the original element if conversion failed.
832 | ---@param elem pandoc.Math|pandoc.RawInline|pandoc.RawBlock|pandoc.Image
833 | ---@param elemType imagifyType type of element to imagify
834 | ---@param renderOptions table rendering options
835 | ---@return pandoc.Image|pandoc.Inlines|pandoc.Para|nil
836 | local function toImage(elem, elemType, renderOptions)
837 | local code, doc
838 | local success, result, img
839 |
840 | -- get code, return nil if none
841 | if elemType == 'TexImage' or elemType == 'TikzImage' then
842 | code = getCodeFromFile(elem.src)
843 | if not code then
844 | message('ERROR', 'Image source file '..elem.src..' not found.')
845 | end
846 | else
847 | code = elem.text
848 | end
849 | if not code then return nil end
850 |
851 | -- prepare LaTeX source document
852 | doc = buildTeXDoc(code, renderOptions, elemType)
853 |
854 | -- convert to file or string
855 | success, result = latexToImage(doc, renderOptions)
856 |
857 | -- prepare Image element
858 | if success then
859 | if (elemType == 'TexImage' or elemType == 'TikzImage') then
860 | elem.src = result
861 | img = elem
862 | elseif elemType == 'RawBlock' then
863 | img = pandoc.Para(
864 | createImageElemFrom(code, result, renderOptions, elemType)
865 | )
866 | else
867 | img = createImageElemFrom(code, result, renderOptions, elemType)
868 | end
869 | else
870 | message('ERROR', result)
871 | img = pandoc.List:new {
872 | pandoc.Emph{ pandoc.Str('') },
873 | pandoc.Space(), pandoc.Str(code), pandoc.Space(),
874 | pandoc.Emph{ pandoc.Str('') },
875 | }
876 | end
877 |
878 | return img
879 |
880 | end
881 |
882 | -- ## Functions to traverse the document tree
883 |
884 | ---imagifyClass: find an element's imagify class, if any.
885 | ---If both `imagify` and a custom class is present, return the latter.
886 | ---@param elem pandoc.Div|pandoc.Span
887 | ---@return string
888 | local function imagifyClass(elem)
889 | -- priority to custom classes other than 'imagify'
890 | for _,class in ipairs(elem.classes) do
891 | if filterOptions.optionsForClass[class] then
892 | return class
893 | end
894 | end
895 | if elem.classes:find('imagify') then
896 | return 'imagify'
897 | end
898 | return nil
899 | end
900 |
901 | ---scanContainer: read imagify options of a Span/Div, imagify if needed.
902 | ---@param elem pandoc.Div|pandoc.Span
903 | ---@param renderOptions table render options handed down from higher-level elems
904 | ---@return pandoc.Span|pandoc.Div|nil span modified element or nil if no change
905 | local function scanContainer(elem, renderOptions)
906 | local class = imagifyClass(elem)
907 |
908 | if class then
909 | -- create new rendering options by applying the class options
910 | local opts = mergeMapInto(filterOptions.optionsForClass[class],
911 | renderOptions)
912 | -- apply any locally specified rendering options
913 | opts = mergeMapInto(getRenderOptions(elem.attributes), opts)
914 | -- build recursive scanner from updated options
915 | local scan = function (elem) return scanContainer(elem, opts) end
916 | --- build imagifier from updated options
917 | local imagify = function(el)
918 | local elemType = imagifyType(el)
919 | if opts.force == true or outputIsLaTeX() == false
920 | or (elemType == 'TexImage' or elemType == 'TikzImage') then
921 | return elemType and toImage(el, elemType, opts) or nil
922 | end
923 | end
924 | --- apply recursion first, then imagifier
925 | return elem:walk({
926 | Div = scan,
927 | Span = scan,
928 | }):walk({
929 | Math = imagify,
930 | RawInline = imagify,
931 | RawBlock = imagify,
932 | Image = imagify,
933 | })
934 |
935 | else
936 |
937 | -- recursion
938 | local scan = function (elem) return scanContainer(elem, renderOptions) end
939 | return elem:walk({
940 | Span = scan,
941 | Div = scan,
942 | })
943 |
944 | end
945 |
946 | end
947 |
948 | ---main: process the main document's body.
949 | -- Handles filterOptions `scope` and `force`
950 | local function main(doc)
951 | local scope = filterOptions.scope
952 | local force = globalRenderOptions.force
953 |
954 | if scope == 'none' then
955 | return nil
956 | end
957 |
958 | -- whole doc wrapped in a Div to use the recursive scanner
959 | local div = pandoc.Div(doc.blocks)
960 |
961 | -- recursive scanning in modes other than 'images'
962 | -- if scope == 'all' we tag the whole doc as `imagify`
963 | if scope ~= 'images' then
964 |
965 | if scope == 'all' then
966 | div.classes:insert('imagify')
967 | end
968 |
969 | div = scanContainer(div, globalRenderOptions)
970 |
971 | end
972 |
973 | -- imagify any leftover tikz / tex images
974 | -- using global render options
975 | div = div:walk({
976 | Image = function (elem)
977 | local elemType = imagifyType(elem)
978 | if elemType then
979 | return toImage(elem, elemType, globalRenderOptions)
980 | end
981 | end,
982 | })
983 |
984 | return div and pandoc.Pandoc(div.content, doc.meta)
985 | or nil
986 |
987 | end
988 |
989 | -- # Return filter
990 |
991 | return {
992 | {
993 | Meta = init,
994 | Pandoc = main,
995 | },
996 | }
997 |
998 |
--------------------------------------------------------------------------------