├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── _extensions └── diagram │ ├── _extension.yaml │ └── diagram.lua ├── diagram.lua ├── sample.md └── test ├── expected-asymptote.html ├── expected-cetz.html ├── expected-dot.html ├── expected-mermaid.html ├── expected-no-alt-or-caption.html ├── expected-plantuml.html ├── expected-tikz.html ├── input-asymptote.md ├── input-cetz.md ├── input-dot.md ├── input-mermaid.md ├── input-no-alt-or-caption.md ├── input-plantuml.md ├── input-tikz.md ├── plantuml-quarto.qmd ├── test-asymptote.yaml ├── test-cetz.yaml ├── test-dot.yaml ├── test-mermaid.yaml ├── test-no-alt-or-caption.yaml ├── test-plantuml.yaml └── test-tikz.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [*.lua] 17 | indent_style = space 18 | indent_size = 2 19 | # Code should stay below 80 characters per line. 20 | max_line_length = 80 21 | 22 | [*.md] 23 | # Text with 60 to 66 characters per line is said to be the easiest 24 | # to read. 25 | max_line_length = 66 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [tarleb] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Run on all pull requests that change code. 5 | pull_request: 6 | paths-ignore: 7 | - 'README.md' 8 | - LICENSE 9 | - .editorconfig 10 | # Run every time a code change is pushed. 11 | push: 12 | paths-ignore: 13 | - 'README.md' 14 | - LICENSE 15 | - .editorconfig 16 | # Test if things still work each Tuesday morning. 17 | # This way we will catch incompatible pandoc changes in a timely 18 | # manner. 19 | schedule: 20 | # At 4:27am each Tuesday 21 | - cron: '27 4 * * 2' 22 | 23 | jobs: 24 | Asymptote: 25 | runs-on: ubuntu-latest 26 | strategy: 27 | fail-fast: true 28 | matrix: 29 | pandoc: 30 | - latest 31 | 32 | container: 33 | image: pandoc/latex:${{ matrix.pandoc }}-ubuntu 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | 39 | - name: Install dependencies 40 | run: | 41 | apt-get -q --no-allow-insecure-repositories update && \ 42 | apt-get install --no-install-recommends --assume-yes \ 43 | make inkscape asymptote 44 | 45 | - name: Test 46 | run: 'make test-asymptote' 47 | 48 | GraphViz: 49 | runs-on: ubuntu-latest 50 | strategy: 51 | fail-fast: true 52 | matrix: 53 | pandoc: 54 | - latest 55 | 56 | container: 57 | image: pandoc/core:${{ matrix.pandoc }}-ubuntu 58 | 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v4 62 | 63 | - name: Install dependencies 64 | run: | 65 | apt-get -q --no-allow-insecure-repositories update && \ 66 | apt-get install --no-install-recommends --assume-yes \ 67 | make graphviz 68 | 69 | - name: Test 70 | run: 'make test-dot test-no-alt-or-caption' 71 | 72 | Mermaid: 73 | runs-on: ubuntu-latest 74 | strategy: 75 | fail-fast: true 76 | matrix: 77 | pandoc: 78 | - latest 79 | 80 | container: 81 | image: pandoc/core:${{ matrix.pandoc }} 82 | 83 | env: 84 | MERMAID_BIN: /usr/local/bin/mmdc-test 85 | PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium-browser 86 | 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v4 90 | 91 | - name: Install dependencies 92 | ## We need a little hack to get puppeteer working in the container. 93 | run: | 94 | apk update && apk add chromium chromium-chromedriver make npm 95 | npm install -g @mermaid-js/mermaid-cli 96 | printf '{"args":["--no-sandbox","--disable-setuid-sandbox", "--disable-gpu"]}' > \ 97 | /etc/puppeteer-conf.json 98 | printf '#!/bin/sh\nmmdc -p /etc/puppeteer-conf.json $@' > $MERMAID_BIN 99 | chmod +x $MERMAID_BIN 100 | 101 | - name: Test 102 | run: 'make test-mermaid' 103 | 104 | PlantUML: 105 | runs-on: ubuntu-latest 106 | strategy: 107 | fail-fast: true 108 | matrix: 109 | pandoc: 110 | - edge 111 | - latest 112 | # The oldest version that's guaranteed to be supported 113 | - '3.0' 114 | 115 | container: 116 | image: pandoc/core:${{ matrix.pandoc }}-ubuntu 117 | 118 | steps: 119 | - name: Checkout 120 | uses: actions/checkout@v4 121 | 122 | - name: Install dependencies 123 | run: | 124 | apt-get -q --no-allow-insecure-repositories update && \ 125 | apt-get install --no-install-recommends --assume-yes \ 126 | make plantuml 127 | 128 | - name: Test 129 | run: make test-plantuml 130 | 131 | TikZ: 132 | runs-on: ubuntu-latest 133 | strategy: 134 | fail-fast: true 135 | matrix: 136 | pandoc: 137 | - latest 138 | 139 | container: 140 | image: pandoc/latex:${{ matrix.pandoc }}-ubuntu 141 | 142 | steps: 143 | - name: Checkout 144 | uses: actions/checkout@v4 145 | 146 | - name: Install dependencies 147 | run: | 148 | tlmgr install pgf standalone 149 | apt-get -q --no-allow-insecure-repositories update && \ 150 | apt-get install --no-install-recommends --assume-yes \ 151 | make inkscape 152 | 153 | - name: Test 154 | run: 'make test-tikz' 155 | 156 | CeTZ: 157 | runs-on: ubuntu-latest 158 | strategy: 159 | fail-fast: true 160 | container: 161 | image: ghcr.io/quarto-dev/quarto:latest 162 | 163 | steps: 164 | - name: Checkout 165 | uses: actions/checkout@v4 166 | 167 | - name: Install dependencies 168 | run: | 169 | apt-get -q --no-allow-insecure-repositories update && \ 170 | apt-get install --no-install-recommends --assume-yes \ 171 | ca-certificates make 172 | 173 | - name: Render 174 | run: make PANDOC=/opt/quarto/bin/tools/x86_64/pandoc test-cetz 175 | 176 | Quarto: 177 | runs-on: ubuntu-latest 178 | strategy: 179 | fail-fast: true 180 | container: 181 | image: ghcr.io/quarto-dev/quarto:latest 182 | 183 | steps: 184 | - name: Checkout 185 | uses: actions/checkout@v4 186 | 187 | - name: Install dependencies 188 | run: | 189 | apt-get -q --no-allow-insecure-repositories update && \ 190 | apt-get install --no-install-recommends --assume-yes \ 191 | make plantuml 192 | 193 | # Quarto rendering should complete without failure, and the 194 | # resulting HTML page should contain an image. 195 | - name: Render 196 | run: quarto render test/plantuml-quarto.qmd 197 | 198 | - name: Check for image 199 | run: grep -q ' [!IMPORTANT] 20 | > This filter makes the generated images available to pandoc, but 21 | > *does not* write image files by itself. Use pandoc's 22 | > `--extract-media` to write the generated images to disk. Or, 23 | > when producing HTML, use `--embed-resources` to incorporate the 24 | > images in the output file via `data` URIs. 25 | 26 | ### Plain pandoc 27 | 28 | Pass the filter to pandoc via the `--lua-filter` (or `-L`) command 29 | line option. 30 | 31 | pandoc --lua-filter diagram.lua ... 32 | 33 | ### Quarto 34 | 35 | Users of Quarto can install this filter as an extension with 36 | 37 | quarto install extension pandoc-ext/diagram 38 | 39 | and use it by adding `diagram` to the `filters` entry in their 40 | YAML header. 41 | 42 | ``` yaml 43 | --- 44 | filters: 45 | - diagram 46 | --- 47 | ``` 48 | 49 | #### Notes on usage with Quarto 50 | 51 | Quarto comes with its own system for diagram generation that can 52 | be used for a variety of diagrams. Especially Mermaid diagram 53 | generation is much faster with Quarto's built-in diagram handling. 54 | 55 | Due to the way in which Quarto handles code blocks, *do not* add 56 | `filename` attributes to code block attribute lists. 57 | 58 | `````` markdown 59 | ``` {.tikz filename="my-graph"} 60 | % DON'T use the filename attribute on code blocks 61 | ... 62 | `````` 63 | 64 | Instead, use the "comment-pipe" syntax to define the graphic's 65 | file name. 66 | 67 | `````` markdown 68 | ``` tikz 69 | %%| filename: my-graph 70 | % This should work ok. 71 | ... 72 | ``` 73 | `````` 74 | 75 | ### R Markdown 76 | 77 | Use `pandoc_args` to invoke the filter. See the [R Markdown 78 | Cookbook](https://bookdown.org/yihui/rmarkdown-cookbook/lua-filters.html) 79 | for details. 80 | 81 | ``` yaml 82 | --- 83 | output: 84 | word_document: 85 | pandoc_args: ['--lua-filter=diagram.lua'] 86 | --- 87 | ``` 88 | 89 | Diagram types 90 | ------------- 91 | 92 | The table below lists the supported diagram drawing systems, the 93 | class that must be used for the system, and the main executable 94 | that the filter calls to generate an image from the code. The 95 | *environment variables* column lists the names of env variables 96 | that can be used to specify a specific executable. 97 | 98 | | System | code block class | executable | env variable | 99 | |-------------|-------------------|------------|-----------------| 100 | | [Asymptote] | `asymptote` | `asy` | `ASYMPTOTE_BIN` | 101 | | [GraphViz] | `dot` | `dot` | `DOT_BIN` | 102 | | [Mermaid] | `mermaid` | `mmdc` | `MERMAID_BIN` | 103 | | [PlantUML] | `plantuml` | `plantuml` | `PLANTUML_BIN` | 104 | | [Ti*k*Z] | `tikz` | `pdflatex` | `PDFLATEX_BIN` | 105 | | [cetz] | `cetz` | `typst` | `TYPST_BIN` | 106 | 107 | ### Other diagram engines 108 | 109 | The filter can be extended with local packages; see 110 | [Configuration](#configuration) below. 111 | 112 | [Asymptote]: https://asymptote.sourceforge.io/ 113 | [GraphViz]: https://www.graphviz.org/ 114 | [Mermaid]: https://mermaid.js.org/ 115 | [PlantUML]: https://plantuml.com/ 116 | [Ti*k*Z]: https://github.com/pgf-tikz/pgf 117 | [Cetz]: https://github.com/cetz-package/cetz 118 | 119 | Figure options 120 | -------------- 121 | 122 | Options can be given using the syntax pioneered by [Quarto]: 123 | 124 | ```` 125 | ``` {.dot} 126 | //| label: fig-boring 127 | //| fig-cap: "A boring Graphviz graph." 128 | digraph boring { 129 | A -> B; 130 | } 131 | ``` 132 | ```` 133 | 134 | [Quarto]: https://quarto.org/ 135 | 136 | Configuration 137 | ------------- 138 | 139 | The filter can be configured with the `diagram` metadata entry. 140 | 141 | Currently supported options: 142 | 143 | - `cache`: controls whether the images are cached. If the cache is 144 | enabled, then the images are recreated only when their code 145 | changes. This option is *disabled* by default. 146 | 147 | - `cache-dir`: Sets the directory in which the images are cached. 148 | The default is to use the `pandoc-diagram-filter` subdir of the 149 | a common caching location. This will be, in the order of 150 | preference, the value of the `XDG_CACHE_HOME` environment 151 | variable if it is set, or alternatively `%USERPROFILE%\.cache` on 152 | Windows and `$HOME/.cache` on all other platforms. 153 | 154 | Caching is disabled if none of the environment variables 155 | mentioned above have been defined. 156 | 157 | - `engine`: options for specific engines, e.g. `plantuml` or 158 | `mermaid`. The options must be nested below the engine name. 159 | Allowed settings are either `true` or `false` to enable or 160 | disable the engine, respectively, or a map of options. 161 | The available settings are: 162 | 163 | + `mime-type`: the output MIME type that should be produced with 164 | this engine. This can be used to choose a specific type, or to 165 | disable certain output formats. For example, the following 166 | disables support for PDF output in PlantUML, which can be 167 | useful when the necessary libraries are unavailable on a 168 | system: 169 | 170 | ``` yaml 171 | diagram: 172 | engine: 173 | plantuml: 174 | mime-type: 175 | application/pdf: false 176 | ``` 177 | 178 | + `line_comment_start`: the character sequence that starts a 179 | line comment; unset or change this to disable or modify the 180 | syntax of user options in the diagram code. 181 | 182 | + `execpath`: the path to the engine's executable. Use this to 183 | override the default executable name listed in the table 184 | above. 185 | 186 | Use a list to pass additional arguments to the executable. 187 | E.g., `execpath: ['xelatex' '-halt-on-error']` will use 188 | `xelatex` as the executable and pass `-halt-on-error` as the 189 | first argument. 190 | 191 | + `package`: if this option is set then the filter will try to 192 | `require` a Lua package with the given name. If the operation 193 | is successful, then the result will be used as the compiler 194 | for that diagram type. 195 | 196 | + Any other option is passed through to the engine. See the 197 | engine-specific settings below. 198 | 199 | ### Engine-specific options 200 | 201 | Some engines accept additional options. These options can either 202 | be passed globally as part of the respective `engine` entry, or 203 | locally by adding `opt-NAME` as an attribute to the diagram code 204 | block. Global options always override local options for security 205 | reasons. 206 | 207 | #### Ti*k*Z 208 | 209 | The Ti*k*Z engine accepts the `header-includes` and 210 | `additional-packages` options. Both options are added to the 211 | intermediary TeX file that is used to produce the output file. The 212 | options differ only in how string values are handled, with bare 213 | strings in `header-includes` being escaped and those in 214 | `additional-packages` being treated as TeX code. 215 | 216 | While mentioned above, it should be highlighted that the 217 | `execpath` option can be used to select a specific LaTeX engine. 218 | The default is `pdflatex`. 219 | 220 | Example: 221 | 222 | ``` yaml 223 | --- 224 | diagram: 225 | engine: 226 | tikz: 227 | execpath: lualatex 228 | header-includes: 229 | - '\usepackage{adjustbox}' 230 | - '\usetikzlibrary{arrows, shapes}' 231 | --- 232 | ``` 233 | 234 | Security 235 | -------- 236 | 237 | This filter **should not** be used with **untrusted documents**, 238 | ***unless*** local configs prevent the setting of filter options 239 | in the metadata: An attacker that can set the execpath for an 240 | engine can execute any binary on the system with the user's 241 | permissions. It is hence recommended to review any document before 242 | using it with this filter to avoid malicious and misuse of the 243 | filter. 244 | 245 | The security is improved considerably if the `diagram` metadata 246 | field is unset or set to a predefined value before this filter is 247 | called, e.g., via another filter or a defaults file. 248 | 249 | Here is an example defaults file that configures the filter such 250 | that the configs cannot be overwritten by the document. 251 | 252 | ``` yaml 253 | # file: diagram-filter.yaml 254 | filters: ['diagram.lua'] 255 | metadata: 256 | engine: 257 | # enable dot/GraphViz and PlantUML with default options 258 | dot: true 259 | plantuml: true 260 | 261 | # disable processing of asymptote and Mermaid diagrams 262 | asymptote: false 263 | mermaid: false 264 | 265 | # Use LuaLaTeX to compile TikZ, define headers 266 | tikz: 267 | execpath: lualatex 268 | additional-packages: | 269 | \usepackage{adjustbox} 270 | \usetikzlibrary{arrows, shapes} 271 | ``` 272 | 273 | Usage: 274 | 275 | pandoc -d diagram-filter ... 276 | -------------------------------------------------------------------------------- /_extensions/diagram/_extension.yaml: -------------------------------------------------------------------------------- 1 | title: diagram 2 | author: Albert Krewinkel 3 | version: 1.2.0 4 | quarto-required: ">=1.3" 5 | contributes: 6 | filters: 7 | - diagram.lua 8 | -------------------------------------------------------------------------------- /_extensions/diagram/diagram.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | diagram – create images and figures from code blocks. 3 | 4 | See copyright notice in file LICENSE. 5 | ]] 6 | -- The filter uses the Figure AST element, which was added in pandoc 3. 7 | PANDOC_VERSION:must_be_at_least '3.0' 8 | 9 | local version = pandoc.types.Version '1.2.0' 10 | 11 | -- Report Lua warnings to stderr if the `warn` function is not plugged into 12 | -- pandoc's logging system. 13 | if not warn then 14 | -- fallback 15 | warn = function(...) io.stderr:write(table.concat({ ... })) end 16 | elseif PANDOC_VERSION < '3.1.4' then 17 | -- starting with pandoc 3.1.4, warnings are reported to pandoc's logging 18 | -- system, so no need to print warnings to stderr. 19 | warn '@on' 20 | end 21 | 22 | local io = require 'io' 23 | local pandoc = require 'pandoc' 24 | local system = require 'pandoc.system' 25 | local utils = require 'pandoc.utils' 26 | local List = require 'pandoc.List' 27 | local stringify = utils.stringify 28 | local with_temporary_directory = system.with_temporary_directory 29 | local with_working_directory = system.with_working_directory 30 | 31 | --- Returns a filter-specific directory in which cache files can be 32 | --- stored, or nil if no such directory is available. 33 | local function cachedir () 34 | local cache_home = os.getenv 'XDG_CACHE_HOME' 35 | if not cache_home or cache_home == '' then 36 | local user_home = system.os == 'windows' 37 | and os.getenv 'USERPROFILE' 38 | or os.getenv 'HOME' 39 | 40 | if not user_home or user_home == '' then 41 | return nil 42 | end 43 | cache_home = pandoc.path.join{user_home, '.cache'} or nil 44 | end 45 | 46 | -- Create filter cache directory 47 | return pandoc.path.join{cache_home, 'pandoc-diagram-filter'} 48 | end 49 | 50 | --- Path holding the image cache, or `nil` if the cache is not used. 51 | local image_cache = nil 52 | 53 | local mimetype_for_extension = { 54 | jpeg = 'image/jpeg', 55 | jpg = 'image/jpeg', 56 | pdf = 'application/pdf', 57 | png = 'image/png', 58 | svg = 'image/svg+xml', 59 | } 60 | 61 | local extension_for_mimetype = { 62 | ['application/pdf'] = 'pdf', 63 | ['image/jpeg'] = 'jpg', 64 | ['image/png'] = 'png', 65 | ['image/svg+xml'] = 'svg', 66 | } 67 | 68 | --- Converts a list of format specifiers to a set of MIME types. 69 | local function mime_types_set (tbl) 70 | local set = {} 71 | local mime_type 72 | for _, image_format_spec in ipairs(tbl) do 73 | mime_type = mimetype_for_extension[image_format_spec] or image_format_spec 74 | set[mime_type] = true 75 | end 76 | return set 77 | end 78 | 79 | --- Reads the contents of a file. 80 | local function read_file (filepath) 81 | local fh = io.open(filepath, 'rb') 82 | local contents = fh:read('a') 83 | fh:close() 84 | return contents 85 | end 86 | 87 | --- Writes the contents into a file at the given path. 88 | local function write_file (filepath, content) 89 | local fh = io.open(filepath, 'wb') 90 | fh:write(content) 91 | fh:close() 92 | end 93 | 94 | --- Like `pandoc.pipe`, but allows "multi word" paths: 95 | -- Supplying a list as the first argument will use the first element as 96 | -- the executable path and prepend the remaining elements to the list of 97 | -- arguments. 98 | local function pipe (command, args, input) 99 | local cmd 100 | if pandoc.utils.type(command) == 'List' then 101 | command = command:map(stringify) 102 | cmd = command:remove(1) 103 | args = command .. args 104 | else 105 | cmd = stringify(command) 106 | end 107 | return pandoc.pipe(cmd, args, input) 108 | end 109 | 110 | 111 | -- 112 | -- Diagram Engines 113 | -- 114 | 115 | -- PlantUML engine; assumes that there's a `plantuml` binary. 116 | local plantuml = { 117 | line_comment_start = [[']], 118 | mime_types = mime_types_set{'pdf', 'png', 'svg'}, 119 | compile = function (self, puml) 120 | local mime_type = self.mime_type or 'image/svg+xml' 121 | -- PlantUML format identifiers correspond to common file extensions. 122 | local format = extension_for_mimetype[mime_type] 123 | if not format then 124 | format, mime_type = 'svg', 'image/svg+xml' 125 | end 126 | local args = {'-t' .. format, "-pipe", "-charset", "UTF8"} 127 | return pipe(self.execpath or 'plantuml', args, puml), mime_type 128 | end, 129 | } 130 | 131 | --- GraphViz engine for the dot language 132 | local graphviz = { 133 | line_comment_start = '//', 134 | mime_types = mime_types_set{'jpg', 'pdf', 'png', 'svg'}, 135 | mime_type = 'image/svg+xml', 136 | compile = function (self, code) 137 | local mime_type = self.mime_type 138 | -- GraphViz format identifiers correspond to common file extensions. 139 | local format = extension_for_mimetype[mime_type] 140 | if not format then 141 | format, mime_type = 'svg', 'image/svg+xml' 142 | end 143 | return pipe(self.execpath or 'dot', {"-T"..format}, code), mime_type 144 | end, 145 | } 146 | 147 | --- Mermaid engine 148 | local mermaid = { 149 | line_comment_start = '%%', 150 | mime_types = mime_types_set{'pdf', 'png', 'svg'}, 151 | compile = function (self, code) 152 | local mime_type = self.mime_type or 'image/svg+xml' 153 | local file_extension = extension_for_mimetype[mime_type] 154 | return with_temporary_directory("diagram", function (tmpdir) 155 | return with_working_directory(tmpdir, function () 156 | local infile = 'diagram.mmd' 157 | local outfile = 'diagram.' .. file_extension 158 | write_file(infile, code) 159 | pipe( 160 | self.execpath or 'mmdc', 161 | {"--pdfFit", "--input", infile, "--output", outfile}, 162 | '' 163 | ) 164 | return read_file(outfile), mime_type 165 | end) 166 | end) 167 | end, 168 | } 169 | 170 | --- TikZ 171 | -- 172 | 173 | --- LaTeX template used to compile TikZ images. 174 | local tikz_template = pandoc.template.compile [[ 175 | \documentclass{standalone} 176 | \usepackage{tikz} 177 | $for(header-includes)$ 178 | $it$ 179 | $endfor$ 180 | $additional-packages$ 181 | \begin{document} 182 | $body$ 183 | \end{document} 184 | ]] 185 | 186 | --- The TikZ engine uses pdflatex to compile TikZ code to an image 187 | local tikz = { 188 | line_comment_start = '%%', 189 | 190 | mime_types = { 191 | ['application/pdf'] = true, 192 | }, 193 | 194 | --- Compile LaTeX with TikZ code to an image 195 | compile = function (self, src, user_opts) 196 | return with_temporary_directory("tikz", function (tmpdir) 197 | return with_working_directory(tmpdir, function () 198 | -- Define file names: 199 | local file_template = "%s/tikz-image.%s" 200 | local tikz_file = file_template:format(tmpdir, "tex") 201 | local pdf_file = file_template:format(tmpdir, "pdf") 202 | 203 | -- Treat string values as raw LaTeX 204 | local meta = { 205 | ['header-includes'] = user_opts['header-includes'], 206 | ['additional-packages'] = {pandoc.RawInline( 207 | 'latex', 208 | stringify(user_opts['additional-packages'] or '') 209 | )}, 210 | } 211 | local tex_code = pandoc.write( 212 | pandoc.Pandoc({pandoc.RawBlock('latex', src)}, meta), 213 | 'latex', 214 | {template = tikz_template} 215 | ) 216 | write_file(tikz_file, tex_code) 217 | 218 | -- Execute the LaTeX compiler: 219 | local success, result = pcall( 220 | pipe, 221 | self.execpath or 'pdflatex', 222 | { '-interaction=nonstopmode', '-output-directory', tmpdir, tikz_file }, 223 | '' 224 | ) 225 | if not success then 226 | warn(string.format( 227 | "The call\n%s\nfailed with error code %s. Output:\n%s", 228 | result.command, 229 | result.error_code, 230 | result.output 231 | )) 232 | end 233 | return read_file(pdf_file), 'application/pdf' 234 | end) 235 | end) 236 | end 237 | } 238 | 239 | --- Asymptote diagram engine 240 | local asymptote = { 241 | line_comment_start = '%%', 242 | mime_types = { 243 | ['application/pdf'] = true, 244 | }, 245 | compile = function (self, code) 246 | return with_temporary_directory("asymptote", function(tmpdir) 247 | return with_working_directory(tmpdir, function () 248 | local pdf_file = "pandoc_diagram.pdf" 249 | local args = {'-tex', 'pdflatex', "-o", "pandoc_diagram", '-'} 250 | pipe(self.execpath or 'asy', args, code) 251 | return read_file(pdf_file), 'application/pdf' 252 | end) 253 | end) 254 | end, 255 | } 256 | 257 | --- Cetz diagram engine 258 | local cetz = { 259 | line_comment_start = '%%', 260 | mime_types = mime_types_set{'jpg', 'pdf', 'png', 'svg'}, 261 | mime_type = 'image/svg+xml', 262 | compile = function (self, code) 263 | local mime_type = self.mime_type 264 | local format = extension_for_mimetype[mime_type] 265 | if not format then 266 | format, mime_type = 'svg', 'image/svg+xml' 267 | end 268 | local preamble = [[ 269 | #import "@preview/cetz:0.3.4" 270 | #set page(width: auto, height: auto, margin: .5cm) 271 | ]] 272 | 273 | local typst_code = preamble .. code 274 | 275 | return with_temporary_directory("diagram", function (tmpdir) 276 | return with_working_directory(tmpdir, function () 277 | local outfile = 'diagram.' .. format 278 | local execpath = self.execpath 279 | if not execpath and quarto and quarto.version >= '1.4' then 280 | -- fall back to the Typst exec shipped with Quarto. 281 | execpath = List{'quarto', 'typst'} 282 | end 283 | pipe( 284 | execpath or 'typst', 285 | {"compile", "-f", format, "-", outfile}, 286 | typst_code 287 | ) 288 | return read_file(outfile), mime_type 289 | end) 290 | end) 291 | end, 292 | } 293 | 294 | local default_engines = { 295 | asymptote = asymptote, 296 | dot = graphviz, 297 | mermaid = mermaid, 298 | plantuml = plantuml, 299 | tikz = tikz, 300 | cetz = cetz, 301 | } 302 | 303 | -- 304 | -- Configuration 305 | -- 306 | 307 | --- Options for the output format of the given name. 308 | local function format_options (name) 309 | local pdf2svg = name ~= 'latex' and name ~= 'context' 310 | local is_office_format = name == 'docx' or name == 'odt' 311 | -- Office formats seem to work better with PNG than with SVG. 312 | local preferred_mime_types = is_office_format 313 | and pandoc.List{'image/png', 'application/pdf'} 314 | or pandoc.List{'application/pdf', 'image/png'} 315 | -- Prefer SVG for non-PDF output formats, except for Office formats 316 | if is_office_format then 317 | preferred_mime_types:insert('image/svg+xml') 318 | elseif pdf2svg then 319 | preferred_mime_types:insert(1, 'image/svg+xml') 320 | end 321 | return { 322 | name = name, 323 | pdf2svg = pdf2svg, 324 | preferred_mime_types = preferred_mime_types, 325 | best_mime_type = function (self, supported_mime_types, requested) 326 | return self.preferred_mime_types:find_if(function (preferred) 327 | return supported_mime_types[preferred] and 328 | (not requested or 329 | (pandoc.utils.type(requested) == 'List' and 330 | requested:includes(preferred)) or 331 | (pandoc.utils.type(requested) == 'table' and 332 | requested[preferred]) or 333 | 334 | -- Assume string, Inlines, and Blocks values specify the only 335 | -- acceptable MIME type. 336 | stringify(requested) == preferred) 337 | end) 338 | end 339 | } 340 | end 341 | 342 | --- Returns a configured diagram engine. 343 | local function get_engine (name, engopts, format) 344 | local engine = default_engines[name] or 345 | select(2, pcall(require, stringify(engopts.package))) 346 | 347 | -- Sanity check 348 | if not engine then 349 | warn(PANDOC_SCRIPT_FILE, ": No such engine '", name, "'.") 350 | return nil 351 | elseif engopts == false then 352 | -- engine is disabled 353 | return nil 354 | elseif engopts == true then 355 | -- use default options 356 | return engine 357 | end 358 | 359 | local execpath = engopts.execpath or os.getenv(name:upper() .. '_BIN') 360 | 361 | local mime_type = format:best_mime_type( 362 | engine.mime_types, 363 | engopts['mime-type'] or engopts['mime-types'] 364 | ) 365 | if not mime_type then 366 | warn(PANDOC_SCRIPT_FILE, ": Cannot use ", name, " with ", format.name) 367 | return nil 368 | end 369 | 370 | return { 371 | execpath = execpath, 372 | compile = engine.compile, 373 | line_comment_start = engine.line_comment_start, 374 | mime_type = mime_type, 375 | opt = engopts or {}, 376 | } 377 | end 378 | 379 | --- Returns the diagram engine configs. 380 | local function configure (meta, format_name) 381 | local conf = meta.diagram or {} 382 | local format = format_options(format_name) 383 | meta.diagram = nil 384 | 385 | -- cache for image files 386 | if conf.cache then 387 | image_cache = conf['cache-dir'] 388 | and stringify(conf['cache-dir']) 389 | or cachedir() 390 | pandoc.system.make_directory(image_cache, true) 391 | end 392 | 393 | -- engine configs 394 | local engine = {} 395 | for name, engopts in pairs(conf.engine or default_engines) do 396 | engine[name] = get_engine(name, engopts, format) 397 | end 398 | 399 | return { 400 | engine = engine, 401 | format = format, 402 | cache = image_cache and true, 403 | image_cache = image_cache, 404 | } 405 | end 406 | 407 | -- 408 | -- Format conversion 409 | -- 410 | 411 | --- Converts a PDF to SVG. 412 | local pdf2svg = function (imgdata) 413 | -- Using `os.tmpname()` instead of a hash would be slightly cleaner, but the 414 | -- function causes problems on Windows (and wasm). See, e.g., 415 | -- https://github.com/pandoc-ext/diagram/issues/49 416 | local pdf_file = 'diagram-' .. pandoc.utils.sha1(imgdata) .. '.pdf' 417 | write_file(pdf_file, imgdata) 418 | local args = { 419 | '--export-type=svg', 420 | '--export-plain-svg', 421 | '--export-filename=-', 422 | pdf_file 423 | } 424 | return pandoc.pipe('inkscape', args, ''), os.remove(pdf_file) 425 | end 426 | 427 | local function properties_from_code (code, comment_start) 428 | local props = {} 429 | local pattern = comment_start:gsub('%p', '%%%1') .. '| ' .. 430 | '([-_%w]+): ([^\n]*)\n' 431 | for key, value in code:gmatch(pattern) do 432 | if key == 'fig-cap' then 433 | props['caption'] = value 434 | else 435 | props[key] = value 436 | end 437 | end 438 | return props 439 | end 440 | 441 | local function diagram_options (cb, comment_start) 442 | local attribs = comment_start 443 | and properties_from_code(cb.text, comment_start) 444 | or {} 445 | for key, value in pairs(cb.attributes) do 446 | attribs[key] = value 447 | end 448 | 449 | local alt 450 | local caption 451 | local fig_attr = {id = cb.identifier} 452 | local filename 453 | local image_attr = {} 454 | local user_opt = {} 455 | 456 | for attr_name, value in pairs(attribs) do 457 | if attr_name == 'alt' then 458 | alt = value 459 | elseif attr_name == 'caption' then 460 | -- Read caption attribute as Markdown 461 | caption = attribs.caption 462 | and pandoc.read(attribs.caption).blocks 463 | or nil 464 | elseif attr_name == 'filename' then 465 | filename = value 466 | elseif attr_name == 'label' then 467 | fig_attr.id = value 468 | elseif attr_name == 'name' then 469 | fig_attr.name = value 470 | else 471 | -- Check for prefixed attributes 472 | local prefix, key = attr_name:match '^(%a+)%-(%a[-%w]*)$' 473 | if prefix == 'fig' then 474 | fig_attr[key] = value 475 | elseif prefix == 'image' or prefix == 'img' then 476 | image_attr[key] = value 477 | elseif prefix == 'opt' then 478 | user_opt[key] = value 479 | else 480 | -- Use as image attribute 481 | image_attr[attr_name] = value 482 | end 483 | end 484 | end 485 | 486 | return { 487 | ['alt'] = alt or 488 | (caption and pandoc.utils.blocks_to_inlines(caption)) or 489 | {}, 490 | ['caption'] = caption, 491 | ['fig-attr'] = fig_attr, 492 | ['filename'] = filename, 493 | ['image-attr'] = image_attr, 494 | ['opt'] = user_opt, 495 | } 496 | end 497 | 498 | local function get_cached_image (hash, mime_type) 499 | if not image_cache then 500 | return nil 501 | end 502 | local filename = hash .. '.' .. extension_for_mimetype[mime_type] 503 | local imgpath = pandoc.path.join{image_cache, filename} 504 | local success, imgdata = pcall(read_file, imgpath) 505 | if success then 506 | return imgdata, mime_type 507 | end 508 | return nil 509 | end 510 | 511 | local function cache_image (codeblock, imgdata, mimetype) 512 | -- do nothing if caching is disabled or not possible. 513 | if not image_cache then 514 | return 515 | end 516 | local ext = extension_for_mimetype[mimetype] 517 | local filename = pandoc.sha1(codeblock.text) .. '.' .. ext 518 | local imgpath = pandoc.path.join{image_cache, filename} 519 | write_file(imgpath, imgdata) 520 | end 521 | 522 | -- Executes each document's code block to find matching code blocks: 523 | local function code_to_figure (conf) 524 | return function (block) 525 | -- Check if a converter exists for this block. If not, return the block 526 | -- unchanged. 527 | local diagram_type = block.classes[1] 528 | if not diagram_type then 529 | return nil 530 | end 531 | 532 | local engine = conf.engine[diagram_type] 533 | if not engine then 534 | return nil 535 | end 536 | 537 | -- Unified properties. 538 | local dgr_opt = diagram_options(block, engine.line_comment_start) 539 | for optname, value in pairs(engine.opt or {}) do 540 | dgr_opt.opt[optname] = dgr_opt.opt[optname] or value 541 | end 542 | 543 | local run_pdf2svg = engine.mime_type == 'application/pdf' 544 | and conf.format.pdf2svg 545 | 546 | -- Try to retrieve the image data from the cache. 547 | local imgdata, imgtype 548 | if conf.cache then 549 | imgdata, imgtype = get_cached_image( 550 | pandoc.sha1(block.text), 551 | run_pdf2svg and 'image/svg+xml' or engine.mime_type 552 | ) 553 | end 554 | 555 | if not imgdata or not imgtype then 556 | -- No cached image; call the converter 557 | local success 558 | success, imgdata, imgtype = 559 | pcall(engine.compile, engine, block.text, dgr_opt.opt) 560 | 561 | -- Bail if an error occurred; imgdata contains the error message 562 | -- when that happens. 563 | if not success then 564 | warn(PANDOC_SCRIPT_FILE, ': ', tostring(imgdata)) 565 | return nil 566 | elseif not imgdata then 567 | warn(PANDOC_SCRIPT_FILE, ': Diagram engine returned no image data.') 568 | return nil 569 | elseif not imgtype then 570 | warn(PANDOC_SCRIPT_FILE, ': Diagram engine did not return a MIME type.') 571 | return nil 572 | end 573 | 574 | -- Convert SVG if necessary. 575 | if imgtype == 'application/pdf' and conf.format.pdf2svg then 576 | imgdata, imgtype = pdf2svg(imgdata), 'image/svg+xml' 577 | end 578 | 579 | -- If we got here, then the transformation went ok and `img` contains 580 | -- the image data. 581 | cache_image(block, imgdata, imgtype) 582 | end 583 | 584 | -- Use the block's filename attribute or create a new name by hashing the 585 | -- image content. 586 | local basename, _extension = pandoc.path.split_extension( 587 | dgr_opt.filename or pandoc.sha1(imgdata) 588 | ) 589 | local fname = basename .. '.' .. extension_for_mimetype[imgtype] 590 | 591 | -- Store the data in the media bag: 592 | pandoc.mediabag.insert(fname, imgtype, imgdata) 593 | 594 | -- Create the image object. 595 | local image = pandoc.Image(dgr_opt.alt, fname, "", dgr_opt['image-attr']) 596 | 597 | -- Create a figure if the diagram has a caption; otherwise return 598 | -- just the image. 599 | return dgr_opt.caption and 600 | pandoc.Figure( 601 | pandoc.Plain{image}, 602 | dgr_opt.caption, 603 | dgr_opt['fig-attr'] 604 | ) or 605 | pandoc.Plain{image} 606 | end 607 | end 608 | 609 | return setmetatable( 610 | {{ 611 | Pandoc = function (doc) 612 | local conf = configure(doc.meta, FORMAT) 613 | return doc:walk { 614 | CodeBlock = code_to_figure(conf), 615 | } 616 | end 617 | }}, 618 | { 619 | version = version, 620 | } 621 | ) 622 | -------------------------------------------------------------------------------- /diagram.lua: -------------------------------------------------------------------------------- 1 | _extensions/diagram/diagram.lua -------------------------------------------------------------------------------- /sample.md: -------------------------------------------------------------------------------- 1 | # Diagram Generator Lua Filter 2 | 3 | ## Introduction 4 | This Lua filter is used to create images with or without captions from code 5 | blocks. Currently PlantUML, Graphviz, Ti*k*Z, Asymptote, and Python can be 6 | processed. This document also serves as a test document, which is why the 7 | subsequent test diagrams are integrated in every supported language. 8 | 9 | ## Prerequisites 10 | To be able to use this Lua filter, the respective external tools must be 11 | installed. However, it is sufficient if the tools to be used are installed. 12 | If you only want to use PlantUML, you don't need LaTeX or Python, etc. 13 | 14 | ### PlantUML 15 | To use PlantUML, you must install PlantUML itself. See the 16 | [PlantUML website](http://plantuml.com/) for more details. It should be 17 | noted that PlantUML is a Java program and therefore Java must also 18 | be installed. 19 | 20 | By default, this filter expects the plantuml.jar file to be in the 21 | working directory. Alternatively, the environment variable 22 | `PLANTUML` can be set with a path. If, for example, a specific 23 | PlantUML version is to be used per pandoc document, the 24 | `plantuml_path` meta variable can be set. 25 | 26 | Furthermore, this filter assumes that Java is located in the 27 | system or user path. This means that from any place of the system 28 | the `java` command is understood. Alternatively, the `JAVA_HOME` 29 | environment variable gets used. To use a specific Java version per 30 | pandoc document, use the `java_path` meta variable. Please notice 31 | that `JAVA_HOME` must be set to the java's home directory e.g. 32 | `c:\Program Files\Java\jre1.8.0_201\` whereas `java_path` must be 33 | set to the absolute path of `java.exe` e.g. 34 | `c:\Program Files\Java\jre1.8.0_201\bin\java.exe`. 35 | 36 | Example usage: 37 | 38 | ```{.plantuml caption="This is an image, created by **PlantUML**." width=50%} 39 | @startuml 40 | Alice -> Bob: Authentication Request Bob --> Alice: Authentication Response 41 | Alice -> Bob: Another authentication Request Alice <-- Bob: another Response 42 | @enduml 43 | ``` 44 | 45 | ### Graphviz 46 | To use Graphviz you only need to install Graphviz, as you can read 47 | on its [website](http://www.graphviz.org/). There are no other 48 | dependencies. 49 | 50 | This filter assumes that the `dot` command is located in the path 51 | and therefore can be used from any location. Alternatively, you can 52 | set the environment variable `DOT` or use the pandoc's meta variable 53 | `dot_path`. 54 | 55 | Example usage from [the Graphviz 56 | gallery](https://graphviz.gitlab.io/_pages/Gallery/directed/fsm.html): 57 | 58 | ```{.dot caption="This is an image, created by **Graphviz**'s dot."} 59 | digraph finite_state_machine { 60 | rankdir=LR; 61 | node [shape = doublecircle]; LR_0 LR_3 LR_4 LR_8; 62 | node [shape = circle]; 63 | LR_0 -> LR_2 [ label = "SS(B)" ]; 64 | LR_0 -> LR_1 [ label = "SS(S)" ]; 65 | LR_1 -> LR_3 [ label = "S($end)" ]; 66 | LR_2 -> LR_6 [ label = "SS(b)" ]; 67 | LR_2 -> LR_5 [ label = "SS(a)" ]; 68 | LR_2 -> LR_4 [ label = "S(A)" ]; 69 | LR_5 -> LR_7 [ label = "S(b)" ]; 70 | LR_5 -> LR_5 [ label = "S(a)" ]; 71 | LR_6 -> LR_6 [ label = "S(b)" ]; 72 | LR_6 -> LR_5 [ label = "S(a)" ]; 73 | LR_7 -> LR_8 [ label = "S(b)" ]; 74 | LR_7 -> LR_5 [ label = "S(a)" ]; 75 | LR_8 -> LR_6 [ label = "S(b)" ]; 76 | LR_8 -> LR_5 [ label = "S(a)" ]; 77 | } 78 | ``` 79 | 80 | ### Ti*k*Z 81 | Ti*k*Z (cf. [Wikipedia](https://en.wikipedia.org/wiki/PGF/TikZ)) is a 82 | description language for graphics of any kind that can be used within 83 | LaTeX (cf. [Wikipedia](https://en.wikipedia.org/wiki/LaTeX)). 84 | 85 | Therefore a LaTeX system must be installed on the system. The Ti*k*Z code is 86 | embedded into a dynamic LaTeX document. This temporary document gets 87 | translated into a PDF document using LaTeX (`pdflatex`). Finally, 88 | Inkscape is used to convert the PDF file to the desired format. 89 | 90 | Note: We are using Inkscape here to use a stable solution for the 91 | convertion. Formerly ImageMagick was used instead. ImageMagick is 92 | not able to convert PDF files. Hence, it uses Ghostscript to do 93 | so, cf. [1](https://stackoverflow.com/a/6599718/2258393). 94 | Unfortunately, Ghostscript behaves unpredictable during Windows and 95 | Linux tests cases, cf. [2](https://stackoverflow.com/questions/21774561/some-pdfs-are-converted-improperly-using-imagemagick), 96 | [3](https://stackoverflow.com/questions/9064706/imagemagic-convert-command-pdf-convertion-with-bad-size-orientation), [4](https://stackoverflow.com/questions/18837093/imagemagic-renders-image-with-black-background), 97 | [5](https://stackoverflow.com/questions/37392798/pdf-to-svg-is-not-perfect), 98 | [6](https://stackoverflow.com/q/10288065/2258393), etc. By using Inkscape, 99 | we need one dependency less and get rid of unexpected Ghostscript issues. 100 | 101 | Due to this more complicated process, the use of Ti*k*Z is also more 102 | complicated overall. The process is error-prone: An insufficiently 103 | configured LaTeX installation or an insufficiently configured 104 | Inkscape installation can lead to errors. Overall, this results in 105 | the following dependencies: 106 | 107 | - Any LaTeX installation. This should be configured so that 108 | missing packages are installed automatically. This filter uses the 109 | `pdflatex` command which is available by the system's path. Alternatively, 110 | you can set the `PDFLATEX` environment variable. In case you have to use 111 | a specific LaTeX version on a pandoc document basis, you might set the 112 | `pdflatex_path` meta variable. 113 | 114 | - An installation of [Inkscape](https://inkscape.org/). 115 | It is assumed that the `inkscape` command is in the path and can be 116 | executed from any location. Alternatively, the environment 117 | variable `INKSCAPE` can be set with a path. If a specific 118 | version per pandoc document is to be used, the `inkscape_path` 119 | meta-variable can be set. 120 | 121 | In order to use additional LaTeX packages, use the optional 122 | `additionalPackages` attribute in your document, as in the 123 | example below. 124 | 125 | Example usage from [TikZ 126 | examples](http://www.texample.net/tikz/examples/parallelepiped/) by 127 | [Kjell Magne Fauske](http://www.texample.net/tikz/examples/nav1d/): 128 | 129 | ```{.tikz caption="This is an image, created by **TikZ i.e. LaTeX**." 130 | additionalPackages="\usepackage{adjustbox}"} 131 | \usetikzlibrary{arrows} 132 | \tikzstyle{int}=[draw, fill=blue!20, minimum size=2em] 133 | \tikzstyle{init} = [pin edge={to-,thin,black}] 134 | 135 | \resizebox{16cm}{!}{% 136 | \trimbox{3.5cm 0cm 0cm 0cm}{ 137 | \begin{tikzpicture}[node distance=2.5cm,auto,>=latex'] 138 | \node [int, pin={[init]above:$v_0$}] (a) {$\frac{1}{s}$}; 139 | \node (b) [left of=a,node distance=2cm, coordinate] {a}; 140 | \node [int, pin={[init]above:$p_0$}] at (0,0) (c) 141 | [right of=a] {$\frac{1}{s}$}; 142 | \node [coordinate] (end) [right of=c, node distance=2cm]{}; 143 | \path[->] (b) edge node {$a$} (a); 144 | \path[->] (a) edge node {$v$} (c); 145 | \draw[->] (c) edge node {$p$} (end) ; 146 | \end{tikzpicture} 147 | } 148 | } 149 | ``` 150 | 151 | ### Python 152 | In order to use Python to generate an diagram, your Python code must store the 153 | final image data in a temporary file with the correct format. In case you use 154 | matplotlib for a diagram, add the following line to do so: 155 | 156 | ```python 157 | plt.savefig("$DESTINATION$", dpi=300, format="$FORMAT$") 158 | ``` 159 | 160 | The placeholder `$FORMAT$` gets replace by the necessary format. Most of the 161 | time, this will be `png` or `svg`. The second placeholder, `$DESTINATION$` 162 | gets replaced by the path and file name of the destination. Both placeholders 163 | can be used as many times as you want. Example usage from the [Matplotlib 164 | examples](https://matplotlib.org/gallery/lines_bars_and_markers/cohere.html#sphx-glr-gallery-lines-bars-and-markers-cohere-py): 165 | 166 | ```{.py2image caption="This is an image, created by **Python**."} 167 | import matplotlib 168 | matplotlib.use('Agg') 169 | 170 | import sys 171 | import numpy as np 172 | import matplotlib.pyplot as plt 173 | 174 | # Fixing random state for reproducibility 175 | np.random.seed(19680801) 176 | 177 | dt = 0.01 178 | t = np.arange(0, 30, dt) 179 | nse1 = np.random.randn(len(t)) # white noise 1 180 | nse2 = np.random.randn(len(t)) # white noise 2 181 | 182 | # Two signals with a coherent part at 10Hz and a random part 183 | s1 = np.sin(2 * np.pi * 10 * t) + nse1 184 | s2 = np.sin(2 * np.pi * 10 * t) + nse2 185 | 186 | fig, axs = plt.subplots(2, 1) 187 | axs[0].plot(t, s1, t, s2) 188 | axs[0].set_xlim(0, 2) 189 | axs[0].set_xlabel('time') 190 | axs[0].set_ylabel('s1 and s2') 191 | axs[0].grid(True) 192 | 193 | cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt) 194 | axs[1].set_ylabel('coherence') 195 | 196 | fig.tight_layout() 197 | plt.savefig("$DESTINATION$", dpi=300, format="$FORMAT$") 198 | ``` 199 | 200 | Precondition to use Python is a Python environment which contains all 201 | necessary libraries you want to use. To use, for example, the standard 202 | [Anaconda Python](https://www.anaconda.com/distribution/) environment 203 | on a Microsoft Windows system ... 204 | 205 | - set the environment variable `PYTHON` or the meta key `pythonPath` 206 | to `c:\ProgramData\Anaconda3\python.exe` 207 | 208 | - set the environment variable `PYTHON_ACTIVATE` or the meta 209 | key `activate_python_path` to `c:\ProgramData\Anaconda3\Scripts\activate.bat`. 210 | 211 | Pandoc will activate this Python environment and starts Python with your code. 212 | 213 | ## Asymptote 214 | [Asymptote](https://asymptote.sourceforge.io/) is a graphics 215 | language inspired by Metapost. To use Asymptote, you will need to 216 | install the software itself, a TeX distribution such as 217 | [TeX Live](https://www.tug.org/texlive/), and 218 | [dvisvgm](https://dvisvgm.de/), which may be included in the TeX 219 | distribution. 220 | 221 | If png output is required (such as for the `docx`, `pptx` and `rtf` 222 | output formats) Inkscape must be installed. See the Ti*k*Z section 223 | for details. 224 | 225 | Ensure that the Asymptote `asy` binary is in the path, or point 226 | the environment variable `ASYMPTOTE` or the metadata variable 227 | `asymptotePath` to the full path name. Asymptote calls the various 228 | TeX utilities and dvipdfm, so you will need to configure Asymptote 229 | so that it finds them. 230 | 231 | ```{.asymptote caption="This is an image, created by **Asymptote**."} 232 | size(5cm); 233 | include graph; 234 | 235 | pair circumcenter(pair A, pair B, pair C) 236 | { 237 | pair P, Q, R, S; 238 | P = (A+B)/2; 239 | Q = (B+C)/2; 240 | R = rotate(90, P) * A; 241 | S = rotate(90, Q) * B; 242 | return extension(P, R, Q, S); 243 | } 244 | 245 | pair incenter(pair A, pair B, pair C) 246 | { 247 | real a = abs(angle(C-A)-angle(B-A)), 248 | b = abs(angle(C-B)-angle(A-B)), 249 | c = abs(angle(A-C)-angle(B-C)); 250 | return (sin(a)*A + sin(b)*B + sin(c)*C) / (sin(a)+sin(b)+sin(c)); 251 | } 252 | 253 | real dist_A_BC(pair A, pair B, pair C) 254 | { 255 | real det = cross(B-A, C-A); 256 | return abs(det/abs(B-C)); 257 | } 258 | 259 | pair A = (0, 0), B = (5, 0), C = (3.5, 4), 260 | O = circumcenter(A, B, C), 261 | I = incenter(A, B, C); 262 | dot(A); dot(B); dot(C); dot(O, blue); dot(I, magenta); 263 | draw(A--B--C--cycle, linewidth(2)); 264 | draw(Circle(O, abs(A-O)), blue+linewidth(1.5)); 265 | draw(Circle(I, dist_A_BC(I, A, B)), magenta+linewidth(1.5)); 266 | label("$A$", A, SW); 267 | label("$B$", B, SE); 268 | label("$C$", C, NE); 269 | label("$O$", O, W); 270 | label("$I$", I, E); 271 | ``` 272 | 273 | ## How to run pandoc 274 | This section will show, how to call Pandoc in order to use this filter with 275 | meta keys. The following command assume, that the filters are stored in the 276 | subdirectory `filters`. Further, this is a example for a Microsoft Windows 277 | system. 278 | 279 | Command to use PlantUML (a single line): 280 | 281 | ``` 282 | pandoc.exe README.md -f markdown -t docx --self-contained --standalone --lua-filter=filters\diagram-generator.lua --metadata=plantumlPath:"c:\ProgramData\chocolatey\lib\plantuml\tools\plantuml.jar" --metadata=javaPath:"c:\Program Files\Java\jre1.8.0_201\bin\java.exe" -o README.docx 283 | ``` 284 | 285 | All available environment variables: 286 | 287 | - `PLANTUML` e.g. `c:\ProgramData\chocolatey\lib\plantuml\tools\plantuml.jar`; Default: `plantuml.jar` 288 | - `INKSCAPE` e.g. `c:\Program Files\Inkscape\inkscape.exe`; Default: `inkscape` 289 | - `PYTHON` e.g. `c:\ProgramData\Anaconda3\python.exe`; Default: n/a 290 | - `PYTHON_ACTIVATE` e.g. `c:\ProgramData\Anaconda3\Scripts\activate.bat`; Default: n/a 291 | - `JAVA_HOME` e.g. `c:\Program Files\Java\jre1.8.0_201`; Default: n/a 292 | - `DOT` e.g. `c:\ProgramData\chocolatey\bin\dot.exe`; Default: `dot` 293 | - `PDFLATEX` e.g. `c:\Program Files\MiKTeX 2.9\miktex\bin\x64\pdflatex.exe`; Default: `pdflatex` 294 | - `ASYMPTOTE` e.g. `c:\Program Files\Asymptote\asy`; Default: `asy` 295 | 296 | All available meta keys: 297 | 298 | - `plantuml_path` 299 | - `inkscape_path` 300 | - `python_path` 301 | - `activate_python_path` 302 | - `java_path` 303 | - `dot_path` 304 | - `pdflatex_path` 305 | - `asymptote_path` 306 | -------------------------------------------------------------------------------- /test/expected-asymptote.html: -------------------------------------------------------------------------------- 1 |

Asymptote

2 |
3 | This is an image, created by Asymptote. 4 | 6 |
7 | -------------------------------------------------------------------------------- /test/expected-cetz.html: -------------------------------------------------------------------------------- 1 |

Cetz

2 |

The image code 4 | comes from the cetz docs licensed under LGPLv3.

6 |
7 | This is an image, created with cetz 9 | 11 |
12 | -------------------------------------------------------------------------------- /test/expected-dot.html: -------------------------------------------------------------------------------- 1 |

Graphviz

2 |

Example usage from the 4 | Graphviz gallery:

5 |
6 | Finite State Machine 7 | 8 |
9 | -------------------------------------------------------------------------------- /test/expected-mermaid.html: -------------------------------------------------------------------------------- 1 |

Mermaid

2 |

Mermaid is a JavaScript-based diagramming and charting tool.

3 |
4 | A simple flowchart. 5 | 6 |
7 | -------------------------------------------------------------------------------- /test/expected-no-alt-or-caption.html: -------------------------------------------------------------------------------- 1 |

This simple graph has neither alt text nor a caption. It should be 2 | converted to a simple image without any description.

3 | 4 | -------------------------------------------------------------------------------- /test/expected-plantuml.html: -------------------------------------------------------------------------------- 1 |

Authentication

2 |
3 | This is an image, created by PlantUML. 5 | 7 |
8 | -------------------------------------------------------------------------------- /test/expected-tikz.html: -------------------------------------------------------------------------------- 1 |

TikZ

2 |

Example usage from TikZ 4 | examples by Kjell Magne 6 | Fauske:

7 |
8 | Tetrahedron inscribed in a parallelepiped. 10 | 12 |
13 |

Diagram showing how the delta-graph relates to the other graphs. Note 14 | that this diagram does not have a caption, so it will be rendered as a 15 | plain image instead of a figure.

16 | Diagram showing how the delta-graph relates to the other graphs. 18 | -------------------------------------------------------------------------------- /test/input-asymptote.md: -------------------------------------------------------------------------------- 1 | ## Asymptote 2 | 3 | ```{.asymptote 4 | caption="This is an image, created by **Asymptote**." 5 | filename='triangle'} 6 | size(5cm); 7 | include graph; 8 | 9 | pair circumcenter(pair A, pair B, pair C) 10 | { 11 | pair P, Q, R, S; 12 | P = (A+B)/2; 13 | Q = (B+C)/2; 14 | R = rotate(90, P) * A; 15 | S = rotate(90, Q) * B; 16 | return extension(P, R, Q, S); 17 | } 18 | 19 | pair incenter(pair A, pair B, pair C) 20 | { 21 | real a = abs(angle(C-A)-angle(B-A)), 22 | b = abs(angle(C-B)-angle(A-B)), 23 | c = abs(angle(A-C)-angle(B-C)); 24 | return (sin(a)*A + sin(b)*B + sin(c)*C) / (sin(a)+sin(b)+sin(c)); 25 | } 26 | 27 | real dist_A_BC(pair A, pair B, pair C) 28 | { 29 | real det = cross(B-A, C-A); 30 | return abs(det/abs(B-C)); 31 | } 32 | 33 | pair A = (0, 0), B = (5, 0), C = (3.5, 4), 34 | O = circumcenter(A, B, C), 35 | I = incenter(A, B, C); 36 | dot(A); dot(B); dot(C); dot(O, blue); dot(I, magenta); 37 | draw(A--B--C--cycle, linewidth(2)); 38 | draw(Circle(O, abs(A-O)), blue+linewidth(1.5)); 39 | draw(Circle(I, dist_A_BC(I, A, B)), magenta+linewidth(1.5)); 40 | label("$A$", A, SW); 41 | label("$B$", B, SE); 42 | label("$C$", C, NE); 43 | label("$O$", O, W); 44 | label("$I$", I, E); 45 | ``` 46 | -------------------------------------------------------------------------------- /test/input-cetz.md: -------------------------------------------------------------------------------- 1 | # Cetz 2 | 3 | The image 4 | [code](https://github.com/cetz-package/cetz/blob/master/gallery/karls-picture.typ) 5 | comes from the cetz docs licensed under 6 | [LGPLv3](https://github.com/cetz-package/cetz/blob/master/LICENSE). 7 | 8 | ```{.cetz caption="This is an image, created with cetz" width=90% filename=karls} 9 | //| label: fig-auth 10 | //| class: important 11 | #show math.equation: block.with(fill: white, inset: 1pt) 12 | 13 | #cetz.canvas(length: 5cm, { 14 | import cetz.draw: * 15 | 16 | set-style( 17 | mark: (fill: black, scale: 2), 18 | stroke: (thickness: 0.4pt, cap: "round"), 19 | angle: ( 20 | radius: 0.3, 21 | label-radius: .22, 22 | fill: green.lighten(80%), 23 | stroke: (paint: green.darken(50%)) 24 | ), 25 | content: (padding: 1pt) 26 | ) 27 | 28 | grid((-1.5, -1.5), (1.4, 1.4), step: 0.5, stroke: gray + 0.2pt) 29 | 30 | circle((0,0), radius: 1) 31 | 32 | line((-1.5, 0), (1.5, 0), mark: (end: "stealth")) 33 | content((), $ x $, anchor: "west") 34 | line((0, -1.5), (0, 1.5), mark: (end: "stealth")) 35 | content((), $ y $, anchor: "south") 36 | 37 | for (x, ct) in ((-1, $ -1 $), (-0.5, $ -1/2 $), (1, $ 1 $)) { 38 | line((x, 3pt), (x, -3pt)) 39 | content((), anchor: "north", ct) 40 | } 41 | 42 | for (y, ct) in ((-1, $ -1 $), (-0.5, $ -1/2 $), (0.5, $ 1/2 $), (1, $ 1 $)) { 43 | line((3pt, y), (-3pt, y)) 44 | content((), anchor: "east", ct) 45 | } 46 | 47 | // Draw the green angle 48 | cetz.angle.angle((0,0), (1,0), (1, calc.tan(30deg)), 49 | label: text(green, [#sym.alpha])) 50 | 51 | line((0,0), (1, calc.tan(30deg))) 52 | 53 | set-style(stroke: (thickness: 1.2pt)) 54 | 55 | line((30deg, 1), ((), "|-", (0,0)), stroke: (paint: red), name: "sin") 56 | content(("sin.start", 50%, "sin.end"), text(red)[$ sin alpha $]) 57 | line("sin.end", (0,0), stroke: (paint: blue), name: "cos") 58 | content(("cos.start", 50%, "cos.end"), text(blue)[$ cos alpha $], anchor: "north") 59 | line((1, 0), (1, calc.tan(30deg)), name: "tan", stroke: (paint: orange)) 60 | content("tan.end", $ text(#orange, tan alpha) = text(#red, sin alpha) / text(#blue, cos alpha) $, anchor: "west") 61 | }) 62 | 63 | ``` 64 | -------------------------------------------------------------------------------- /test/input-dot.md: -------------------------------------------------------------------------------- 1 | ### Graphviz 2 | 3 | Example usage from [the Graphviz 4 | gallery](https://graphviz.gitlab.io/_pages/Gallery/directed/fsm.html): 5 | 6 | ```{.dot caption="Finite State Machine" filename="fsm"} 7 | digraph finite_state_machine { 8 | rankdir=LR; 9 | node [shape = doublecircle]; LR_0 LR_3 LR_4 LR_8; 10 | node [shape = circle]; 11 | LR_0 -> LR_2 [ label = "SS(B)" ]; 12 | LR_0 -> LR_1 [ label = "SS(S)" ]; 13 | LR_1 -> LR_3 [ label = "S($end)" ]; 14 | LR_2 -> LR_6 [ label = "SS(b)" ]; 15 | LR_2 -> LR_5 [ label = "SS(a)" ]; 16 | LR_2 -> LR_4 [ label = "S(A)" ]; 17 | LR_5 -> LR_7 [ label = "S(b)" ]; 18 | LR_5 -> LR_5 [ label = "S(a)" ]; 19 | LR_6 -> LR_6 [ label = "S(b)" ]; 20 | LR_6 -> LR_5 [ label = "S(a)" ]; 21 | LR_7 -> LR_8 [ label = "S(b)" ]; 22 | LR_7 -> LR_5 [ label = "S(a)" ]; 23 | LR_8 -> LR_6 [ label = "S(b)" ]; 24 | LR_8 -> LR_5 [ label = "S(a)" ]; 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /test/input-mermaid.md: -------------------------------------------------------------------------------- 1 | ### Mermaid 2 | 3 | Mermaid is a JavaScript-based diagramming and charting tool. 4 | 5 | ``` mermaid 6 | %%| filename: flowchart 7 | %%| fig-cap: A simple flowchart. 8 | graph TD; 9 | A-->B; 10 | A-->C; 11 | B-->D; 12 | C-->D; 13 | ``` 14 | -------------------------------------------------------------------------------- /test/input-no-alt-or-caption.md: -------------------------------------------------------------------------------- 1 | This simple graph has neither alt text nor a caption. It should be 2 | converted to a simple image without any description. 3 | 4 | ```{.dot} 5 | digraph { 6 | A -> B; 7 | B -> C; 8 | C -> A; 9 | } 10 | ``` 11 | -------------------------------------------------------------------------------- /test/input-plantuml.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | ```{.plantuml caption="This is an image, created by **PlantUML**." width=50% filename=auth} 4 | '| label: fig-auth 5 | '| class: important 6 | @startuml 7 | Alice -> Bob: Authentication Request Bob --> Alice: Authentication Response 8 | Alice -> Bob: Another authentication Request Alice <-- Bob: another Response 9 | @enduml 10 | ``` 11 | -------------------------------------------------------------------------------- /test/input-tikz.md: -------------------------------------------------------------------------------- 1 | --- 2 | diagram: 3 | cache: false 4 | engine: 5 | tikz: 6 | execpath: ['xelatex', '-halt-on-error'] 7 | header-includes: 8 | - '\usetikzlibrary{arrows, shapes}' 9 | --- 10 | 11 | ### Ti*k*Z 12 | 13 | Example usage from [TikZ 14 | examples](http://www.texample.net/tikz/examples/parallelepiped/) by 15 | [Kjell Magne Fauske](http://www.texample.net/tikz/examples/nav1d/): 16 | 17 | ```{.tikz 18 | caption="Tetrahedron inscribed in a parallelepiped." 19 | filename="parallelepiped" 20 | opt-additional-packages="\usepackage{adjustbox}"} 21 | \tikzstyle{int}=[draw, fill=blue!20, minimum size=2em] 22 | \tikzstyle{init} = [pin edge={to-,thin,black}] 23 | 24 | \resizebox{16cm}{!}{% 25 | \trimbox{3.5cm 0cm 0cm 0cm}{ 26 | \begin{tikzpicture}[node distance=2.5cm,auto,>=latex'] 27 | \node [int, pin={[init]above:$v_0$}] (a) {$\frac{1}{s}$}; 28 | \node (b) [left of=a,node distance=2cm, coordinate] {a}; 29 | \node [int, pin={[init]above:$p_0$}] at (0,0) (c) 30 | [right of=a] {$\frac{1}{s}$}; 31 | \node [coordinate] (end) [right of=c, node distance=2cm]{}; 32 | \path[->] (b) edge node {$a$} (a); 33 | \path[->] (a) edge node {$v$} (c); 34 | \draw[->] (c) edge node {$p$} (end) ; 35 | \end{tikzpicture} 36 | } 37 | } 38 | ``` 39 | 40 | Diagram showing how the delta-graph relates to the other graphs. 41 | Note that this diagram does not have a caption, so it will be 42 | rendered as a plain image instead of a figure. 43 | 44 | ``` {.tikz} 45 | %%| label: delta-graph 46 | %%| filename: delta-graph.pdf 47 | %%| alt: Diagram showing how the delta-graph relates to the other graphs. 48 | \tikzset{cat object/.style= {node distance=4em}} 49 | 50 | \begin{tikzpicture}[] 51 | \node [cat object] (Del) {$D$}; 52 | \node [cat object] (L) [below of=Del] {$X$}; 53 | \node [cat object] (I) [right of=L] {$I$}; 54 | \node [cat object] (F) [left of=L] {$F$}; 55 | 56 | \draw [->] (Del) to node [left,near end]{$\scriptstyle{d_X}$} (L); 57 | \draw [->] (I) to node [below] {$\scriptstyle{x}$} (L); 58 | \draw [->] (Del) to node [above left] {$\scriptstyle{d_{F}}$} (F); 59 | 60 | \draw [->,dashed] (Del) to node {/}(I); 61 | \end{tikzpicture} 62 | ``` 63 | -------------------------------------------------------------------------------- /test/plantuml-quarto.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: PlantUML diagram 3 | author: Tester McTestface 4 | format: 5 | html: 6 | filters: 7 | - '../diagram.lua' 8 | diagram: 9 | cache: false 10 | --- 11 | 12 | ```{.plantuml caption="This is an image, created by **PlantUML**." width=50%} 13 | '| label: fig-auth 14 | '| class: important 15 | '| filename: auth 16 | @startuml 17 | Alice -> Bob: Authentication Request Bob --> Alice: Authentication Response 18 | Alice -> Bob: Another authentication Request Alice <-- Bob: another Response 19 | @enduml 20 | ``` 21 | -------------------------------------------------------------------------------- /test/test-asymptote.yaml: -------------------------------------------------------------------------------- 1 | input-files: ['test/input-asymptote.md'] 2 | filters: 3 | - diagram.lua 4 | to: html 5 | metadata: 6 | pagetitle: Asymptote diagram 7 | variables: 8 | document-css: false 9 | -------------------------------------------------------------------------------- /test/test-cetz.yaml: -------------------------------------------------------------------------------- 1 | input-files: ['test/input-cetz.md'] 2 | filters: 3 | - diagram.lua 4 | to: html 5 | metadata: 6 | diagram: 7 | engine: 8 | cetz: 9 | execpath: ['quarto', 'typst'] -------------------------------------------------------------------------------- /test/test-dot.yaml: -------------------------------------------------------------------------------- 1 | input-files: ['test/input-dot.md'] 2 | filters: 3 | - diagram.lua 4 | to: html 5 | metadata: 6 | pagetitle: dot diagram 7 | diagram: 8 | cache: false 9 | engine: 10 | dot: true 11 | variables: 12 | document-css: false 13 | -------------------------------------------------------------------------------- /test/test-mermaid.yaml: -------------------------------------------------------------------------------- 1 | input-files: ['test/input-mermaid.md'] 2 | filters: 3 | - diagram.lua 4 | to: html 5 | metadata: 6 | pagetitle: Mermaid diagram 7 | diagram: 8 | cache: false 9 | variables: 10 | document-css: false 11 | -------------------------------------------------------------------------------- /test/test-no-alt-or-caption.yaml: -------------------------------------------------------------------------------- 1 | input-files: ['test/input-no-alt-or-caption.md'] 2 | filters: 3 | - diagram.lua 4 | to: html 5 | metadata: 6 | pagetitle: dot diagram 7 | -------------------------------------------------------------------------------- /test/test-plantuml.yaml: -------------------------------------------------------------------------------- 1 | input-files: ['test/input-plantuml.md'] 2 | filters: 3 | - diagram.lua 4 | to: html 5 | metadata: 6 | pagetitle: plantuml diagram 7 | diagram: 8 | cache: false 9 | variables: 10 | document-css: false 11 | -------------------------------------------------------------------------------- /test/test-tikz.yaml: -------------------------------------------------------------------------------- 1 | input-files: ['test/input-tikz.md'] 2 | filters: 3 | - diagram.lua 4 | to: html 5 | --------------------------------------------------------------------------------