├── .asciidoctor └── kroki │ └── .gitkeep ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── build-js.yml │ ├── build-ruby.yml │ ├── release-js.yml │ └── release-ruby.yml ├── .gitignore ├── .mocharc.json ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── RELEASE.md ├── dist └── browser │ └── asciidoctor-kroki.js ├── package-lock.json ├── package.json ├── renovate.json5 ├── ruby ├── .asciidoctor │ └── kroki │ │ └── .gitkeep ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── asciidoctor-kroki.gemspec ├── lib │ ├── asciidoctor-kroki.rb │ └── asciidoctor │ │ └── extensions │ │ ├── asciidoctor_kroki.rb │ │ └── asciidoctor_kroki │ │ ├── extension.rb │ │ └── version.rb ├── spec │ ├── .rubocop.yml │ ├── asciidoctor_kroki_block_macro_spec.rb │ ├── asciidoctor_kroki_client_spec.rb │ ├── asciidoctor_kroki_diagram_spec.rb │ ├── asciidoctor_kroki_processor_spec.rb │ ├── asciidoctor_kroki_spec.rb │ ├── fixtures │ │ ├── alice.puml │ │ ├── config.puml │ │ └── plantuml-diagram.png │ ├── require_spec.rb │ └── rspec_helper.rb └── tasks │ ├── bundler.rake │ ├── lint.rake │ └── rspec.rake ├── src ├── antora-adapter.js ├── asciidoctor-kroki.js ├── fetch.js ├── http │ ├── browser-http.js │ ├── http-client.js │ └── node-http.js ├── kroki-client.js ├── node-fs.js └── preprocess.js ├── tasks ├── package.sh ├── publish.js └── publish.sh └── test ├── 204-server.js ├── 500-server.js ├── antora ├── .gitignore ├── docs │ ├── antora.yml │ └── modules │ │ └── ROOT │ │ ├── examples │ │ ├── ab-all.puml │ │ ├── ab.puml │ │ ├── barley.json │ │ └── styles.puml │ │ ├── nav.adoc │ │ ├── pages │ │ ├── attributes.adoc │ │ ├── embedding.adoc │ │ ├── embeddingblockmacro.adoc │ │ ├── index.adoc │ │ ├── resolve-antora-resource-ids.adoc │ │ ├── source-location.adoc │ │ └── topic │ │ │ └── index.adoc │ │ └── partials │ │ ├── ab-all.adoc │ │ ├── ab.puml │ │ └── ab_inc.puml ├── site-remote.yml ├── site.yml └── test.spec.js ├── block-attributes.spec.js ├── browser ├── index.html ├── run.js └── test.js ├── fixtures ├── alice.puml ├── cars-repeated-charts.vlite ├── chart.vlite ├── docs │ ├── data.adoc │ ├── diagrams │ │ ├── data │ │ │ └── seattle-weather.csv │ │ ├── hello.puml │ │ ├── style.puml │ │ └── weather.vlite │ └── hello.adoc ├── expected │ ├── alice-bluegray.svg │ ├── alice.svg │ ├── cars-repeated-charts.svg │ └── chart.svg ├── fetch │ └── doc.adoc ├── macro │ └── doc.adoc ├── plantuml │ ├── alice-with-styles.puml │ ├── diagrams │ │ ├── hello-with-base-and-note.puml │ │ ├── hello-with-style.puml │ │ ├── id.puml │ │ ├── index.puml │ │ └── subs.puml │ ├── hello.puml │ ├── include │ │ ├── base.iuml │ │ ├── grand-parent.iuml │ │ ├── itself.iuml │ │ └── parent │ │ │ ├── child │ │ │ ├── child.iuml │ │ │ └── handwritten.iuml │ │ │ ├── parent.iuml │ │ │ └── shadow.iuml │ └── styles │ │ ├── general with spaces.iuml │ │ ├── general.iuml │ │ ├── general.puml │ │ ├── note.iuml │ │ ├── sequence.iuml │ │ ├── style with spaces.iuml │ │ ├── style-include-once-general.iuml │ │ └── style.iuml ├── simple.bytefield └── vegalite-data.csv ├── kroki-client.spec.js ├── node-http.spec.js ├── preprocess.spec.js ├── test.spec.js └── utils.js /.asciidoctor/kroki/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asciidoctor/asciidoctor-kroki/602658c6c1fc0c50b33fd0116961ce6b12afabdc/.asciidoctor/kroki/.gitkeep -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [test/fixtures/**] 13 | insert_final_newline = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # To match the .editorconfig that asks for lf only 2 | # Otherwise you will end up with files that show up as modified after you IDE 3 | * text=auto eol=lf 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ggrossetie 2 | -------------------------------------------------------------------------------- /.github/workflows/build-js.yml: -------------------------------------------------------------------------------- 1 | name: Build JavaScript 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | - 'RELEASE.md' 8 | - 'ruby/**' 9 | branches: 10 | - master 11 | 12 | pull_request: 13 | paths-ignore: 14 | - 'README.md' 15 | - 'RELEASE.md' 16 | - 'ruby/**' 17 | branches: 18 | - '*' 19 | 20 | jobs: 21 | build: 22 | strategy: 23 | matrix: 24 | os: 25 | - ubuntu-latest 26 | - windows-latest 27 | node-version: 28 | - 16 29 | - 18 30 | runs-on: ${{ matrix.os }} 31 | steps: 32 | - uses: actions/checkout@v3 33 | - name: Set up Node ${{ matrix.node-version }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | # libgbm-dev is required by Puppeteer 3+ 38 | - name: Install system dependencies 39 | run: | 40 | sudo apt-get install -y libgbm-dev 41 | if: ${{ runner.os == 'Linux' }} 42 | - name: Install dependencies 43 | run: | 44 | npm ci 45 | - name: Lint and test 46 | run: | 47 | npm run lint 48 | npm t 49 | -------------------------------------------------------------------------------- /.github/workflows/build-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Build Ruby 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'ruby/**' 7 | branches: 8 | - master 9 | pull_request: 10 | paths: 11 | - 'ruby/**' 12 | branches: 13 | - '*' 14 | 15 | jobs: 16 | build: 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest] 20 | ruby: ['2.7', '3.2'] 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | bundler-cache: true 28 | working-directory: ./ruby 29 | - run: bundle exec rake 30 | working-directory: ./ruby 31 | env: 32 | RUBYOPT: "W:deprecated" 33 | -------------------------------------------------------------------------------- /.github/workflows/release-js.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v2.1.3 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - name: Install dependencies 17 | run: | 18 | npm ci 19 | - name: Lint, build and test 20 | run: | 21 | npm run lint 22 | npm t 23 | publish: 24 | needs: build 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 16 31 | # install dependencies 32 | - name: Install dependencies 33 | run: | 34 | npm ci 35 | # package and publish 36 | - name: Package and publish 37 | env: 38 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | run: | 40 | ./tasks/package.sh 41 | ./tasks/publish.sh 42 | # create the GitHub release 43 | - name: Create release 44 | id: create_release 45 | uses: actions/create-release@v1.1.4 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | tag_name: ${{ github.ref }} 50 | release_name: "🐙 JavaScript - ${{ github.ref }}" 51 | draft: false 52 | prerelease: false 53 | # upload assets 54 | - name: Upload source code as a zip file 55 | uses: actions/upload-release-asset@v1.0.2 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | upload_url: ${{ steps.create_release.outputs.upload_url }} 60 | asset_path: bin/asciidoctor-kroki.dist.zip 61 | asset_name: asciidoctor-kroki.dist.zip 62 | asset_content_type: application/zip 63 | - name: Upload source code as a tar.gz file 64 | uses: actions/upload-release-asset@v1.0.2 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | with: 68 | upload_url: ${{ steps.create_release.outputs.upload_url }} 69 | asset_path: bin/asciidoctor-kroki.dist.tar.gz 70 | asset_name: asciidoctor-kroki.dist.tar.gz 71 | asset_content_type: application/tar+gzip 72 | -------------------------------------------------------------------------------- /.github/workflows/release-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Release Ruby 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'ruby-v*' # Push events to matching ruby-v*, i.e. ruby-v1.0, ruby-v2.1.3 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: 2.7 16 | bundler-cache: true 17 | working-directory: ./ruby 18 | - run: bundle install 19 | working-directory: ./ruby 20 | - run: bundle exec rake 21 | working-directory: ./ruby 22 | publish: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: 2.7 30 | bundler-cache: true 31 | working-directory: ./ruby 32 | - name: Install and test 33 | run: | 34 | bundle install 35 | bundle exec rake 36 | working-directory: ./ruby 37 | - name: Configure credentials 38 | run: | 39 | mkdir -p $HOME/.gem 40 | touch $HOME/.gem/credentials 41 | chmod 0600 $HOME/.gem/credentials 42 | printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}" > $HOME/.gem/credentials 43 | env: 44 | RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }} 45 | - name: Build gem 46 | run: | 47 | bundle exec rake build 48 | working-directory: ./ruby 49 | - name: Publish to rubygems.org 50 | run: | 51 | gem push pkg/asciidoctor-kroki-${GITHUB_REF#refs/tags/ruby-v}.gem 52 | working-directory: ./ruby 53 | # create the GitHub release 54 | - name: Create release 55 | id: create_release 56 | uses: actions/create-release@v1.1.4 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | tag_name: ${{ github.ref }} 61 | release_name: "💎 Ruby - ${{ github.ref }}" 62 | draft: false 63 | prerelease: false 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .asciidoctor/kroki 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter-option": ["maxDiffSize=1048576"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Mocha All", 8 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 9 | "args": [ 10 | "--timeout", 11 | "999999", 12 | "--colors", 13 | "${workspaceFolder}/test" 14 | ], 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen" 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Mocha Current File", 22 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 23 | "args": [ 24 | "--timeout", 25 | "999999", 26 | "--colors", 27 | "${file}" 28 | ], 29 | "console": "integratedTerminal", 30 | "internalConsoleOptions": "neverOpen" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.detectIndentation": false, 3 | "editor.tabSize": 2, 4 | "javascript.format.insertSpaceBeforeFunctionParenthesis": true, 5 | "javascript.format.semicolons": "remove", 6 | "javascript.preferences.quoteStyle": "single", 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Guillaume Grossetie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🖍 Asciidoctor Kroki Extension 2 | 3 | [![Build JavaScript](https://github.com/ggrossetie/asciidoctor-kroki/actions/workflows/build-js.yml/badge.svg)](https://github.com/ggrossetie/asciidoctor-kroki/actions/workflows/build-js.yml) 4 | [![Build JavaScript](https://github.com/ggrossetie/asciidoctor-kroki/actions/workflows/build-ruby.yml/badge.svg)](https://github.com/ggrossetie/asciidoctor-kroki/actions/workflows/build-ruby.yml) 5 | [![npm version](http://img.shields.io/npm/v/asciidoctor-kroki.svg)](https://www.npmjs.com/package/asciidoctor-kroki) 6 | [![Gem version](https://img.shields.io/gem/v/asciidoctor-kroki)](https://rubygems.org/gems/asciidoctor-kroki) 7 | [![Zulip Chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://kroki.zulipchat.com/) 8 | 9 | An extension for [Asciidoctor.js](https://github.com/asciidoctor/asciidoctor.js) to convert diagrams to images using [Kroki](https://kroki.io)! 10 | 11 | * [Install](#install) 12 | + [Node.js](#nodejs) 13 | + [Browser](#browser) 14 | + [Ruby](#ruby) 15 | + [Antora Integration](#antora-integration) 16 | * [Usage](#usage) 17 | + [Supported diagram types](#supported-diagram-types) 18 | * [Configuration](#configuration) 19 | * [Using Your Own Kroki](#using-your-own-kroki) 20 | * [Contributing](#contributing) 21 | + [Setup](#setup) 22 | + [Building](#building) 23 | 24 | ## Install 25 | 26 | ### Node.js 27 | 28 | Install the dependencies: 29 | 30 | $ npm i asciidoctor asciidoctor-kroki 31 | 32 | Create a file named `kroki.js` with following content and run it: 33 | 34 | ```javascript 35 | const asciidoctor = require('@asciidoctor/core')() 36 | const kroki = require('asciidoctor-kroki') 37 | 38 | const input = 'plantuml::hello.puml[svg,role=sequence]' 39 | 40 | kroki.register(asciidoctor.Extensions) // <1> 41 | console.log(asciidoctor.convert(input, { safe: 'safe' })) 42 | 43 | const registry = asciidoctor.Extensions.create() 44 | kroki.register(registry) // <2> 45 | console.log(asciidoctor.convert(input, { safe: 'safe', extension_registry: registry })) 46 | ``` 47 | **<1>** Register the extension in the global registry
48 | **<2>** Register the extension in a dedicated registry 49 | 50 | ### Browser 51 | 52 | Install the dependencies: 53 | 54 | $ npm i asciidoctor asciidoctor-kroki 55 | 56 | Create a file named `kroki.html` with the following content and open it in your browser: 57 | 58 | ```html 59 | 60 | 61 | Asciidoctor x Kroki 62 | 63 | 64 | 65 | 66 | 67 |
68 | 85 | 86 | 87 | ``` 88 | **<1>** Register the extension in a dedicated registry 89 | 90 | **❗ IMPORTANT:** 91 | If you want to reference a diagram file in a browser environment you will need to define the base directory using the `base_dir` option. 92 | In addition, you will also need to provide an implementation to read a binary file **synchronously** for a given path. 93 | You can find an implementation based on `XMLHttpRequest` in the source code: https://github.com/ggrossetie/asciidoctor-kroki/blob/9585b969014a1894d0c9fb76df10e1e8c66ce2b2/test/browser/test.js#L2-L34. 94 | Once `httpGet` is defined, here's how we should configure the extension: 95 | 96 | ```js 97 | const registry = asciidoctor.Extensions.create() 98 | AsciidoctorKroki.register(registry, { 99 | vfs: { 100 | read: (path, encoding = 'utf8') => httpGet(path, encoding), 101 | exists: (_) => false, 102 | add: (_) => { /* no-op */ } 103 | } 104 | }) 105 | const input = 'plantuml::hello.puml[svg,role=sequence]' 106 | asciidoctor.convert(input, { safe: 'safe', base_dir: window.location.origin, extension_registry: registry }) 107 | ``` 108 | 109 | ### Ruby 110 | 111 | Install the dependency: 112 | 113 | $ gem install asciidoctor-kroki 114 | 115 | Require the library using the `--require` (or `-r`) option from the Asciidoctor CLI: 116 | 117 | $ asciidoctor -r asciidoctor-kroki doc.adoc 118 | 119 | ### Antora Integration 120 | 121 | If you are using [Antora](https://antora.org/), you can integrate Kroki in your documentation site. 122 | 123 | 1. Install the extension in your playbook project: 124 | 125 | $ npm i asciidoctor-kroki 126 | 127 | 2. Register the extension in your playbook file: 128 | 129 | ```yaml 130 | asciidoc: 131 | extensions: 132 | - asciidoctor-kroki 133 | ``` 134 | 135 | https://docs.antora.org/antora/2.3/playbook/configure-asciidoc/#extensions 136 | 137 | 3. Enjoy! 138 | 139 | **💡 TIP**: 140 | You can use the `kroki-fetch-diagram` option to download the images from Kroki at build time. 141 | In other words, while viewing pages you won't rely on Kroki anymore. 142 | However, in Antora, this is not currently compatible with inline SVG images. 143 | 144 | ```yaml 145 | asciidoc: 146 | attributes: 147 | kroki-fetch-diagram: true 148 | ``` 149 | 150 | ## Usage 151 | 152 | In your AsciiDoc document, you can either write your diagram inline or, alternatively, you can make a reference to the diagram file using macro form or with the `include` directive. 153 | 154 | Here's an example where we declare a GraphViz diagram directly in our AsciiDoc document using the block syntax: 155 | 156 | ```adoc 157 | [graphviz] 158 | .... 159 | digraph foo { 160 | node [style=rounded] 161 | node1 [shape=box] 162 | node2 [fillcolor=yellow, style="rounded,filled", shape=diamond] 163 | node3 [shape=record, label="{ a | b | c }"] 164 | 165 | node1 -> node2 -> node3 166 | } 167 | .... 168 | ``` 169 | 170 | ![GraphViz diagram](https://kroki.io/graphviz/png/eNo9jjEOwjAMRfee4itzGKBzuEjVIaldWsnEVQBBVXp3AqQdLFlP_32bxkvy04BeFUsFRCVGc7vPwi7pIxJTW_Ax88FP7IK-NnZC048inYomN7OIPi3-tim6_QaYTOY_m0Z_1bi31ltr4k4TWYgPLM4s8Hgj5Omwmrbanzicy-Wy1NX6AUS2QVQ=) 171 | 172 | In the example below, we are using the `vegalite` macro to reference a file named *chart.vlite*: 173 | 174 | ``` 175 | vegalite::chart.vlite[svg,role=chart,opts=interactive] 176 | ``` 177 | 178 | ![Vega-Lite chart diagram](https://kroki.io/vegalite/png/eNrtVktz2yAQvvtXMEyOqt9pnNz6To-d6c3jA5ZWEg0CF7Ba26P_3gVb2JJSN8mhTWdyMIb92CffCnY9QuiFiXMoGL0hNLd2ZW4GgxIy1s-4zdfLPleD_QYvfSW4hUE57X8zStLI6SdgYs1XlqMAbdwqzbdKWibEhsRKxsyCxF9C4pxpa4jNmSUmVz9IwtMUNEhL7GYFhqgURWgMLN9ymRETMwGmf3DDrItxh3NclUysweB67teE7KjP4A2NCF3ibDyroib0toYuL9vQuxqaTtrQ-xq6HrWhDzU060Afg6-OwU81NLpuQ7fB4FUb-hwMjiuPLHD0m2i-L3Koxe6gSQum75xuzHUsgNYWKchYJVjfUE0v3TSWKEg5iMTpL4Oql7uzcmKpCi6ZaIJGaReJXAvRkLOf3LQcOFM8vnPilAkDURNLVMG4_A1ouRVw8HOCVGFeHRWo4Vt4bHLf10yiE2Z5Ca0MHSnvSaWhiA7_GFashNJ_P65WJbegFeJWr-E04oZpARnI5L7j258C_XI-6d7p_8H0C0v_PUtFhw2aycxtmM-GERm9xmE8xWEyxmE6HC6eJam7afgLy-8oWIZX26OZnSpd-E8qTWh0lvTihfT_C-ltrgHfHaJzpCGf-QR5fjVcnOuK8XDfEM-tF56c3bFZSq45PsDo0y-CryGIhzQFjj4YikpKlMfkOrmGWlIuE1hhEPhqPLbNgUYNMLioelXvF-H7eDo=) 179 | 180 | 181 | Finally, we can use the `include` directive to reference a diagram file: 182 | 183 | ``` 184 | [plantuml,alice-bob,svg,role=sequence] 185 | .... 186 | include::alice-bob.puml[] 187 | .... 188 | ``` 189 | 190 | ![PlantUML diagram](https://kroki.io/plantuml/png/eNpzKC5JLCopzc3hSszJTE5V0LVTSMpP4nJIzUsBCgIApPUKcg==) 191 | 192 | ### References and includes with Antora 193 | 194 | If you use this Asciidoctor Kroki Extension in combination with Antora, all references and includes MUST use [Antora Resource IDs](https://docs.antora.org/antora/latest/page/resource-id/). The `.puml`-files are best placed in the [_partials_-directory](https://docs.antora.org/antora/latest/page/partials/) of the modules. 195 | 196 | #### Block macros 197 | 198 | ```adoc 199 | vegalite::partial$chart.vlite[svg,role=chart,opts=interactive] 200 | ``` 201 | #### Includes 202 | 203 | ```adoc 204 | [plantuml,alice-bob,svg,role=sequence] 205 | .... 206 | include::partial$alice-bob.puml[] 207 | .... 208 | ``` 209 | 210 | 211 | ### Syntax 212 | 213 | You can declare positional and named attributes when using the block or the macro form. 214 | 215 | **Positional attributes** 216 | 217 | When using the block form: 218 | 219 | 1. The first positional attribute specifies the diagram type (see below for a complete list). 220 | 2. The second optional positional attribute assigns a file name (i.e. target) to the generated diagram. *Currently, the value of this attribute is ignored, and an auto-generated hash will be used as file name for caching purposes (see #48)*. 221 | 3. The third optional positional attribute specifies the image format. 222 | 223 | Example: 224 | 225 | ``` 226 | [mermaid,abcd-flowchart,svg] 227 | .... 228 | graph TD; 229 | A-->B; 230 | A-->C; 231 | B-->D; 232 | C-->D; 233 | .... 234 | ``` 235 | 236 | In the above example, 237 | the diagram type is `mermaid`, 238 | the file name (i.e. target) is `abcd-flowchart`, 239 | and the image format is `svg`. 240 | 241 | When using the macro form: 242 | 243 | 1. The first optional positional attribute specifies the image format. 244 | 245 | Example: 246 | 247 | ``` 248 | vegalite::chart.vlite[svg] 249 | ``` 250 | 251 | In the above example, 252 | the diagram type is `vegalite`, 253 | the target is `chart.vlite`, 254 | and the image format is `svg`. 255 | 256 | **Named attributes** 257 | 258 | You can also use both positional and named attributes. Here's an example: 259 | 260 | ```adoc 261 | .PlantUML example 262 | [plantuml#diagAliceBob,alice-and-bob,svg,role=sequence] 263 | .... 264 | alice -> bob 265 | .... 266 | ``` 267 | 268 | As you can see, we specified an id using the syntax `#diagAliceBob` just after the diagram type, and we are also using a named attribute to assign a role using `role=sequence`. 269 | 270 | Here's another example using the macro form: 271 | 272 | ``` 273 | vegalite::chart.vlite[svg,role=chart,opts=interactive] 274 | ``` 275 | 276 | We are using a positional attribute to declare the image format and two named attributes to define the `role` and `options`. 277 | 278 | **Attributes** 279 | 280 | It's important to note that not all attributes are used in all converters. 281 | Here's a list of all attributes used in the built-in HTML 5 converter: 282 | 283 | - `target` 284 | - `width` 285 | - `height` 286 | - `format` (default `svg`) 287 | - `fallback` 288 | - `link` 289 | - `float` 290 | - `align` 291 | - `role` 292 | - `title` (used to define an alternative text description of the diagram) 293 | 294 | **Options** 295 | 296 | In addition, the following options are available when using the SVG format: 297 | 298 | - `inline` 299 | - `interactive` 300 | - `none` (used for cancelling defaults) 301 | 302 | Options can be defined using `options` attribute (or `opts` for short): 303 | 304 | ```adoc 305 | [blockdiag,opts=inline] 306 | .... 307 | blockdiag { 308 | Kroki -> generates -> "Block diagrams"; 309 | 310 | Kroki [color = "greenyellow"]; 311 | "Block diagrams" [color = "pink"]; 312 | } 313 | .... 314 | ``` 315 | 316 | ### Supported diagram types 317 | 318 | Kroki currently supports the following diagram libraries: 319 | 320 | * [ActDiag](https://github.com/blockdiag/actdiag): `actdiag` 321 | * [BlockDiag](https://github.com/blockdiag/blockdiag): `blockdiag` 322 | * [BPMN](https://github.com/bpmn-io/bpmn-js): `bpmn` 323 | * [Bytefield](https://github.com/Deep-Symmetry/bytefield-svg/): `bytefield` 324 | * [C4 (PlantUML)](https://github.com/RicardoNiepel/C4-PlantUML): `c4plantuml` 325 | * [D2](https://d2lang.com/tour/intro/): `d2` 326 | * [DBML](https://www.dbml.org/home/): `dbml` 327 | * [Ditaa](http://ditaa.sourceforge.net): `ditaa` 328 | * [ERD](https://github.com/BurntSushi/erd): `erd` 329 | * [Excalidraw](https://github.com/excalidraw/excalidraw): `excalidraw` 330 | * [GraphViz](https://www.graphviz.org/): `graphviz` 331 | * [Mermaid](https://github.com/knsv/mermaid): `mermaid` 332 | * [Nomnoml](https://github.com/skanaar/nomnoml): `nomnoml` 333 | * [NwDiag](https://github.com/blockdiag/nwdiag): `nwdiag` 334 | * [PacketDiag](https://github.com/blockdiag/nwdiag): `packetdiag` 335 | * [Pikchr](https://github.com/drhsqlite/pikchr): `pikchr` 336 | * [PlantUML](https://github.com/plantuml/plantuml): `plantuml` 337 | * [RackDiag](https://github.com/blockdiag/nwdiag): `rackdiag` 338 | * [SeqDiag](https://github.com/blockdiag/seqdiag): `seqdiag` 339 | * [SVGBob](https://github.com/ivanceras/svgbob): `svgbob` 340 | * [Symbolator](https://github.com/zebreus/symbolator): `symbolator` 341 | * [UMLet](https://github.com/umlet/umlet): `umlet` 342 | * [Vega](https://github.com/vega/vega): `vega` 343 | * [Vega-Lite](https://github.com/vega/vega-lite): `vegalite` 344 | * [WaveDrom](https://github.com/wavedrom/wavedrom): `wavedrom` 345 | * [Structurizr](https://github.com/structurizr/dsl): `structurizr` 346 | * [Diagrams.net](https://github.com/jgraph/drawio): `diagramsnet` _(only available via [Using Your Own Kroki](#using-your-own-kroki "Using Your Own Kroki"))_ 347 | 348 | Each diagram libraries support one or more output formats. 349 | Consult the [Kroki documentation](https://kroki.io/#support) to find out which formats are supported. 350 | 351 | ## Configuration 352 | 353 | | Attribute name | Description | Default value | 354 | |--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------| 355 | | `kroki-server-url` | The URL of the Kroki server (see "Using Your Own Kroki") | `https://kroki.io` | 356 | | `kroki-data-uri` | Embed images as data-uri elements in HTML elements so file is completely self-contained. | `false` | 357 | | `kroki-fetch-diagram` | Define if we should download (and save on the disk) the images from the Kroki server.
This feature is not available when running in the browser. | `false` | 358 | | `kroki-http-method` | Define how we should get the image from the Kroki server. Possible values:
| `adaptive` | 359 | | `kroki-plantuml-include` | A file that will be included at the top of all PlantUML diagrams as if `!include file` was used. This can be useful when you want to define a common skin for all your diagrams. The value can be a path or a URL. | | 360 | | `kroki-plantuml-include-paths` | Search path(s) that will be used to resolve `!include file` additionally to current diagram directory, similar to PlantUML property [plantuml.include.path](https://plantuml.com/de/preprocessing). Please use directory delimiter `;` (Windows) or `:` (Unix) for multiple paths, e.g.: `"c:/docu/styles;c:/docu/library"` or `"~/docu/styles:~/docu/library"` | | 361 | | `kroki-max-uri-length` | Define the max URI length before using a POST request when using `adaptive` HTTP method (`kroki-http-method`) | `4000` | 362 | 363 | **❗ IMPORTANT:** 364 | `kroki-fetch-diagram` and `kroki-plantuml-include` are only available when safe mode is `server` or lower. 365 | If you want to learn more about Asciidoctor safe modes: https://docs.asciidoctor.org/asciidoctor/latest/safe-modes/ 366 | 367 | ### Default configuration 368 | 369 | By default, images are generated as SVG when possible. 370 | To alter this, set the `kroki-default-format` attribute: 371 | 372 | ```adoc 373 | :kroki-default-format: png 374 | ``` 375 | 376 | You can unset this with `:kroki-default-format!:` or `:kroki-default-format: svg`. 377 | 378 | **ℹ️ NOTE:** 379 | An AsciiDoc attribute can be defined through the CLI or API, in the document’s header or in the document’s body. 380 | In addition, if you are using Antora, you can define AsciiDoc attributes in your playbook and/or in your component descriptor. 381 | 382 | References: 383 | 384 | - https://asciidoctor.org/docs/user-manual/#setting-attributes-on-a-document 385 | - https://docs.antora.org/antora/2.3/page/attributes/#custom-attributes 386 | 387 | For instance, in an Antora playbook or component descriptor: 388 | 389 | ```yaml 390 | asciidoc: 391 | attributes: 392 | kroki-default-format: png@ 393 | ``` 394 | 395 | (the `@` allows the attribute value to be reset in documents) 396 | 397 | By default, Asciidoctor Kroki generates a link, to a Kroki server or a local file. 398 | To change the default for SVG diagrams, set the `kroki-default-options` attribute. 399 | 400 | ```adoc 401 | :kroki-default-options: inline 402 | ``` 403 | 404 | You can unset this with `:kroki-default-options: none` or `:kroki-default-options!:` or specify `opts=none` in a block or macro. 405 | 406 | ## Preprocessing 407 | 408 | Some diagram libraries allow referencing external entities by URL or accessing resources from the filesystem. 409 | For example PlantUML allows the `!import` directive to pull fragments from the filesystem or a remote URL or the standard library. 410 | Similarly, Vega-Lite can load data from a URL using the `url` property. 411 | 412 | By default, the Kroki server is running in `SECURE` mode which restrict access to files on the file system and on the network. 413 | 414 | For ease of use and convenience, Asciidoctor Kroki will try to resolve and load external resources before sending a request to the Kroki server. 415 | This feature is only available when Asciidoctor safe mode is `server` or lower. 416 | 417 | ## Using Your Own Kroki 418 | 419 | By default, this extension sends information and receives diagrams back from https://kroki.io. 420 | 421 | You may choose to use your own server due to: 422 | 423 | * Network restrictions - if Kroki is not available behind your corporate firewall 424 | * Network latency - you are far from the European public instance 425 | * Privacy - you don't want to send your diagrams to a remote server on the internet 426 | 427 | This is done using the `kroki-server-url` attribute. 428 | Typically, this is at the top of the document (under the title): 429 | 430 | ```adoc 431 | :kroki-server-url: http://my-server-url:port 432 | ``` 433 | 434 | For instance, if you have followed [the instructions](https://docs.kroki.io/kroki/setup/install/#_using_docker) to set up a self-managed server using Docker you can use the following: 435 | 436 | ```adoc 437 | :kroki-server-url: http://localhost:8080 438 | ``` 439 | 440 | Note that either the `http://` or `https://` prefix _is_ required (the default Docker image only uses `http`). 441 | 442 | You can also set this attribute using the Javascript API, for instance: 443 | 444 | ```js 445 | asciidoctor.convertFile('file.adoc', { safe: 'safe', attributes: { 'kroki-server-url': 'http://my-server-url:port' } }) 446 | ``` 447 | 448 | ## Contributing 449 | 450 | ### Setup 451 | 452 | To build this project, you will need the latest active LTS of Node.js and npm (we recommend `nvm` to manage multiple active Node.js versions). 453 | 454 | The current latest Node LTS version is: `v14.15.x` 455 | 456 | Please use latest npm version `v7.x` to generate lockfile using v2 format (i.e., `"lockfileVersion": 2`), see [lockfileversion](https://docs.npmjs.com/cli/v7/configuring-npm/package-lock-json#lockfileversion) 457 | 458 | #### Update NPM @ Linux 459 | 460 | npm i -g npm 461 | 462 | #### Update NPM @ Windows 463 | 464 | 1. Open PowerShell as Administrator selecting `Run as Administrator` 465 | 466 | 2. Install `npm-windows-upgrade` 467 | 468 | Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force 469 | npm install --global --production npm-windows-upgrade 470 | 471 | 3. Upgrade npm 472 | 473 | npm-windows-upgrade 474 | 475 | Reference: [npm-windows-upgrade](https://github.com/felixrieseberg/npm-windows-upgrade) 476 | 477 | ### Building 478 | 479 | 1. Install the dependencies: 480 | 481 | $ npm i 482 | 483 | 2. Generate a distribution: 484 | 485 | $ npm run dist 486 | 487 | When working on a new feature or when fixing a bug, make sure to run the linter and the tests suite: 488 | 489 | $ npm run lint 490 | $ npm run test 491 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | How to perform a release. 4 | 5 | ## Ruby 6 | 7 | 1. Update the version number `VERSION` in `lib/asciidoctor/extensions/asciidoctor_kroki/version.rb` 8 | 2. Run `bundle exec rake` in the `ruby` directory to make sure that everything is working 9 | 3. Commit both `lib/asciidoctor/extensions/asciidoctor_kroki/version.rb` and `Gemfile.lock` files 10 | 4. Create a tag starting with `ruby-v` (eg. `ruby-v1.2.3`) 11 | 5. Push your changes with the tag: `git push origin master --tags` 12 | 13 | ## JavaScript 14 | 15 | 1. Run `npm version x.y.z` at the root of the repository 16 | 2. Push your changes with the tag: `git push origin master --tags` 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asciidoctor-kroki", 3 | "version": "0.18.1", 4 | "description": "Asciidoctor extension to convert diagrams to images using Kroki", 5 | "type": "commonjs", 6 | "main": "./src/asciidoctor-kroki.js", 7 | "exports": { 8 | "node": "./src/asciidoctor-kroki.js", 9 | "default": "./dist/browser/asciidoctor-kroki.js" 10 | }, 11 | "files": [ 12 | "src", 13 | "dist" 14 | ], 15 | "scripts": { 16 | "test": "npm run test:node && npm run test:browser && npm run test:antora", 17 | "test:node": "mocha test/**.spec.js", 18 | "test:browser": "node test/browser/run.js", 19 | "test:antora": "mocha test/antora/**.spec.js", 20 | "lint": "standard src/**.js test/**.js tasks/**.js", 21 | "lint-fix": "npm run lint -- --fix", 22 | "clean": "shx rm -rf dist/*", 23 | "dist": "npm run clean && npm run dist:browser", 24 | "dist:browser": "shx mkdir -p dist/browser && browserify src/asciidoctor-kroki.js --exclude ./node-fs.js --exclude ./fetch.js --exclude ./antora-adapter.js --standalone AsciidoctorKroki -o dist/browser/asciidoctor-kroki.js" 25 | }, 26 | "dependencies": { 27 | "json5": "2.2.3", 28 | "mkdirp": "2.1.3", 29 | "pako": "2.1.0", 30 | "rusha": "0.8.14", 31 | "unxhr": "1.2.0" 32 | }, 33 | "devDependencies": { 34 | "@antora/site-generator-default": "~3.1", 35 | "@asciidoctor/core": ">=2.2 <4.0", 36 | "base64-js": "1.5.1", 37 | "browserify": "17.0.0", 38 | "chai": "~4.3", 39 | "chai-string": "1.5.0", 40 | "cheerio": "1.0.0-rc.12", 41 | "dirty-chai": "2.0.1", 42 | "libnpmpublish": "4.0.2", 43 | "lodash": "4.17.21", 44 | "mocha": "10.2.0", 45 | "pacote": "12.0.2", 46 | "puppeteer": "~21.3", 47 | "shx": "0.3.4", 48 | "sinon": "~16.1", 49 | "standard": "17.1.0" 50 | }, 51 | "peerDependencies": { 52 | "@asciidoctor/core": ">=2.2 <4.0" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "https://github.com/Mogztter/asciidoctor-kroki.git" 57 | }, 58 | "keywords": [ 59 | "asciidoctor", 60 | "kroki", 61 | "diagrams", 62 | "javascript", 63 | "extension" 64 | ], 65 | "author": "Guillaume Grossetie (https://github.com/mogztter)", 66 | "license": "MIT", 67 | "bugs": { 68 | "url": "https://github.com/Mogztter/asciidoctor-kroki/issues" 69 | }, 70 | "homepage": "https://github.com/Mogztter/asciidoctor-kroki#readme", 71 | "publishConfig": { 72 | "access": "public" 73 | }, 74 | "engines": { 75 | "node": ">=10" 76 | }, 77 | "volta": { 78 | "node": "16.20.1" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "dependencyDashboard": true, 4 | "separateMajorMinor": false, 5 | "extends": [ 6 | ":preserveSemverRanges", 7 | "group:all", 8 | "schedule:monthly", 9 | ":maintainLockFilesMonthly" 10 | ], 11 | "packageRules": [ 12 | { 13 | // disable libnpmpublish and pacote automatic updates (too sensitive) 14 | "matchPackagePatterns": ["libnpmpublish", "pacote"], 15 | "enabled": false 16 | } 17 | ], 18 | "lockFileMaintenance": { 19 | "extends": [ 20 | "group:all" 21 | ], 22 | "commitMessageAction": "Update" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ruby/.asciidoctor/kroki/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asciidoctor/asciidoctor-kroki/602658c6c1fc0c50b33fd0116961ce6b12afabdc/ruby/.asciidoctor/kroki/.gitkeep -------------------------------------------------------------------------------- /ruby/.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | .asciidoctor/kroki 3 | -------------------------------------------------------------------------------- /ruby/.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.7 3 | SuggestExtensions: false 4 | NewCops: enable 5 | 6 | Style/Encoding: 7 | Enabled: false 8 | 9 | Layout/EndOfLine: 10 | EnforcedStyle: lf 11 | 12 | Layout/LineLength: 13 | Max: 180 14 | 15 | Metrics/ClassLength: 16 | Max: 150 17 | 18 | Metrics/MethodLength: 19 | Max: 50 20 | 21 | Metrics/CyclomaticComplexity: 22 | Max: 10 23 | 24 | Metrics/PerceivedComplexity: 25 | Max: 10 26 | 27 | Metrics/AbcSize: 28 | Max: 31 29 | 30 | Metrics/ParameterLists: 31 | Max: 7 32 | 33 | Gemspec/RequiredRubyVersion: 34 | Enabled: false 35 | -------------------------------------------------------------------------------- /ruby/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.0 2 | -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | asciidoctor-kroki (0.10.0) 5 | asciidoctor (~> 2.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | asciidoctor (2.0.22) 11 | ast (2.4.2) 12 | diff-lcs (1.4.4) 13 | parallel (1.22.1) 14 | parser (3.1.2.0) 15 | ast (~> 2.4.1) 16 | rainbow (3.1.1) 17 | rake (13.0.6) 18 | regexp_parser (2.5.0) 19 | rexml (3.2.5) 20 | rspec (3.10.0) 21 | rspec-core (~> 3.10.0) 22 | rspec-expectations (~> 3.10.0) 23 | rspec-mocks (~> 3.10.0) 24 | rspec-core (3.10.1) 25 | rspec-support (~> 3.10.0) 26 | rspec-expectations (3.10.1) 27 | diff-lcs (>= 1.2.0, < 2.0) 28 | rspec-support (~> 3.10.0) 29 | rspec-mocks (3.10.2) 30 | diff-lcs (>= 1.2.0, < 2.0) 31 | rspec-support (~> 3.10.0) 32 | rspec-support (3.10.2) 33 | rubocop (1.30.0) 34 | parallel (~> 1.10) 35 | parser (>= 3.1.0.0) 36 | rainbow (>= 2.2.2, < 4.0) 37 | regexp_parser (>= 1.8, < 3.0) 38 | rexml (>= 3.2.5, < 4.0) 39 | rubocop-ast (>= 1.18.0, < 2.0) 40 | ruby-progressbar (~> 1.7) 41 | unicode-display_width (>= 1.4.0, < 3.0) 42 | rubocop-ast (1.18.0) 43 | parser (>= 3.1.1.0) 44 | ruby-progressbar (1.11.0) 45 | unicode-display_width (2.1.0) 46 | 47 | PLATFORMS 48 | ruby 49 | 50 | DEPENDENCIES 51 | asciidoctor-kroki! 52 | rake (~> 13.0.6) 53 | rspec (~> 3.10.0) 54 | rubocop (~> 1.30) 55 | 56 | BUNDLED WITH 57 | 2.3.15 58 | -------------------------------------------------------------------------------- /ruby/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir.glob('tasks/*.rake').each { |file| load file } 4 | 5 | task default: %w[lint spec] 6 | -------------------------------------------------------------------------------- /ruby/asciidoctor-kroki.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/asciidoctor/extensions/asciidoctor_kroki/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'asciidoctor-kroki' 7 | s.version = Asciidoctor::AsciidoctorKroki::VERSION 8 | s.summary = 'Asciidoctor extension to convert diagrams to images using Kroki' 9 | s.description = 'An extension for Asciidoctor to convert diagrams to images using https://kroki.io' 10 | 11 | s.authors = ['Guillaume Grossetie'] 12 | s.email = ['ggrossetie@yuzutech.fr'] 13 | s.homepage = 'https://github.com/ggrossetie/asciidoctor-kroki' 14 | s.license = 'MIT' 15 | s.metadata = { 16 | 'bug_tracker_uri' => 'https://github.com/ggrossetie/asciidoctor-kroki/issues', 17 | 'source_code_uri' => 'https://github.com/ggrossetie/asciidoctor-kroki', 18 | 'rubygems_mfa_required' => 'true' 19 | } 20 | s.files = `git ls-files`.split($RS) 21 | s.require_paths = ['lib'] 22 | 23 | s.add_runtime_dependency 'asciidoctor', '~> 2.0' 24 | 25 | s.add_development_dependency 'rake', '~> 13.0.6' 26 | s.add_development_dependency 'rspec', '~> 3.10.0' 27 | s.add_development_dependency 'rubocop', '~> 1.30' 28 | end 29 | -------------------------------------------------------------------------------- /ruby/lib/asciidoctor-kroki.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Naming/FileName 2 | # rubocop:enable Naming/FileName 3 | # frozen_string_literal: true 4 | 5 | require_relative 'asciidoctor/extensions/asciidoctor_kroki' 6 | -------------------------------------------------------------------------------- /ruby/lib/asciidoctor/extensions/asciidoctor_kroki.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' 4 | require_relative 'asciidoctor_kroki/version' 5 | require_relative 'asciidoctor_kroki/extension' 6 | 7 | Asciidoctor::Extensions.register do 8 | ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.each do |name| 9 | block_macro ::AsciidoctorExtensions::KrokiBlockMacroProcessor, name 10 | block ::AsciidoctorExtensions::KrokiBlockProcessor, name 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /ruby/lib/asciidoctor/extensions/asciidoctor_kroki/extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cgi' 4 | require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' 5 | 6 | # Asciidoctor extensions 7 | # 8 | module AsciidoctorExtensions 9 | include Asciidoctor 10 | 11 | # A block extension that converts a diagram into an image. 12 | # 13 | class KrokiBlockProcessor < Extensions::BlockProcessor 14 | use_dsl 15 | 16 | on_context :listing, :literal 17 | name_positional_attributes 'target', 'format' 18 | 19 | # @param name [String] name of the block macro (optional) 20 | # @param config [Hash] a config hash (optional) 21 | # - :logger a logger used to log warning and errors (optional) 22 | # 23 | def initialize(name = nil, config = {}) 24 | @logger = (config || {}).delete(:logger) { ::Asciidoctor::LoggerManager.logger } 25 | super(name, config) 26 | end 27 | 28 | def process(parent, reader, attrs) 29 | diagram_type = @name 30 | diagram_text = reader.string 31 | KrokiProcessor.process(self, parent, attrs, diagram_type, diagram_text, @logger) 32 | end 33 | 34 | protected 35 | 36 | attr_reader :logger 37 | end 38 | 39 | # A block macro extension that converts a diagram into an image. 40 | # 41 | class KrokiBlockMacroProcessor < Asciidoctor::Extensions::BlockMacroProcessor 42 | include Asciidoctor::Logging 43 | use_dsl 44 | 45 | name_positional_attributes 'format' 46 | 47 | # @param name [String] name of the block macro (optional) 48 | # @param config [Hash] a config hash (optional) 49 | # - :logger a logger used to log warning and errors (optional) 50 | # 51 | def initialize(name = nil, config = {}) 52 | @logger = (config || {}).delete(:logger) { ::Asciidoctor::LoggerManager.logger } 53 | super(name, config) 54 | end 55 | 56 | # Processes the diagram block or block macro by converting it into an image or literal block. 57 | # 58 | # @param parent [Asciidoctor::AbstractBlock] the parent asciidoc block of the block or block macro being processed 59 | # @param target [String] the target value of a block macro 60 | # @param attrs [Hash] the attributes of the block or block macro 61 | # @return [Asciidoctor::AbstractBlock] a new block that replaces the original block or block macro 62 | def process(parent, target, attrs) 63 | diagram_type = @name 64 | target = parent.apply_subs(target, [:attributes]) 65 | 66 | unless read_allowed?(target) 67 | link = create_inline(parent, :anchor, target, type: :link, target: target) 68 | return create_block(parent, :paragraph, link.convert, {}, content_model: :raw) 69 | end 70 | 71 | unless (path = resolve_target_path(parent, target)) 72 | logger.error message_with_context "#{diagram_type} block macro not found: #{target}.", source_location: parent.document.reader.cursor_at_mark 73 | return create_block(parent, 'paragraph', unresolved_block_macro_message(diagram_type, target), {}) 74 | end 75 | 76 | begin 77 | diagram_text = read(path) 78 | rescue => e # rubocop:disable Style/RescueStandardError 79 | logger.error message_with_context "Failed to read #{diagram_type} file: #{path}. #{e}.", source_location: parent.document.reader.cursor_at_mark 80 | return create_block(parent, 'paragraph', unresolved_block_macro_message(diagram_type, path), {}) 81 | end 82 | KrokiProcessor.process(self, parent, attrs, diagram_type, diagram_text, @logger) 83 | end 84 | 85 | protected 86 | 87 | attr_reader :logger 88 | 89 | # @param parent [Asciidoctor::AbstractBlock] the parent asciidoc block of the block or block macro being processed 90 | # @param target [String] the target value of a block macro 91 | def resolve_target_path(parent, target) 92 | parent.normalize_system_path(target) 93 | end 94 | 95 | def read_allowed?(_target) 96 | true 97 | end 98 | 99 | def read(target) 100 | if target.start_with?('http://') || target.start_with?('https://') 101 | require 'open-uri' 102 | ::OpenURI.open_uri(target, &:read) 103 | else 104 | File.read(target, mode: 'rb:utf-8:utf-8') 105 | end 106 | end 107 | 108 | def unresolved_block_macro_message(name, target) 109 | "Unresolved block macro - #{name}::#{target}[]" 110 | end 111 | end 112 | 113 | # Kroki API 114 | # 115 | module Kroki 116 | SUPPORTED_DIAGRAM_NAMES = %w[ 117 | actdiag 118 | blockdiag 119 | bpmn 120 | bytefield 121 | c4plantuml 122 | d2 123 | dbml 124 | ditaa 125 | erd 126 | excalidraw 127 | graphviz 128 | mermaid 129 | nomnoml 130 | nwdiag 131 | packetdiag 132 | pikchr 133 | plantuml 134 | rackdiag 135 | seqdiag 136 | svgbob 137 | symbolator 138 | umlet 139 | vega 140 | vegalite 141 | wavedrom 142 | structurizr 143 | diagramsnet 144 | wireviz 145 | ].freeze 146 | end 147 | 148 | # Internal processor 149 | # 150 | class KrokiProcessor 151 | include Asciidoctor::Logging 152 | 153 | TEXT_FORMATS = %w[txt atxt utxt].freeze 154 | BUILTIN_ATTRIBUTES = %w[target width height format fallback link float align role caption title cloaked-context subs].freeze 155 | 156 | class << self 157 | # rubocop:disable Metrics/AbcSize 158 | def process(processor, parent, attrs, diagram_type, diagram_text, logger) 159 | doc = parent.document 160 | diagram_text = prepend_plantuml_config(diagram_text, diagram_type, doc, logger) 161 | # If "subs" attribute is specified, substitute accordingly. 162 | # Be careful not to specify "specialcharacters" or your diagram code won't be valid anymore! 163 | if (subs = attrs['subs']) 164 | diagram_text = parent.apply_subs(diagram_text, parent.resolve_subs(subs)) 165 | end 166 | attrs.delete('opts') 167 | format = get_format(doc, attrs, diagram_type) 168 | attrs['role'] = get_role(format, attrs['role']) 169 | attrs['format'] = format 170 | opts = attrs.filter { |key, _| key.is_a?(String) && BUILTIN_ATTRIBUTES.none? { |k| key == k } && !key.end_with?('-option') } 171 | kroki_diagram = KrokiDiagram.new(diagram_type, format, diagram_text, attrs['target'], opts) 172 | kroki_client = KrokiClient.new({ 173 | server_url: server_url(doc), 174 | http_method: http_method(doc), 175 | max_uri_length: max_uri_length(doc), 176 | source_location: doc.reader.cursor_at_mark, 177 | http_client: KrokiHttpClient 178 | }, logger) 179 | alt = get_alt(attrs) 180 | title = attrs.delete('title') 181 | caption = attrs.delete('caption') 182 | if TEXT_FORMATS.include?(format) 183 | text_content = kroki_client.text_content(kroki_diagram) 184 | block = processor.create_block(parent, 'literal', text_content, attrs) 185 | else 186 | attrs['alt'] = alt 187 | attrs['target'] = create_image_src(doc, kroki_diagram, kroki_client) 188 | block = processor.create_image_block(parent, attrs) 189 | end 190 | block.title = title if title 191 | block.assign_caption(caption, 'figure') 192 | block 193 | end 194 | # rubocop:enable Metrics/AbcSize 195 | 196 | private 197 | 198 | def prepend_plantuml_config(diagram_text, diagram_type, doc, logger) 199 | if diagram_type == :plantuml && doc.safe < ::Asciidoctor::SafeMode::SECURE && doc.attr?('kroki-plantuml-include') 200 | # REMIND: this behaves different than the JS version 201 | # Once we have a preprocessor for Ruby, the value should be added in the diagram source as "!include #{plantuml_include}" 202 | plantuml_include_path = doc.normalize_system_path(doc.attr('kroki-plantuml-include')) 203 | if ::File.readable? plantuml_include_path 204 | config = File.read(plantuml_include_path) 205 | diagram_text = "#{config}\n#{diagram_text}" 206 | else 207 | logger.warn message_with_context "Unable to read plantuml-include. File not found or not readable: #{plantuml_include_path}.", 208 | source_location: doc.reader.cursor_at_mark 209 | end 210 | end 211 | diagram_text 212 | end 213 | 214 | def get_alt(attrs) 215 | if (title = attrs['title']) 216 | title 217 | elsif (target = attrs['target']) 218 | target 219 | else 220 | 'Diagram' 221 | end 222 | end 223 | 224 | def get_role(format, role) 225 | if role 226 | if format 227 | "#{role} kroki-format-#{format} kroki" 228 | else 229 | "#{role} kroki" 230 | end 231 | else 232 | 'kroki' 233 | end 234 | end 235 | 236 | def get_format(doc, attrs, diagram_type) 237 | format = attrs['format'] || doc.attr('kroki-default-format') || 'svg' 238 | if format == 'png' 239 | # redirect PNG format to SVG if the diagram library only supports SVG as output format. 240 | # this is useful when the default format has been set to PNG 241 | # Currently, nomnoml, svgbob, wavedrom only support SVG as output format. 242 | svg_only_diagram_types = %i[nomnoml svgbob wavedrom] 243 | format = 'svg' if svg_only_diagram_types.include?(diagram_type) 244 | end 245 | format 246 | end 247 | 248 | def create_image_src(doc, kroki_diagram, kroki_client) 249 | if doc.attr('kroki-fetch-diagram') && doc.safe < ::Asciidoctor::SafeMode::SECURE 250 | kroki_diagram.save(output_dir_path(doc), kroki_client) 251 | else 252 | kroki_diagram.get_diagram_uri(server_url(doc)) 253 | end 254 | end 255 | 256 | def server_url(doc) 257 | doc.attr('kroki-server-url', 'https://kroki.io') 258 | end 259 | 260 | def http_method(doc) 261 | doc.attr('kroki-http-method', 'adaptive').downcase 262 | end 263 | 264 | def max_uri_length(doc) 265 | doc.attr('kroki-max-uri-length', '4000').to_i 266 | end 267 | 268 | def output_dir_path(doc) 269 | images_dir = doc.attr('imagesdir', '') 270 | if (images_output_dir = doc.attr('imagesoutdir')) 271 | images_output_dir 272 | # the nested document logic will become obsolete once https://github.com/asciidoctor/asciidoctor/commit/7edc9da023522be67b17e2a085d72e056703a438 is released 273 | elsif (out_dir = doc.attr('outdir') || (doc.nested? ? doc.parent_document : doc).options[:to_dir]) 274 | File.join(out_dir, images_dir) 275 | else 276 | File.join(doc.base_dir, images_dir) 277 | end 278 | end 279 | end 280 | end 281 | 282 | # Kroki diagram 283 | # 284 | class KrokiDiagram 285 | require 'fileutils' 286 | require 'zlib' 287 | require 'digest' 288 | 289 | attr_reader :type, :text, :format, :target, :opts 290 | 291 | def initialize(type, format, text, target = nil, opts = {}) 292 | @text = text 293 | @type = type 294 | @format = format 295 | @target = target 296 | @opts = opts 297 | end 298 | 299 | def get_diagram_uri(server_url) 300 | query_params = opts.map { |k, v| "#{k}=#{_url_encode(v.to_s)}" }.join('&') unless opts.empty? 301 | _join_uri_segments(server_url, @type, @format, encode) + (query_params ? "?#{query_params}" : '') 302 | end 303 | 304 | def encode 305 | ([Zlib::Deflate.deflate(@text, 9)].pack 'm0').tr '+/', '-_' 306 | end 307 | 308 | def save(output_dir_path, kroki_client) 309 | diagram_url = get_diagram_uri(kroki_client.server_url) 310 | diagram_name = "#{@target || 'diag'}-#{Digest::SHA256.hexdigest diagram_url}.#{@format}" 311 | file_path = File.join(output_dir_path, diagram_name) 312 | encoding = case @format 313 | when 'txt', 'atxt', 'utxt', 'svg' 314 | 'utf8' 315 | else 316 | 'binary' 317 | end 318 | # file is either (already) on the file system or we should read it from Kroki 319 | unless File.exist?(file_path) 320 | contents = kroki_client.get_image(self, encoding) 321 | FileUtils.mkdir_p(output_dir_path) 322 | File.write(file_path, contents, mode: 'wb') 323 | end 324 | 325 | diagram_name 326 | end 327 | 328 | private 329 | 330 | def _url_encode(text) 331 | CGI.escape(text).gsub(/\+/, '%20') 332 | end 333 | 334 | def _join_uri_segments(base, *uris) 335 | segments = [] 336 | # remove trailing slashes 337 | segments.push(base.gsub(%r{/+$}, '')) 338 | segments.concat(uris.map do |uri| 339 | # remove leading and trailing slashes 340 | uri.to_s 341 | .gsub(%r{^/+}, '') 342 | .gsub(%r{/+$}, '') 343 | end) 344 | segments.join('/') 345 | end 346 | end 347 | 348 | # Kroki client 349 | # 350 | class KrokiClient 351 | include Asciidoctor::Logging 352 | 353 | attr_reader :server_url, :method, :max_uri_length 354 | 355 | SUPPORTED_HTTP_METHODS = %w[get post adaptive].freeze 356 | 357 | def initialize(opts, logger = ::Asciidoctor::LoggerManager.logger) 358 | @server_url = opts[:server_url] 359 | @max_uri_length = opts.fetch(:max_uri_length, 4000) 360 | @http_client = opts[:http_client] 361 | method = opts.fetch(:http_method, 'adaptive').downcase 362 | if SUPPORTED_HTTP_METHODS.include?(method) 363 | @method = method 364 | else 365 | logger.warn message_with_context "Invalid value '#{method}' for kroki-http-method attribute. The value must be either: " \ 366 | "'get', 'post' or 'adaptive'. Proceeding using: 'adaptive'.", 367 | source_location: opts[:source_location] 368 | @method = 'adaptive' 369 | end 370 | end 371 | 372 | def text_content(kroki_diagram) 373 | get_image(kroki_diagram, 'utf-8') 374 | end 375 | 376 | def get_image(kroki_diagram, encoding) 377 | type = kroki_diagram.type 378 | format = kroki_diagram.format 379 | text = kroki_diagram.text 380 | opts = kroki_diagram.opts 381 | if @method == 'adaptive' || @method == 'get' 382 | uri = kroki_diagram.get_diagram_uri(server_url) 383 | if uri.length > @max_uri_length 384 | # The request URI is longer than the max URI length. 385 | if @method == 'get' 386 | # The request might be rejected by the server with a 414 Request-URI Too Large. 387 | # Consider using the attribute kroki-http-method with the value 'adaptive'. 388 | @http_client.get(uri, opts, encoding) 389 | else 390 | @http_client.post("#{@server_url}/#{type}/#{format}", text, opts, encoding) 391 | end 392 | else 393 | @http_client.get(uri, opts, encoding) 394 | end 395 | else 396 | @http_client.post("#{@server_url}/#{type}/#{format}", text, opts, encoding) 397 | end 398 | end 399 | end 400 | 401 | # Kroki HTTP client 402 | # 403 | class KrokiHttpClient 404 | require 'net/http' 405 | require 'uri' 406 | require 'json' 407 | 408 | class << self 409 | REFERER = "asciidoctor/kroki.rb/#{Asciidoctor::AsciidoctorKroki::VERSION}" 410 | 411 | def get(uri, opts, _) 412 | uri = URI(uri) 413 | headers = opts.transform_keys { |key| "Kroki-Diagram-Options-#{key}" } 414 | .merge({ 'referer' => REFERER }) 415 | request = ::Net::HTTP::Get.new(uri, headers) 416 | ::Net::HTTP.start( 417 | uri.hostname, 418 | uri.port, 419 | use_ssl: (uri.scheme == 'https') 420 | ) do |http| 421 | http.request(request).body 422 | end 423 | end 424 | 425 | def post(uri, data, opts, _) 426 | headers = opts.transform_keys { |key| "Kroki-Diagram-Options-#{key}" } 427 | .merge({ 428 | 'Content-Type' => 'text/plain', 429 | 'referer' => REFERER 430 | }) 431 | res = ::Net::HTTP.post( 432 | URI(uri), 433 | data, 434 | headers 435 | ) 436 | res.body 437 | end 438 | end 439 | end 440 | end 441 | -------------------------------------------------------------------------------- /ruby/lib/asciidoctor/extensions/asciidoctor_kroki/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Asciidoctor 4 | module AsciidoctorKroki 5 | VERSION = '0.10.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /ruby/spec/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - ../.rubocop.yml 3 | 4 | Metrics/BlockLength: 5 | Max: 500 6 | -------------------------------------------------------------------------------- /ruby/spec/asciidoctor_kroki_block_macro_spec.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/ConstantDefinitionInBlock 2 | # frozen_string_literal: true 3 | 4 | require 'tmpdir' 5 | require 'rspec_helper' 6 | require 'asciidoctor' 7 | require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki' 8 | require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki/extension' 9 | 10 | describe ::AsciidoctorExtensions::KrokiBlockMacroProcessor do 11 | context 'convert to html5' do 12 | it 'should catch exception if target is not readable' do 13 | class PlainResolutionKrokiMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor 14 | def resolve_target_path(_parent, target) 15 | target 16 | end 17 | end 18 | registry = Asciidoctor::Extensions.create do 19 | block_macro PlainResolutionKrokiMacroProcessor, 'plantuml' 20 | end 21 | input = <<~'ADOC' 22 | plantuml::spec/fixtures/missing.puml[svg,role=sequence] 23 | ADOC 24 | output = Asciidoctor.convert(input, standalone: false, extension_registry: registry) 25 | (expect output).to eql %(
26 |

Unresolved block macro - plantuml::spec/fixtures/missing.puml[]

27 |
) 28 | end 29 | end 30 | context 'using a custom block macro' do 31 | it 'should disallow read' do 32 | # noinspection RubyClassModuleNamingConvention 33 | class DisallowReadKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor 34 | def resolve_target_path(_parent, target) 35 | target 36 | end 37 | 38 | def read_allowed?(_target) 39 | false 40 | end 41 | end 42 | registry = Asciidoctor::Extensions.create do 43 | block_macro DisallowReadKrokiBlockMacroProcessor, 'plantuml' 44 | end 45 | input = <<~'ADOC' 46 | plantuml::spec/fixtures/alice.puml[svg,role=sequence] 47 | ADOC 48 | output = Asciidoctor.convert(input, standalone: false, extension_registry: registry) 49 | (expect output).to eql %(
50 |

spec/fixtures/alice.puml

51 |
) 52 | end 53 | it 'should allow read if target is not a URI' do 54 | # noinspection RubyClassModuleNamingConvention 55 | class DisallowUriReadKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor 56 | def read_allowed?(target) 57 | return false if ::Asciidoctor::Helpers.uriish?(target) 58 | 59 | true 60 | end 61 | end 62 | registry = Asciidoctor::Extensions.create do 63 | block_macro DisallowUriReadKrokiBlockMacroProcessor, 'plantuml' 64 | end 65 | input = <<~'ADOC' 66 | plantuml::https://domain.org/alice.puml[svg,role=sequence] 67 | 68 | plantuml::file://path/to/alice.puml[svg,role=sequence] 69 | 70 | plantuml::spec/fixtures/alice.puml[svg,role=sequence] 71 | ADOC 72 | output = Asciidoctor.convert(input, standalone: false, extension_registry: registry) 73 | (expect output).to eql %(
74 |

https://domain.org/alice.puml

75 |
76 |
77 |

file://path/to/alice.puml

78 |
79 |
80 |
81 | Diagram 82 |
83 |
) 84 | end 85 | it 'should override the resolve target method' do 86 | # noinspection RubyClassModuleNamingConvention 87 | class FixtureResolveTargetKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor 88 | def resolve_target_path(_parent, target) 89 | "spec/fixtures/#{target}" 90 | end 91 | end 92 | registry = Asciidoctor::Extensions.create do 93 | block_macro FixtureResolveTargetKrokiBlockMacroProcessor, 'plantuml' 94 | end 95 | input = <<~'ADOC' 96 | plantuml::alice.puml[svg,role=sequence] 97 | ADOC 98 | output = Asciidoctor.convert(input, standalone: false, extension_registry: registry) 99 | (expect output).to eql %(
100 |
101 | Diagram 102 |
103 |
) 104 | end 105 | it 'should display unresolved block macro message when the target cannot be resolved' do 106 | # noinspection RubyClassModuleNamingConvention 107 | class UnresolvedTargetKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor 108 | def resolve_target_path(_, _) 109 | nil 110 | end 111 | end 112 | registry = Asciidoctor::Extensions.create do 113 | block_macro UnresolvedTargetKrokiBlockMacroProcessor, 'plantuml' 114 | end 115 | input = <<~'ADOC' 116 | plantuml::alice.puml[svg,role=sequence] 117 | ADOC 118 | output = Asciidoctor.convert(input, standalone: false, extension_registry: registry) 119 | (expect output).to eql %(
120 |

Unresolved block macro - plantuml::alice.puml[]

121 |
) 122 | end 123 | it 'should override the unresolved block macro message' do 124 | # noinspection RubyClassModuleNamingConvention 125 | class CustomUnresolvedTargetMessageKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor 126 | def resolve_target_path(_parent, target) 127 | target 128 | end 129 | 130 | def unresolved_block_macro_message(name, target) 131 | "*[ERROR: #{name}::#{target}[] - unresolved block macro]*" 132 | end 133 | end 134 | registry = Asciidoctor::Extensions.create do 135 | block_macro CustomUnresolvedTargetMessageKrokiBlockMacroProcessor, 'plantuml' 136 | end 137 | input = <<~'ADOC' 138 | plantuml::spec/fixtures/missing.puml[svg,role=sequence] 139 | ADOC 140 | output = Asciidoctor.convert(input, standalone: false, extension_registry: registry) 141 | (expect output).to eql %(
142 |

[ERROR: plantuml::spec/fixtures/missing.puml[] - unresolved block macro]

143 |
) 144 | end 145 | 146 | it 'should properly resolve relative path to files' do 147 | Dir.mktmpdir('rspec-') do |temp_dir| 148 | temp_file = "#{temp_dir}/test.adoc" 149 | assets_dir = "#{temp_dir}/_assets" 150 | 151 | File.open(temp_file, 'w') do |f| 152 | content = <<~'ADOC' 153 | plantuml::_assets/alice.puml[svg,role=sequence] 154 | ADOC 155 | f.write(content) 156 | end 157 | Dir.mkdir(assets_dir) 158 | FileUtils.cp('spec/fixtures/alice.puml', "#{assets_dir}/alice.puml") 159 | Asciidoctor.convert_file(temp_file, standalone: false) 160 | (expect File.read("#{temp_dir}/test.html")).to eql %(
161 |
162 | Diagram 163 |
164 |
) 165 | end 166 | end 167 | end 168 | end 169 | # rubocop:enable Lint/ConstantDefinitionInBlock 170 | -------------------------------------------------------------------------------- /ruby/spec/asciidoctor_kroki_client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec_helper' 4 | require 'asciidoctor' 5 | require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki' 6 | 7 | describe ::AsciidoctorExtensions::KrokiClient do 8 | it 'should use adaptive method when http method is invalid' do 9 | kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient 10 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'http://localhost:8000', http_method: 'patch', http_client: kroki_http_client) 11 | expect(kroki_client.method).to eq('adaptive') 12 | end 13 | it 'should use post method when http method is post' do 14 | kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient 15 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'http://localhost:8000', http_method: 'POST', http_client: kroki_http_client) 16 | expect(kroki_client.method).to eq('post') 17 | end 18 | it 'should use get method when http method is get' do 19 | kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient 20 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'http://localhost:8000', http_method: 'get', http_client: kroki_http_client) 21 | expect(kroki_client.method).to eq('get') 22 | end 23 | it 'should use 4000 as the default max URI length' do 24 | kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient 25 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'http://localhost:8000', http_method: 'get', http_client: kroki_http_client) 26 | expect(kroki_client.max_uri_length).to eq(4000) 27 | end 28 | it 'should use a custom value as max URI length' do 29 | kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient 30 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'http://localhost:8000', http_method: 'get', http_client: kroki_http_client, max_uri_length: 8000) 31 | expect(kroki_client.max_uri_length).to eq(8000) 32 | end 33 | it 'should get an image with POST request if the URI length is greater than the value configured' do 34 | kroki_http_client = Class.new do 35 | class << self 36 | def get(uri, _) 37 | "GET #{uri}" 38 | end 39 | 40 | def post(uri, data, _, _) 41 | "POST #{uri} - #{data}" 42 | end 43 | end 44 | end 45 | kroki_diagram = Class.new do 46 | attr_reader :type, :text, :format, :opts 47 | 48 | def initialize(type, format, text, opts = {}) 49 | @text = text 50 | @type = type 51 | @format = format 52 | @opts = opts 53 | end 54 | 55 | def get_diagram_uri(_) 56 | 'diagram-uri' 57 | end 58 | end.new('type', 'format', 'text') 59 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'http://localhost:8000', http_method: 'adaptive', http_client: kroki_http_client, max_uri_length: 10) 60 | result = kroki_client.get_image(kroki_diagram, 'utf8') 61 | expect(result).to eq('POST http://localhost:8000/type/format - text') 62 | end 63 | it 'should get an image with GET request if the URI length is lower or equals than the value configured' do 64 | kroki_http_client = Class.new do 65 | class << self 66 | def get(uri, _, _) 67 | "GET #{uri}" 68 | end 69 | 70 | def post(uri, data, _, _) 71 | "POST #{uri} - #{data}" 72 | end 73 | end 74 | end 75 | kroki_diagram = Class.new do 76 | attr_reader :type, :text, :format, :opts 77 | 78 | def initialize(type, format, text, opts = {}) 79 | @text = text 80 | @type = type 81 | @format = format 82 | @opts = opts 83 | end 84 | 85 | def get_diagram_uri(_) 86 | 'diagram-uri' 87 | end 88 | end.new('type', 'format', 'text') 89 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'http://localhost:8000', http_method: 'adaptive', http_client: kroki_http_client, max_uri_length: 11) 90 | result = kroki_client.get_image(kroki_diagram, 'utf8') 91 | expect(result).to eq('GET diagram-uri') 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /ruby/spec/asciidoctor_kroki_diagram_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec_helper' 4 | require 'asciidoctor' 5 | require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki' 6 | 7 | describe ::AsciidoctorExtensions::KrokiDiagram do 8 | it 'should compute a diagram URI' do 9 | kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}') 10 | diagram_uri = kroki_diagram.get_diagram_uri('http://localhost:8000') 11 | expect(diagram_uri).to eq('http://localhost:8000/vegalite/png/eNqrrgUAAXUA-Q==') 12 | end 13 | it 'should compute a diagram URI with a trailing slashes' do 14 | kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}') 15 | diagram_uri = kroki_diagram.get_diagram_uri('https://my.domain.org/kroki/') 16 | expect(diagram_uri).to eq('https://my.domain.org/kroki/vegalite/png/eNqrrgUAAXUA-Q==') 17 | end 18 | it 'should compute a diagram URI with trailing slashes' do 19 | kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}') 20 | diagram_uri = kroki_diagram.get_diagram_uri('https://my-server/kroki//') 21 | expect(diagram_uri).to eq('https://my-server/kroki/vegalite/png/eNqrrgUAAXUA-Q==') 22 | end 23 | it 'should compute a diagram URI with query parameters' do 24 | text = %q{ 25 | .---. 26 | /-o-/-- 27 | .-/ / /-> 28 | ( * \/ 29 | '-. \ 30 | \ / 31 | ' 32 | } 33 | opts = { 34 | 'stroke-width' => 1, 35 | 'background' => 'black' 36 | } 37 | kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('svgbob', 'png', text, nil, opts) 38 | diagram_uri = kroki_diagram.get_diagram_uri('http://localhost:8000') 39 | expect(diagram_uri).to eq('http://localhost:8000/svgbob/png/eNrjUoAAPV1dXT0uCFtfN19XX1eXCyysrwCEunZAjoaCloJCjD5IWF1XD8gEK49R0IdoUwdTAN3kC7U=?stroke-width=1&background=black') 40 | end 41 | it 'should encode a diagram text definition' do 42 | kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'txt', ' alice -> bob: hello') 43 | diagram_definition_encoded = kroki_diagram.encode 44 | expect(diagram_definition_encoded).to eq('eNpTSMzJTE5V0LVTSMpPslLISM3JyQcAQAwGaw==') 45 | end 46 | it 'should fetch a diagram from Kroki and save it to disk' do 47 | kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'txt', ' alice -> bob: hello') 48 | kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient 49 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'https://kroki.io', http_method: 'get', http_client: kroki_http_client) 50 | output_dir_path = "#{__dir__}/../.asciidoctor/kroki" 51 | diagram_name = kroki_diagram.save(output_dir_path, kroki_client) 52 | diagram_path = File.join(output_dir_path, diagram_name) 53 | expect(File.exist?(diagram_path)).to be_truthy, "diagram should be saved at: #{diagram_path}" 54 | content = <<-TXT.chomp 55 | ,-----. ,---. 56 | |alice| |bob| 57 | `--+--' `-+-' 58 | | hello | 59 | |-------------->| 60 | ,--+--. ,-+-. 61 | |alice| |bob| 62 | `-----' `---' 63 | TXT 64 | expect(File.read(diagram_path).split("\n").map(&:rstrip).join("\n")).to eq(content) 65 | end 66 | it 'should fetch a diagram from Kroki and save it to disk using the target name' do 67 | kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'txt', ' alice -> bob: hello', 'hello-world') 68 | kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient 69 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'https://kroki.io', http_method: 'get', http_client: kroki_http_client) 70 | output_dir_path = "#{__dir__}/../.asciidoctor/kroki" 71 | diagram_name = kroki_diagram.save(output_dir_path, kroki_client) 72 | diagram_path = File.join(output_dir_path, diagram_name) 73 | expect(diagram_name).to start_with('hello-world-'), "diagram name should use the target as a prefix, got: #{diagram_name}" 74 | expect(File.exist?(diagram_path)).to be_truthy, "diagram should be saved at: #{diagram_path}" 75 | content = <<-TXT.chomp 76 | ,-----. ,---. 77 | |alice| |bob| 78 | `--+--' `-+-' 79 | | hello | 80 | |-------------->| 81 | ,--+--. ,-+-. 82 | |alice| |bob| 83 | `-----' `---' 84 | TXT 85 | expect(File.read(diagram_path).split("\n").map(&:rstrip).join("\n")).to eq(content) 86 | end 87 | it 'should fetch a diagram from Kroki with the same definition only once' do 88 | kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'png', ' guillaume -> dan: hello') 89 | kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient 90 | kroki_client = ::AsciidoctorExtensions::KrokiClient.new(server_url: 'https://kroki.io', http_method: 'get', http_client: kroki_http_client) 91 | output_dir_path = "#{__dir__}/../.asciidoctor/kroki" 92 | # make sure that we are doing only one GET request 93 | diagram_contents = File.read("#{__dir__}/fixtures/plantuml-diagram.png", mode: 'rb') 94 | expect(kroki_http_client).to receive(:get).once.and_return(diagram_contents) 95 | diagram_name = kroki_diagram.save(output_dir_path, kroki_client) 96 | diagram_path = File.join(output_dir_path, diagram_name) 97 | expect(File.exist?(diagram_path)).to be_truthy, "diagram should be saved at: #{diagram_path}" 98 | # calling again... should read the file from disk (and not do a GET request) 99 | kroki_diagram.save(output_dir_path, kroki_client) 100 | expect(File.size(diagram_path)).to be_eql(diagram_contents.length), 'diagram should be fully saved on disk' 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /ruby/spec/asciidoctor_kroki_processor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec_helper' 4 | require 'asciidoctor' 5 | require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki' 6 | 7 | describe '::AsciidoctorExtensions::KrokiProcessor' do 8 | it 'should return the images output directory (imagesoutdir attribute)' do 9 | doc = Asciidoctor.load('hello', attributes: { 'imagesoutdir' => '.asciidoctor/kroki/images', 'imagesdir' => '../images' }) 10 | output_dir_path = AsciidoctorExtensions::KrokiProcessor.send(:output_dir_path, doc) 11 | expect(output_dir_path).to eq '.asciidoctor/kroki/images' 12 | end 13 | it 'should return a path relative to output directory (to_dir option)' do 14 | doc = Asciidoctor.load('hello', to_dir: '.asciidoctor/kroki/relative', attributes: { 'imagesdir' => '../images' }) 15 | output_dir_path = AsciidoctorExtensions::KrokiProcessor.send(:output_dir_path, doc) 16 | expect(output_dir_path).to eq '.asciidoctor/kroki/relative/../images' 17 | end 18 | it 'should return a path relative to output directory (outdir attribute)' do 19 | doc = Asciidoctor.load('hello', attributes: { 'imagesdir' => 'resources/images', 'outdir' => '.asciidoctor/kroki/out' }) 20 | output_dir_path = AsciidoctorExtensions::KrokiProcessor.send(:output_dir_path, doc) 21 | expect(output_dir_path).to eq '.asciidoctor/kroki/out/resources/images' 22 | end 23 | it 'should return a path relative to the base directory (base_dir option)' do 24 | doc = Asciidoctor.load('hello', base_dir: '.asciidoctor/kroki', attributes: { 'imagesdir' => 'img' }) 25 | output_dir_path = AsciidoctorExtensions::KrokiProcessor.send(:output_dir_path, doc) 26 | expect(output_dir_path).to eq "#{::Dir.pwd}/.asciidoctor/kroki/img" 27 | end 28 | it 'should return a path relative to the base directory (default value is current working directory)' do 29 | doc = Asciidoctor.load('hello', attributes: { 'imagesdir' => 'img' }) 30 | output_dir_path = AsciidoctorExtensions::KrokiProcessor.send(:output_dir_path, doc) 31 | expect(output_dir_path).to eq "#{::Dir.pwd}/img" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /ruby/spec/asciidoctor_kroki_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec_helper' 4 | require 'asciidoctor' 5 | require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki' 6 | 7 | describe ::AsciidoctorExtensions::KrokiBlockProcessor do 8 | context 'convert to html5' do 9 | it 'should convert a PlantUML block to an image' do 10 | input = <<~'ADOC' 11 | [plantuml] 12 | .... 13 | alice -> bob: hello 14 | .... 15 | ADOC 16 | output = Asciidoctor.convert(input, standalone: false) 17 | (expect output).to eql %(
18 |
19 | Diagram 20 |
21 |
) 22 | end 23 | it 'should only pass diagram options as query parameters' do 24 | input = <<~'ADOC' 25 | [plantuml,alice-bob,svg,role=sequence,width=100,format=svg,link=https://asciidoc.org/,align=center,float=right,theme=bluegray] 26 | .... 27 | alice -> bob: hello 28 | .... 29 | ADOC 30 | output = Asciidoctor.convert(input, standalone: false) 31 | (expect output).to eql %(
32 |
33 | alice-bob 34 |
35 |
) 36 | end 37 | it 'should use the title attribute as the alt value' do 38 | input = <<~'ADOC' 39 | [plantuml,title="Alice saying hello to Bob"] 40 | .... 41 | alice -> bob: hello 42 | .... 43 | ADOC 44 | output = Asciidoctor.convert(input, standalone: false) 45 | (expect output).to eql %(
46 |
47 | Alice saying hello to Bob 48 |
49 |
Figure 1. Alice saying hello to Bob
50 |
) 51 | end 52 | it 'should use png if kroki-default-format is set to png' do 53 | input = <<~'ADOC' 54 | [plantuml] 55 | .... 56 | alice -> bob: hello 57 | .... 58 | ADOC 59 | output = Asciidoctor.convert(input, attributes: { 'kroki-default-format' => 'png' }, standalone: false) 60 | (expect output).to eql %(
61 |
62 | Diagram 63 |
64 |
) 65 | end 66 | it 'should use svg if kroki-default-format is set to png and the diagram type does not support png' do 67 | input = <<~'ADOC' 68 | [nomnoml] 69 | .... 70 | [Pirate|eyeCount: Int|raid();pillage()| 71 | [beard]--[parrot] 72 | [beard]-:>[foul mouth] 73 | ] 74 | .... 75 | ADOC 76 | output = Asciidoctor.convert(input, attributes: { 'kroki-default-format' => 'png' }, standalone: false) 77 | (expect output).to eql %(
78 |
79 | Diagram 80 |
81 |
) 82 | end 83 | it 'should include the plantuml-include file when safe mode is safe' do 84 | input = <<~'ADOC' 85 | [plantuml] 86 | .... 87 | alice -> bob: hello 88 | .... 89 | ADOC 90 | output = Asciidoctor.convert(input, 91 | attributes: { 'kroki-plantuml-include' => 'spec/fixtures/config.puml' }, 92 | standalone: false, safe: :safe) 93 | (expect output).to eql %(
94 |
95 | Diagram 96 |
97 |
) 98 | end 99 | it 'should normalize plantuml-include path when safe mode is safe' do 100 | input = <<~'ADOC' 101 | [plantuml] 102 | .... 103 | alice -> bob: hello 104 | .... 105 | ADOC 106 | output = Asciidoctor.convert(input, attributes: { 'kroki-plantuml-include' => '../../../spec/fixtures/config.puml' }, standalone: false, safe: :safe) 107 | (expect output).to eql %(
108 |
109 | Diagram 110 |
111 |
) 112 | end 113 | it 'should not include file which reside outside of the parent directory of the source when safe mode is safe' do 114 | input = <<~'ADOC' 115 | [plantuml] 116 | .... 117 | alice -> bob: hello 118 | .... 119 | ADOC 120 | output = Asciidoctor.convert(input, attributes: { 'kroki-plantuml-include' => '/etc/passwd' }, standalone: false, safe: :safe) 121 | (expect output).to eql %(
122 |
123 | Diagram 124 |
125 |
) 126 | end 127 | it 'should not include file when safe mode is secure' do 128 | input = <<~'ADOC' 129 | [plantuml] 130 | .... 131 | alice -> bob: hello 132 | .... 133 | ADOC 134 | output = Asciidoctor.convert(input, attributes: { 'kroki-plantuml-include' => 'spec/fixtures/config.puml' }, standalone: false, safe: :secure) 135 | (expect output).to eql %(
136 |
137 | Diagram 138 |
139 |
) 140 | end 141 | it 'should create SVG diagram in imagesdir if kroki-fetch-diagram is set' do 142 | input = <<~'ADOC' 143 | :imagesdir: .asciidoctor/kroki 144 | 145 | plantuml::spec/fixtures/alice.puml[svg,role=sequence] 146 | ADOC 147 | output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false, safe: :safe) 148 | (expect output).to eql %(
149 |
150 | Diagram 151 |
152 |
) 153 | end 154 | it 'should not fetch diagram when safe mode is secure' do 155 | input = <<~'ADOC' 156 | :imagesdir: .asciidoctor/kroki 157 | 158 | plantuml::spec/fixtures/alice.puml[svg,role=sequence] 159 | ADOC 160 | output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false) 161 | (expect output).to eql %(
162 |
163 | Diagram 164 |
165 |
) 166 | end 167 | it 'should create PNG diagram in imagesdir if kroki-fetch-diagram is set' do 168 | input = <<~'ADOC' 169 | :imagesdir: .asciidoctor/kroki 170 | 171 | plantuml::spec/fixtures/alice.puml[png,role=sequence] 172 | ADOC 173 | output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false, safe: :safe) 174 | (expect output).to eql %(
175 |
176 | Diagram 177 |
178 |
) 179 | end 180 | end 181 | context 'instantiate' do 182 | it 'should instantiate block processor without warning' do 183 | original_stderr = $stderr 184 | $stderr = StringIO.new 185 | ::AsciidoctorExtensions::KrokiBlockProcessor.new :plantuml, {} 186 | output = $stderr.string 187 | (expect output).to eql '' 188 | ensure 189 | $stderr = original_stderr 190 | end 191 | end 192 | end 193 | 194 | describe ::AsciidoctorExtensions::Kroki do 195 | it 'should return the list of supported diagrams' do 196 | diagram_names = ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES 197 | expect(diagram_names).to include('vegalite', 'plantuml', 'bytefield', 'bpmn', 'excalidraw', 'wavedrom', 'pikchr', 'structurizr', 'diagramsnet') 198 | end 199 | it 'should register the extension for the list of supported diagrams' do 200 | doc = Asciidoctor::Document.new 201 | registry = Asciidoctor::Extensions::Registry.new 202 | registry.activate doc 203 | ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.each do |name| 204 | expect(registry.find_block_extension(name)).to_not be_nil, "expected block extension named '#{name}' to be registered" 205 | expect(registry.find_block_macro_extension(name)).to_not be_nil, "expected block macro extension named '#{name}' to be registered " 206 | end 207 | end 208 | end 209 | -------------------------------------------------------------------------------- /ruby/spec/fixtures/alice.puml: -------------------------------------------------------------------------------- 1 | alice -> bob: hello 2 | -------------------------------------------------------------------------------- /ruby/spec/fixtures/config.puml: -------------------------------------------------------------------------------- 1 | skinparam monochrome true 2 | -------------------------------------------------------------------------------- /ruby/spec/fixtures/plantuml-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asciidoctor/asciidoctor-kroki/602658c6c1fc0c50b33fd0116961ce6b12afabdc/ruby/spec/fixtures/plantuml-diagram.png -------------------------------------------------------------------------------- /ruby/spec/require_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe 'require' do 4 | it 'should require the library' do 5 | lib = File.expand_path('lib', __dir__) 6 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 7 | require 'asciidoctor-kroki' 8 | 9 | (expect Asciidoctor::Extensions.groups[:extgrp0]).to_not be_nil 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /ruby/spec/rspec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.before(:suite) do 5 | FileUtils.rm(Dir.glob("#{__dir__}/../.asciidoctor/kroki/diag-*")) 6 | end 7 | config.after(:suite) do 8 | FileUtils.rm(Dir.glob("#{__dir__}/../.asciidoctor/kroki/diag-*")) unless ENV['DEBUG'] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /ruby/tasks/bundler.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'bundler/gem_tasks' 5 | rescue LoadError 6 | warn $ERROR_INFO.message 7 | end 8 | -------------------------------------------------------------------------------- /ruby/tasks/lint.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubocop/rake_task' 4 | 5 | RuboCop::RakeTask.new :lint 6 | -------------------------------------------------------------------------------- /ruby/tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new :spec 6 | rescue LoadError 7 | warn $ERROR_INFO.message 8 | end 9 | -------------------------------------------------------------------------------- /src/antora-adapter.js: -------------------------------------------------------------------------------- 1 | const ospath = require('path').posix 2 | 3 | module.exports = (file, contentCatalog, vfs) => { 4 | let baseReadFn 5 | if (typeof vfs === 'undefined' || typeof vfs.read !== 'function') { 6 | baseReadFn = require('./node-fs').read 7 | } else { 8 | baseReadFn = vfs.read 9 | } 10 | let baseParseFn 11 | if (typeof vfs === 'undefined' || typeof vfs.parse !== 'function') { 12 | baseParseFn = require('./node-fs').parse 13 | } else { 14 | baseParseFn = vfs.parse 15 | } 16 | let baseExistsFn 17 | if (typeof vfs === 'undefined' || typeof vfs.exists !== 'function') { 18 | baseExistsFn = require('./node-fs').exists 19 | } else { 20 | baseExistsFn = vfs.exists 21 | } 22 | return { 23 | add: (image) => { 24 | const { component, version, module } = file.src 25 | if (!contentCatalog.getById({ component, version, module, family: 'image', relative: image.basename })) { 26 | contentCatalog.addFile({ 27 | contents: image.contents, 28 | src: { 29 | component, 30 | version, 31 | module, 32 | family: 'image', 33 | mediaType: image.mediaType, 34 | path: ospath.join(image.relative, image.basename), 35 | basename: image.basename, 36 | relative: image.basename 37 | } 38 | }) 39 | } 40 | }, 41 | read: (resourceId, format, hash) => { 42 | const ctx = hash || file.src 43 | const target = contentCatalog.resolveResource(resourceId, ctx, ctx.family) 44 | return target ? target.contents.toString() : baseReadFn(resourceId, format) 45 | }, 46 | exists: (resourceId) => { 47 | const target = contentCatalog.resolveResource(resourceId, file.src) 48 | return target ? true : baseExistsFn(resourceId) 49 | }, 50 | parse: (resourceId, hash) => { 51 | const ctx = hash || file.src 52 | const target = contentCatalog.resolveResource(resourceId, ctx, ctx.family) 53 | return target ? target.src : baseParseFn(resourceId) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/asciidoctor-kroki.js: -------------------------------------------------------------------------------- 1 | /* global Opal */ 2 | // @ts-check 3 | const { KrokiDiagram, KrokiClient } = require('./kroki-client.js') 4 | 5 | function UnsupportedFormatError (message) { 6 | this.name = 'UnsupportedFormatError' 7 | this.message = message 8 | this.stack = (new Error()).stack 9 | } 10 | 11 | // eslint-disable-next-line new-parens 12 | UnsupportedFormatError.prototype = new Error 13 | 14 | function InvalidConfigurationError (message) { 15 | this.name = 'InvalidConfigurationError' 16 | this.message = message 17 | this.stack = (new Error()).stack 18 | } 19 | 20 | // eslint-disable-next-line new-parens 21 | InvalidConfigurationError.prototype = new Error 22 | 23 | const isBrowser = () => { 24 | return typeof window === 'object' && typeof window.XMLHttpRequest === 'object' 25 | } 26 | 27 | // A value of 20 (SECURE) disallows the document from attempting to read files from the file system 28 | const SAFE_MODE_SECURE = 20 29 | 30 | const BUILTIN_ATTRIBUTES = [ 31 | 'target', 32 | 'width', 33 | 'height', 34 | 'format', 35 | 'fallback', 36 | 'link', 37 | 'float', 38 | 'align', 39 | 'role', 40 | 'title', 41 | 'caption', 42 | 'cloaked-context', 43 | '$positional', 44 | 'subs' 45 | ] 46 | 47 | const wrapError = (err, message) => { 48 | const errWrapper = new Error(message) 49 | errWrapper.stack += `\nCaused by: ${err.stack || 'unknown'}` 50 | const result = { 51 | err: { 52 | package: 'asciidoctor-kroki', 53 | message, 54 | stack: errWrapper.stack 55 | } 56 | } 57 | result.$inspect = function () { 58 | return JSON.stringify(this.err) 59 | } 60 | return result 61 | } 62 | 63 | const createImageSrc = (doc, krokiDiagram, target, vfs, krokiClient) => { 64 | const shouldFetch = doc.isAttribute('kroki-fetch-diagram') 65 | let imageUrl 66 | if (shouldFetch && doc.getSafe() < SAFE_MODE_SECURE) { 67 | imageUrl = require('./fetch.js').save(krokiDiagram, doc, target, vfs, krokiClient) 68 | } else { 69 | imageUrl = krokiDiagram.getDiagramUri(krokiClient.getServerUrl()) 70 | } 71 | return imageUrl 72 | } 73 | 74 | /** 75 | * Get the option defined on the block or macro. 76 | * 77 | * First, check if an option is defined as an attribute. 78 | * If there is no match, check if an option is defined as a default settings in the document attributes. 79 | * 80 | * @param attrs - list of attributes 81 | * @param document - Asciidoctor document 82 | * @returns {string|undefined} - the option name or undefined 83 | */ 84 | function getOption (attrs, document) { 85 | const availableOptions = ['inline', 'interactive', 'none'] 86 | for (const option of availableOptions) { 87 | if (attrs[`${option}-option`] === '') { 88 | return option 89 | } 90 | } 91 | for (const option of availableOptions) { 92 | if (document.getAttribute('kroki-default-options') === option) { 93 | return option 94 | } 95 | } 96 | } 97 | 98 | const processKroki = (processor, parent, attrs, diagramType, diagramText, context, resource) => { 99 | const doc = parent.getDocument() 100 | // If "subs" attribute is specified, substitute accordingly. 101 | // Be careful not to specify "specialcharacters" or your diagram code won't be valid anymore! 102 | const subs = attrs.subs 103 | if (subs) { 104 | diagramText = parent.applySubstitutions(diagramText, parent.$resolve_subs(subs)) 105 | } 106 | if (doc.getSafe() < SAFE_MODE_SECURE) { 107 | if (diagramType === 'vegalite') { 108 | diagramText = require('./preprocess.js').preprocessVegaLite(diagramText, context, (resource && resource.dir) || '') 109 | } else if (diagramType === 'plantuml' || diagramType === 'c4plantuml') { 110 | const plantUmlIncludeFile = doc.getAttribute('kroki-plantuml-include') 111 | if (plantUmlIncludeFile) { 112 | diagramText = `!include ${plantUmlIncludeFile}\n${diagramText}` 113 | } 114 | const plantUmlIncludePaths = doc.getAttribute('kroki-plantuml-include-paths') 115 | diagramText = require('./preprocess.js').preprocessPlantUML(diagramText, context, plantUmlIncludePaths, resource) 116 | } 117 | } 118 | const blockId = attrs.id 119 | const format = attrs.format || doc.getAttribute('kroki-default-format') || 'svg' 120 | const caption = attrs.caption 121 | const title = attrs.title 122 | let role = attrs.role 123 | if (role) { 124 | if (format) { 125 | role = `${role} kroki-format-${format} kroki` 126 | } else { 127 | role = `${role} kroki` 128 | } 129 | } else { 130 | role = 'kroki' 131 | } 132 | const blockAttrs = Object.assign({}, attrs) 133 | blockAttrs.role = role 134 | blockAttrs.format = format 135 | delete blockAttrs.title 136 | delete blockAttrs.caption 137 | delete blockAttrs.opts 138 | const option = getOption(attrs, doc) 139 | if (option && option !== 'none') { 140 | blockAttrs[`${option}-option`] = '' 141 | } 142 | 143 | if (blockId) { 144 | blockAttrs.id = blockId 145 | } 146 | const opts = Object.fromEntries(Object.entries(attrs).filter(([key, _]) => !key.endsWith('-option') && !BUILTIN_ATTRIBUTES.includes(key))) 147 | const krokiDiagram = new KrokiDiagram(diagramType, format, diagramText, opts) 148 | const httpClient = isBrowser() ? require('./http/browser-http.js') : require('./http/node-http.js') 149 | const krokiClient = new KrokiClient(doc, httpClient) 150 | let block 151 | if (format === 'txt' || format === 'atxt' || format === 'utxt') { 152 | const textContent = krokiClient.getTextContent(krokiDiagram) 153 | block = processor.createBlock(parent, 'literal', textContent, blockAttrs) 154 | } else { 155 | let alt 156 | if (attrs.title) { 157 | alt = attrs.title 158 | } else if (attrs.target) { 159 | alt = attrs.target 160 | } else { 161 | alt = 'Diagram' 162 | } 163 | blockAttrs.target = createImageSrc(doc, krokiDiagram, attrs.target, context.vfs, krokiClient) 164 | blockAttrs.alt = alt 165 | block = processor.createImageBlock(parent, blockAttrs) 166 | } 167 | if (title) { 168 | block['$title='](title) 169 | } 170 | block.$assign_caption(caption, 'figure') 171 | return block 172 | } 173 | 174 | function diagramBlock (context) { 175 | return function () { 176 | const self = this 177 | self.onContext(['listing', 'literal']) 178 | self.positionalAttributes(['target', 'format']) 179 | self.process((parent, reader, attrs) => { 180 | const diagramType = this.name.toString() 181 | const role = attrs.role 182 | const diagramText = reader.$read() 183 | try { 184 | return processKroki(this, parent, attrs, diagramType, diagramText, context) 185 | } catch (err) { 186 | const errorMessage = wrapError(err, `Skipping ${diagramType} block.`) 187 | parent.getDocument().getLogger().warn(errorMessage) 188 | attrs.role = role ? `${role} kroki-error` : 'kroki-error' 189 | return this.createBlock(parent, attrs['cloaked-context'], diagramText, attrs) 190 | } 191 | }) 192 | } 193 | } 194 | 195 | function diagramBlockMacro (name, context) { 196 | return function () { 197 | const self = this 198 | self.named(name) 199 | self.positionalAttributes(['format']) 200 | self.process((parent, target, attrs) => { 201 | let vfs = context.vfs 202 | target = parent.applySubstitutions(target, ['attributes']) 203 | if (isBrowser()) { 204 | if (!['file://', 'https://', 'http://'].some(prefix => target.startsWith(prefix))) { 205 | // if not an absolute URL, prefix with baseDir in the browser environment 206 | const doc = parent.getDocument() 207 | const baseDir = doc.getBaseDir() 208 | const startDir = typeof baseDir !== 'undefined' ? baseDir : '.' 209 | target = startDir !== '.' ? doc.normalizeWebPath(target, startDir) : target 210 | } 211 | } else { 212 | if (vfs === undefined || typeof vfs.read !== 'function') { 213 | vfs = require('./node-fs.js') 214 | target = parent.normalizeSystemPath(target) 215 | } 216 | } 217 | const role = attrs.role 218 | const diagramType = name 219 | try { 220 | const diagramText = vfs.read(target) 221 | const resource = (typeof vfs.parse === 'function' && vfs.parse(target)) || { dir: '' } 222 | return processKroki(this, parent, attrs, diagramType, diagramText, context, resource) 223 | } catch (err) { 224 | const errorMessage = wrapError(err, `Skipping ${diagramType} block.`) 225 | parent.getDocument().getLogger().warn(errorMessage) 226 | attrs.role = role ? `${role} kroki-error` : 'kroki-error' 227 | return this.createBlock(parent, 'paragraph', `${err.message} - ${diagramType}::${target}[]`, attrs) 228 | } 229 | }) 230 | } 231 | } 232 | 233 | module.exports.register = function register (registry, context = {}) { 234 | // patch context in case of Antora 235 | if (typeof context.contentCatalog !== 'undefined' && typeof context.contentCatalog.addFile === 'function' && typeof context.file !== 'undefined') { 236 | context.vfs = require('./antora-adapter.js')(context.file, context.contentCatalog, context.vfs) 237 | } 238 | context.logger = Opal.Asciidoctor.LoggerManager.getLogger() 239 | const names = [ 240 | 'actdiag', 241 | 'blockdiag', 242 | 'bpmn', 243 | 'bytefield', 244 | 'c4plantuml', 245 | 'd2', 246 | 'dbml', 247 | 'ditaa', 248 | 'erd', 249 | 'excalidraw', 250 | 'graphviz', 251 | 'mermaid', 252 | 'nomnoml', 253 | 'nwdiag', 254 | 'packetdiag', 255 | 'pikchr', 256 | 'plantuml', 257 | 'rackdiag', 258 | 'seqdiag', 259 | 'svgbob', 260 | 'symbolator', 261 | 'umlet', 262 | 'vega', 263 | 'vegalite', 264 | 'wavedrom', 265 | 'structurizr', 266 | 'diagramsnet', 267 | 'wireviz' 268 | ] 269 | if (typeof registry.register === 'function') { 270 | registry.register(function () { 271 | for (const name of names) { 272 | this.block(name, diagramBlock(context)) 273 | this.blockMacro(diagramBlockMacro(name, context)) 274 | } 275 | }) 276 | } else if (typeof registry.block === 'function') { 277 | for (const name of names) { 278 | registry.block(name, diagramBlock(context)) 279 | registry.blockMacro(diagramBlockMacro(name, context)) 280 | } 281 | } 282 | return registry 283 | } 284 | -------------------------------------------------------------------------------- /src/fetch.js: -------------------------------------------------------------------------------- 1 | const rusha = require('rusha') 2 | const path = require('path').posix 3 | 4 | const getImagesOutputDirectory = (doc) => { 5 | const imagesOutputDir = doc.getAttribute('imagesoutdir') 6 | if (imagesOutputDir) { 7 | return imagesOutputDir 8 | } 9 | const outputDirectory = getOutputDirectory(doc) 10 | const imagesDir = doc.getAttribute('imagesdir') || '' 11 | return path.join(outputDirectory, imagesDir) 12 | } 13 | 14 | const getOutputDirectory = (doc) => { 15 | // the nested document logic will become obsolete once https://github.com/asciidoctor/asciidoctor/commit/7edc9da023522be67b17e2a085d72e056703a438 is released 16 | const outDir = doc.getAttribute('outdir') || (doc.isNested() ? doc.getParentDocument() : doc).getOptions().to_dir 17 | const baseDir = doc.getBaseDir() 18 | if (outDir) { 19 | return outDir 20 | } 21 | return baseDir 22 | } 23 | 24 | module.exports.save = function (krokiDiagram, doc, target, vfs, krokiClient) { 25 | const exists = typeof vfs !== 'undefined' && typeof vfs.exists === 'function' ? vfs.exists : require('./node-fs.js').exists 26 | const read = typeof vfs !== 'undefined' && typeof vfs.read === 'function' ? vfs.read : require('./node-fs.js').read 27 | const add = typeof vfs !== 'undefined' && typeof vfs.add === 'function' ? vfs.add : require('./node-fs.js').add 28 | 29 | const imagesOutputDirectory = getImagesOutputDirectory(doc) 30 | const dataUri = doc.isAttribute('data-uri') || doc.isAttribute('kroki-data-uri') 31 | const diagramUrl = krokiDiagram.getDiagramUri(krokiClient.getServerUrl()) 32 | const format = krokiDiagram.format 33 | const diagramName = `${target || 'diag'}-${rusha.createHash().update(diagramUrl).digest('hex')}.${format}` 34 | const filePath = path.format({ dir: imagesOutputDirectory, base: diagramName }) 35 | let encoding 36 | let mediaType 37 | if (format === 'txt' || format === 'atxt' || format === 'utxt') { 38 | mediaType = 'text/plain; charset=utf-8' 39 | encoding = 'utf8' 40 | } else if (format === 'svg') { 41 | mediaType = 'image/svg+xml' 42 | encoding = 'binary' 43 | } else { 44 | mediaType = 'image/png' 45 | encoding = 'binary' 46 | } 47 | // file is either (already) on the file system or we should read it from Kroki 48 | let contents 49 | if (exists(filePath)) { 50 | contents = read(filePath, encoding) 51 | } else { 52 | contents = krokiClient.getImage(krokiDiagram, encoding) 53 | } 54 | if (dataUri) { 55 | return 'data:' + mediaType + ';base64,' + Buffer.from(contents, encoding).toString('base64') 56 | } 57 | add({ 58 | relative: imagesOutputDirectory, 59 | basename: diagramName, 60 | mediaType, 61 | contents: Buffer.from(contents, encoding) 62 | }) 63 | return diagramName 64 | } 65 | -------------------------------------------------------------------------------- /src/http/browser-http.js: -------------------------------------------------------------------------------- 1 | /* global XMLHttpRequest */ 2 | const httpClient = require('./http-client.js') 3 | 4 | const httpPost = (uri, body, headers, encoding = 'utf8') => httpClient.post(XMLHttpRequest, uri, body, headers, encoding) 5 | const httpGet = (uri, headers, encoding = 'utf8') => httpClient.get(XMLHttpRequest, uri, headers, encoding) 6 | 7 | module.exports = { 8 | get: httpGet, 9 | post: httpPost 10 | } 11 | -------------------------------------------------------------------------------- /src/http/http-client.js: -------------------------------------------------------------------------------- 1 | const httpRequest = (XMLHttpRequest, uri, method, headers, encoding = 'utf8', body) => { 2 | let data = '' 3 | let status = -1 4 | try { 5 | const xhr = new XMLHttpRequest() 6 | xhr.open(method, uri, false) 7 | if (headers) { 8 | for (const [name, value] in Object.entries(headers)) { 9 | xhr.setRequestHeader(name, value) 10 | } 11 | } 12 | if (encoding === 'binary') { 13 | xhr.responseType = 'arraybuffer' 14 | } 15 | xhr.addEventListener('load', function () { 16 | status = this.status 17 | if (status === 200) { 18 | if (encoding === 'binary') { 19 | const arrayBuffer = xhr.response 20 | const byteArray = new Uint8Array(arrayBuffer) 21 | for (let i = 0; i < byteArray.byteLength; i++) { 22 | data += String.fromCharCode(byteArray[i]) 23 | } 24 | } else { 25 | data = this.responseText 26 | } 27 | } else if (encoding !== 'binary') { 28 | data = this.responseText 29 | } 30 | }) 31 | if (body) { 32 | xhr.send(body) 33 | } else { 34 | xhr.send() 35 | } 36 | } catch (e) { 37 | throw new Error(`${method} ${uri} - error; reason: ${e.message}`) 38 | } 39 | if (status >= 200 && status < 400) { 40 | // successful status 41 | if (data) { 42 | return data 43 | } 44 | throw new Error(`${method} ${uri} - server returns an empty response`) 45 | } 46 | 47 | throw new Error(`${method} ${uri} - server returns ${status} status code; response: ${data}`) 48 | } 49 | 50 | const httpPost = (XMLHttpRequest, uri, body, headers, encoding = 'utf8') => { 51 | return httpRequest(XMLHttpRequest, uri, 'POST', headers, encoding, body) 52 | } 53 | 54 | const httpGet = (XMLHttpRequest, uri, headers, encoding = 'utf8') => { 55 | return httpRequest(XMLHttpRequest, uri, 'GET', headers, encoding) 56 | } 57 | 58 | module.exports = { 59 | get: httpGet, 60 | post: httpPost 61 | } 62 | -------------------------------------------------------------------------------- /src/http/node-http.js: -------------------------------------------------------------------------------- 1 | const XMLHttpRequest = require('unxhr').XMLHttpRequest 2 | const httpClient = require('./http-client.js') 3 | 4 | const httpPost = (uri, body, headers, encoding = 'utf8') => httpClient.post(XMLHttpRequest, uri, body, headers, encoding) 5 | const httpGet = (uri, headers, encoding = 'utf8') => httpClient.get(XMLHttpRequest, uri, headers, encoding) 6 | 7 | module.exports = { 8 | get: httpGet, 9 | post: httpPost 10 | } 11 | -------------------------------------------------------------------------------- /src/kroki-client.js: -------------------------------------------------------------------------------- 1 | const { version } = require('../package.json') 2 | const pako = require('pako') 3 | 4 | const MAX_URI_DEFAULT_VALUE = 4000 5 | const REFERER = `asciidoctor/kroki.js/${version}` 6 | 7 | module.exports.KrokiDiagram = class KrokiDiagram { 8 | constructor (type, format, text, opts) { 9 | this.text = text 10 | this.type = type 11 | this.format = format 12 | this.opts = opts 13 | } 14 | 15 | getDiagramUri (serverUrl) { 16 | const queryParams = Object.entries(this.opts).map(([key, value]) => `${key}=${encodeURIComponent(value.toString())}`).join('&') 17 | return `${serverUrl}/${this.type}/${this.format}/${this.encode()}${queryParams ? `?${queryParams}` : ''}` 18 | } 19 | 20 | encode () { 21 | const data = Buffer.from(this.text, 'utf8') 22 | const compressed = pako.deflate(data, { level: 9 }) 23 | return Buffer.from(compressed) 24 | .toString('base64') 25 | .replace(/\+/g, '-') 26 | .replace(/\//g, '_') 27 | } 28 | } 29 | 30 | module.exports.KrokiClient = class KrokiClient { 31 | constructor (doc, httpClient) { 32 | const maxUriLengthValue = parseInt(doc.getAttribute('kroki-max-uri-length', MAX_URI_DEFAULT_VALUE.toString())) 33 | this.maxUriLength = isNaN(maxUriLengthValue) ? MAX_URI_DEFAULT_VALUE : maxUriLengthValue 34 | this.httpClient = httpClient 35 | const method = doc.getAttribute('kroki-http-method', 'adaptive').toLowerCase() 36 | if (method === 'get' || method === 'post' || method === 'adaptive') { 37 | this.method = method 38 | } else { 39 | console.warn(`Invalid value '${method}' for kroki-http-method attribute. The value must be either: 'get', 'post' or 'adaptive'. Proceeding using: 'adaptive'.`) 40 | this.method = 'adaptive' 41 | } 42 | this.doc = doc 43 | } 44 | 45 | getTextContent (krokiDiagram) { 46 | return this.getImage(krokiDiagram, 'utf8') 47 | } 48 | 49 | getImage (krokiDiagram, encoding) { 50 | const serverUrl = this.getServerUrl() 51 | const type = krokiDiagram.type 52 | const format = krokiDiagram.format 53 | const text = krokiDiagram.text 54 | const opts = krokiDiagram.opts 55 | const headers = { 56 | Referer: REFERER, 57 | ...Object.fromEntries(Object.entries(opts).map(([key, value]) => [`Kroki-Diagram-Options-${key}`, value])) 58 | } 59 | if (this.method === 'adaptive' || this.method === 'get') { 60 | const uri = krokiDiagram.getDiagramUri(serverUrl) 61 | if (uri.length > this.maxUriLength) { 62 | // The request URI is longer than the max URI length. 63 | if (this.method === 'get') { 64 | // The request might be rejected by the server with a 414 Request-URI Too Large. 65 | // Consider using the attribute kroki-http-method with the value 'adaptive'. 66 | return this.httpClient.get(uri, headers, encoding) 67 | } 68 | return this.httpClient.post(`${serverUrl}/${type}/${format}`, text, headers, encoding) 69 | } 70 | return this.httpClient.get(uri, headers, encoding) 71 | } 72 | return this.httpClient.post(`${serverUrl}/${type}/${format}`, text, headers, encoding) 73 | } 74 | 75 | getServerUrl () { 76 | return this.doc.getAttribute('kroki-server-url') || 'https://kroki.io' 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/node-fs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const mkdirp = require('mkdirp') 4 | const url = require('url') 5 | 6 | const http = require('./http/node-http.js') 7 | 8 | module.exports = { 9 | add: (image) => { 10 | mkdirp.sync(image.relative) 11 | const filePath = path.format({ dir: image.relative, base: image.basename }) 12 | fs.writeFileSync(filePath, image.contents, 'binary') 13 | }, 14 | exists: (path) => { 15 | return fs.existsSync(path) 16 | }, 17 | read: (path, encoding = 'utf8') => { 18 | if (path.startsWith('http://') || path.startsWith('https://')) { 19 | return http.get(path, encoding) 20 | } 21 | if (path.startsWith('file://')) { 22 | return fs.readFileSync(url.fileURLToPath(path), encoding) 23 | } 24 | return fs.readFileSync(path, encoding) 25 | }, 26 | parse: (resourceId) => { 27 | return { 28 | dir: path.dirname(resourceId), 29 | path: resourceId 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/preprocess.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // The previous line must be the first non-comment line in the file to enable TypeScript checks: 3 | // https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html#ts-check 4 | const { delimiter, posix: path } = require('path') 5 | 6 | /** 7 | * @param {string} diagramText 8 | * @param {any} context 9 | * @param {string} diagramDir 10 | * @returns {string} 11 | */ 12 | module.exports.preprocessVegaLite = function (diagramText, context = {}, diagramDir = '') { 13 | const logger = 'logger' in context && typeof context.logger !== 'undefined' ? context.logger : console 14 | let diagramObject 15 | try { 16 | const JSON5 = require('json5') 17 | diagramObject = JSON5.parse(diagramText) 18 | } catch (e) { 19 | const message = `Preprocessing of Vega-Lite view specification failed, because of a parsing error: 20 | ${e} 21 | The invalid view specification was: 22 | ${diagramText} 23 | ` 24 | throw addCauseToError(new Error(message), e) 25 | } 26 | 27 | if (!diagramObject || !diagramObject.data || !diagramObject.data.url) { 28 | return diagramText 29 | } 30 | const read = 'vfs' in context && typeof context.vfs !== 'undefined' && typeof context.vfs.read === 'function' ? context.vfs.read : require('./node-fs.js').read 31 | const data = diagramObject.data 32 | const urlOrPath = data.url 33 | try { 34 | data.values = read(isLocalAndRelative(urlOrPath) ? path.join(diagramDir, urlOrPath) : urlOrPath) 35 | } catch (e) { 36 | if (isRemoteUrl(urlOrPath)) { 37 | // Includes a remote file that cannot be found but might be resolved by the Kroki server (https://github.com/yuzutech/kroki/issues/60) 38 | logger.info(`Skipping preprocessing of Vega-Lite view specification, because reading the remote data file '${urlOrPath}' referenced in the diagram caused an error:\n${e}`) 39 | return diagramText 40 | } 41 | const message = `Preprocessing of Vega-Lite view specification failed, because reading the local data file '${urlOrPath}' referenced in the diagram caused an error:\n${e}` 42 | throw addCauseToError(new Error(message), e) 43 | } 44 | 45 | if (!data.format) { 46 | // Extract extension from URL using snippet from 47 | // http://stackoverflow.com/questions/680929/how-to-extract-extension-from-filename-string-in-javascript 48 | // Same code as in Vega-Lite: 49 | // https://github.com/vega/vega-lite/blob/master/src/compile/data/source.ts 50 | let type = /(?:\.([^.]+))?$/.exec(data.url)[1] 51 | if (['json', 'csv', 'tsv', 'dsv', 'topojson'].indexOf(type) < 0) { 52 | type = 'json' 53 | } 54 | data.format = { type } 55 | } 56 | data.url = undefined 57 | // reconsider once #42 is fixed: 58 | // return JSON.stringify(diagramObject, undefined, 2) 59 | return JSON.stringify(diagramObject) 60 | } 61 | 62 | const plantUmlBlocksRx = /@startuml(?:\r?\n)([\s\S]*?)(?:\r?\n)@enduml/gm 63 | const plantUmlFirstBlockRx = /@startuml(?:\r?\n)([\s\S]*?)(?:\r?\n)@enduml/m 64 | 65 | /** 66 | * Removes all plantuml tags (@startuml/@enduml) from the diagram 67 | * It's possible to have more than one diagram in a single file in the cli version of plantuml 68 | * This does not work for the server, so recent plantuml versions remove the tags before processing the diagram 69 | * We don't want to rely on the server to handle this, so we remove the tags in here before sending the diagram to the server 70 | * 71 | * Some diagrams have special tags (ie. @startmindmap for mindmap) - these are mandatory, so we can't do much about them... 72 | * 73 | * @param diagramText 74 | * @returns {string} diagramText without any plantuml tags 75 | */ 76 | function removePlantUmlTags (diagramText) { 77 | if (diagramText) { 78 | diagramText = diagramText.replace(/^\s*@(startuml|enduml).*\n?/gm, '') 79 | } 80 | return diagramText 81 | } 82 | 83 | /** 84 | * @param {string} diagramText 85 | * @param {any} context 86 | * @param {string} diagramIncludePaths - predefined include paths (can be null) 87 | * @param {{[key: string]: string}} resource - diagram resource identity 88 | * @returns {string} 89 | */ 90 | module.exports.preprocessPlantUML = function (diagramText, context, diagramIncludePaths = '', resource = { dir: '' }) { 91 | const logger = 'logger' in context ? context.logger : console 92 | const includeOnce = [] 93 | const includeStack = [] 94 | const includePaths = diagramIncludePaths ? diagramIncludePaths.split(delimiter) : [] 95 | diagramText = preprocessPlantUmlIncludes(diagramText, resource, includeOnce, includeStack, includePaths, context.vfs, logger) 96 | return removePlantUmlTags(diagramText) 97 | } 98 | 99 | /** 100 | * @param {string} diagramText 101 | * @param {{[key: string]: string}} resource 102 | * @param {string[]} includeOnce 103 | * @param {string[]} includeStack 104 | * @param {string[]} includePaths 105 | * @param {any} vfs 106 | * @param {any} logger 107 | * @returns {string} 108 | */ 109 | function preprocessPlantUmlIncludes (diagramText, resource, includeOnce, includeStack, includePaths, vfs, logger) { 110 | // See: http://plantuml.com/en/preprocessing 111 | // Please note that we cannot use lookbehind for compatibility reasons with Safari: https://caniuse.com/mdn-javascript_builtins_regexp_lookbehind_assertion objects are stateful when they have the global flag set (e.g. /foo/g). 112 | // const regExInclude = /^\s*!(include(?:_many|_once|url|sub)?)\s+((?:(?<=\\)[ ]|[^ ])+)(.*)/ 113 | const regExInclude = /^\s*!(include(?:_many|_once|url|sub)?)\s+([\s\S]*)/ 114 | const diagramLines = diagramText.split('\n') 115 | let insideCommentBlock = false 116 | const diagramProcessed = diagramLines.map(line => { 117 | let result = line 118 | // replace the !include directive unless inside a comment block 119 | if (!insideCommentBlock) { 120 | result = line.replace( 121 | regExInclude, 122 | (match, ...args) => { 123 | const include = args[0].toLowerCase() 124 | const target = parseTarget(args[1]) 125 | const urlSub = target.url.split('!') 126 | const trailingContent = target.comment 127 | const url = urlSub[0].replace(/\\ /g, ' ').replace(/\s+$/g, '') 128 | const sub = urlSub[1] 129 | const result = readPlantUmlInclude(url, resource, includePaths, includeStack, vfs, logger) 130 | if (result.skip) { 131 | return line 132 | } 133 | if (include === 'include_once') { 134 | checkIncludeOnce(result.text, result.filePath, includeOnce) 135 | } 136 | let text = result.text 137 | if (sub !== undefined && sub !== null && sub !== '') { 138 | if (include === 'includesub') { 139 | text = getPlantUmlTextFromSub(text, sub) 140 | } else { 141 | const index = parseInt(sub, 10) 142 | if (isNaN(index)) { 143 | text = getPlantUmlTextFromId(text, sub) 144 | } else { 145 | text = getPlantUmlTextFromIndex(text, index) 146 | } 147 | } 148 | } else { 149 | text = getPlantUmlTextOrFirstBlock(text) 150 | } 151 | includeStack.push(result.filePath) 152 | const parse = typeof vfs !== 'undefined' && typeof vfs.parse === 'function' ? vfs.parse : require('./node-fs.js').parse 153 | text = preprocessPlantUmlIncludes(text, parse(result.filePath, resource), includeOnce, includeStack, includePaths, vfs, logger) 154 | includeStack.pop() 155 | if (trailingContent !== '') { 156 | return text + ' ' + trailingContent 157 | } 158 | return text 159 | }) 160 | } 161 | if (line.includes('/\'')) { 162 | insideCommentBlock = true 163 | } 164 | if (insideCommentBlock && line.includes('\'/')) { 165 | insideCommentBlock = false 166 | } 167 | return result 168 | }) 169 | return diagramProcessed.join('\n') 170 | } 171 | 172 | /** 173 | * @param {string} includeFile - relative or absolute include file 174 | * @param {{[key: string]: string}} resource 175 | * @param {string[]} includePaths - array with include paths 176 | * @param {any} vfs 177 | * @returns {string} the found file or include file path 178 | */ 179 | function resolveIncludeFile (includeFile, resource, includePaths, vfs) { 180 | const exists = typeof vfs !== 'undefined' && typeof vfs.exists === 'function' ? vfs.exists : require('./node-fs.js').exists 181 | if (resource.module) { 182 | // antora resource id 183 | return includeFile 184 | } 185 | let filePath = includeFile 186 | for (const includePath of [resource.dir, ...includePaths]) { 187 | const localFilePath = path.join(includePath, includeFile) 188 | if (exists(localFilePath)) { 189 | filePath = localFilePath 190 | break 191 | } 192 | } 193 | return filePath 194 | } 195 | 196 | function parseTarget (value) { 197 | for (let i = 0; i < value.length; i++) { 198 | const char = value.charAt(i) 199 | if (i > 2) { 200 | // # inline comment 201 | if (char === '#' && value.charAt(i - 1) === ' ' && value.charAt(i - 2) !== '\\') { 202 | return { url: value.substr(0, i - 1).trim(), comment: value.substr(i) } 203 | } 204 | // /' multi-lines comment '/ 205 | if (char === '\'' && value.charAt(i - 1) === '/' && value.charAt(i - 2) !== '\\') { 206 | return { url: value.substr(0, i - 1).trim(), comment: value.substr(i - 1) } 207 | } 208 | } 209 | } 210 | return { url: value, comment: '' } 211 | } 212 | 213 | /** 214 | * @param {string} url 215 | * @param {{[key: string]: string}} resource 216 | * @param {string[]} includePaths 217 | * @param {string[]} includeStack 218 | * @param {any} vfs 219 | * @param {any} logger 220 | * @returns {any} 221 | */ 222 | function readPlantUmlInclude (url, resource, includePaths, includeStack, vfs, logger) { 223 | const read = typeof vfs !== 'undefined' && typeof vfs.read === 'function' ? vfs.read : require('./node-fs.js').read 224 | let skip = false 225 | let text = '' 226 | let filePath = url 227 | if (url.startsWith('<')) { 228 | // Includes a standard library that cannot be resolved locally but might be resolved the by Kroki server 229 | logger.info(`Skipping preprocessing of PlantUML standard library include '${url}'`) 230 | skip = true 231 | } else if (includeStack.includes(url)) { 232 | const message = `Preprocessing of PlantUML include failed, because recursive reading already included referenced file '${url}'` 233 | throw new Error(message) 234 | } else { 235 | if (isRemoteUrl(url)) { 236 | try { 237 | text = read(url) 238 | } catch (e) { 239 | // Includes a remote file that cannot be found but might be resolved by the Kroki server (https://github.com/yuzutech/kroki/issues/60) 240 | logger.info(`Skipping preprocessing of PlantUML include, because reading the referenced remote file '${url}' caused an error:\n${e}`) 241 | skip = true 242 | } 243 | } else { 244 | filePath = resolveIncludeFile(url, resource, includePaths, vfs) 245 | if (includeStack.includes(filePath)) { 246 | const message = `Preprocessing of PlantUML include failed, because recursive reading already included referenced file '${filePath}'` 247 | throw new Error(message) 248 | } else { 249 | try { 250 | text = read(filePath, 'utf8', resource) 251 | } catch (e) { 252 | // Includes a local file that cannot be found but might be resolved by the Kroki server 253 | logger.info(`Skipping preprocessing of PlantUML include, because reading the referenced local file '${filePath}' caused an error:\n${e}`) 254 | skip = true 255 | } 256 | } 257 | } 258 | } 259 | return { skip, text, filePath } 260 | } 261 | 262 | /** 263 | * @param {string} text 264 | * @param {string} sub 265 | * @returns {string} 266 | */ 267 | function getPlantUmlTextFromSub (text, sub) { 268 | const regEx = new RegExp(`!startsub\\s+${sub}(?:\\r\\n|\\n)([\\s\\S]*?)(?:\\r\\n|\\n)!endsub`, 'gm') 269 | return getPlantUmlTextRegEx(text, regEx) 270 | } 271 | 272 | /** 273 | * @param {string} text 274 | * @param {string} id 275 | * @returns {string} 276 | */ 277 | function getPlantUmlTextFromId (text, id) { 278 | const regEx = new RegExp(`@startuml\\(id=${id}\\)(?:\\r\\n|\\n)([\\s\\S]*?)(?:\\r\\n|\\n)@enduml`, 'gm') 279 | return getPlantUmlTextRegEx(text, regEx) 280 | } 281 | 282 | /** 283 | * @param {string} text 284 | * @param {RegExp} regEx 285 | * @returns {string} 286 | */ 287 | function getPlantUmlTextRegEx (text, regEx) { 288 | let matchedStrings = '' 289 | let match = regEx.exec(text) 290 | if (match != null) { 291 | matchedStrings += match[1] 292 | match = regEx.exec(text) 293 | while (match != null) { 294 | matchedStrings += '\n' + match[1] 295 | match = regEx.exec(text) 296 | } 297 | } 298 | return matchedStrings 299 | } 300 | 301 | /** 302 | * @param {string} text 303 | * @param {number} index 304 | * @returns {string} 305 | */ 306 | function getPlantUmlTextFromIndex (text, index) { 307 | // Please note that RegExp objects are stateful when they have the global flag set (e.g. /foo/g). 308 | // They store a lastIndex from the previous match. 309 | // Using exec() multiple times will return the next occurrence. 310 | // Reset to find the first occurrence. 311 | let idx = 0 312 | plantUmlBlocksRx.lastIndex = 0 313 | let match = plantUmlBlocksRx.exec(text) 314 | while (match && idx < index) { 315 | // find the nth occurrence 316 | match = plantUmlBlocksRx.exec(text) 317 | idx++ 318 | } 319 | if (match) { 320 | // [0] - result matching the complete regular expression 321 | // [1] - the first capturing group 322 | return match[1] 323 | } 324 | return '' 325 | } 326 | 327 | /** 328 | * @param {string} text 329 | * @returns {string} 330 | */ 331 | function getPlantUmlTextOrFirstBlock (text) { 332 | const match = text.match(plantUmlFirstBlockRx) 333 | if (match) { 334 | return match[1] 335 | } 336 | return text 337 | } 338 | 339 | /** 340 | * @param {string} text 341 | * @param {string} filePath 342 | * @param {string[]} includeOnce 343 | */ 344 | function checkIncludeOnce (text, filePath, includeOnce) { 345 | if (includeOnce.includes(filePath)) { 346 | const message = `Preprocessing of PlantUML include failed, because including multiple times referenced file '${filePath}' with '!include_once' guard` 347 | throw new Error(message) 348 | } else { 349 | includeOnce.push(filePath) 350 | } 351 | } 352 | 353 | /** 354 | * @param {Error} error 355 | * @param {any} causedBy 356 | * @returns {Error} 357 | */ 358 | function addCauseToError (error, causedBy) { 359 | if (causedBy.stack) { 360 | error.stack += '\nCaused by: ' + causedBy.stack 361 | } 362 | return error 363 | } 364 | 365 | /** 366 | * @param {string} string 367 | * @returns {boolean} 368 | */ 369 | function isRemoteUrl (string) { 370 | try { 371 | const url = new URL(string) 372 | return url.protocol !== 'file:' 373 | } catch (_) { 374 | return false 375 | } 376 | } 377 | 378 | /** 379 | * @param {string} string 380 | * @returns {boolean} 381 | */ 382 | function isLocalAndRelative (string) { 383 | if (string.startsWith('/')) { 384 | return false 385 | } 386 | 387 | try { 388 | // eslint-disable-next-line no-new 389 | new URL(string) 390 | // A URL can not be local AND relative: https://stackoverflow.com/questions/7857416/file-uri-scheme-and-relative-files 391 | return false 392 | } catch (_) { 393 | return true 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /tasks/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Package a distribution as a zip and tar.gz archive 3 | 4 | set -e 5 | 6 | cd "$(dirname "$0")" 7 | cd .. 8 | npm run dist 9 | mkdir bin 10 | cd dist/ 11 | zip -r ../bin/asciidoctor-kroki.dist.zip . 12 | tar -zcvf ../bin/asciidoctor-kroki.dist.tar.gz . 13 | -------------------------------------------------------------------------------- /tasks/publish.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const pacote = require('pacote') // see: http://npm.im/pacote 4 | const { publish: npmPublish } = require('libnpmpublish') 5 | 6 | const publish = async (directory) => { 7 | const pkg = require(path.join(directory, 'package.json')) 8 | if (process.env.DRY_RUN) { 9 | console.log(`${pkg.name}@${pkg.version}`) 10 | } else { 11 | const manifest = await pacote.manifest(directory) 12 | const tarData = await pacote.tarball(directory) 13 | return npmPublish(manifest, tarData, { 14 | access: 'public', 15 | forceAuth: { 16 | token: process.env.NPM_AUTH_TOKEN 17 | } 18 | }) 19 | } 20 | } 21 | 22 | ;(async () => { 23 | try { 24 | if (process.env.DRY_RUN) { 25 | console.warn('Dry run! To publish the release, run the command again without DRY_RUN environment variable') 26 | } 27 | const projectRootDirectory = path.join(__dirname, '..') 28 | await publish(projectRootDirectory) 29 | } catch (e) { 30 | console.log('Unable to publish the package', e) 31 | process.exit(1) 32 | } 33 | })() 34 | -------------------------------------------------------------------------------- /tasks/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Publish the package to npmjs 3 | 4 | set -e 5 | 6 | cd "$(dirname "$0")" 7 | node publish.js 8 | -------------------------------------------------------------------------------- /test/204-server.js: -------------------------------------------------------------------------------- 1 | const { parentPort } = require('node:worker_threads') 2 | const { createServer } = require('node:http') 3 | 4 | const server = createServer(function (req, res) { 5 | res.writeHead(204) 6 | res.end('') 7 | }) 8 | server.listen(0, 'localhost', () => { 9 | parentPort.postMessage({ port: server.address().port }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/500-server.js: -------------------------------------------------------------------------------- 1 | const { parentPort } = require('node:worker_threads') 2 | const { createServer } = require('node:http') 3 | 4 | const server = createServer(function (req, res) { 5 | res.writeHead(500) 6 | res.end('500 Something went bad!') 7 | }) 8 | server.listen(0, 'localhost', () => { 9 | parentPort.postMessage({ port: server.address().port }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/antora/.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | public/ 3 | -------------------------------------------------------------------------------- /test/antora/docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: antora-kroki 2 | title: Antora x Kroki 3 | version: master 4 | start_page: ROOT:index.adoc 5 | nav: 6 | - modules/ROOT/nav.adoc 7 | 8 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/examples/ab-all.puml: -------------------------------------------------------------------------------- 1 | [plantuml,ab-example-all-1,svg] 2 | ---- 3 | alice -> bob 4 | bob -> alice 5 | ---- 6 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/examples/ab.puml: -------------------------------------------------------------------------------- 1 | alice -> bob 2 | bob -> alice 3 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/examples/barley.json: -------------------------------------------------------------------------------- 1 | [{"yield":27,"variety":"Manchuria","year":1931,"site":"University Farm"}, 2 | {"yield":48.86667,"variety":"Manchuria","year":1931,"site":"Waseca"}, 3 | {"yield":27.43334,"variety":"Manchuria","year":1931,"site":"Morris"}, 4 | {"yield":39.93333,"variety":"Manchuria","year":1931,"site":"Crookston"}, 5 | {"yield":32.96667,"variety":"Manchuria","year":1931,"site":"Grand Rapids"}, 6 | {"yield":28.96667,"variety":"Manchuria","year":1931,"site":"Duluth"}, 7 | {"yield":43.06666,"variety":"Glabron","year":1931,"site":"University Farm"}, 8 | {"yield":55.2,"variety":"Glabron","year":1931,"site":"Waseca"}, 9 | {"yield":28.76667,"variety":"Glabron","year":1931,"site":"Morris"}, 10 | {"yield":38.13333,"variety":"Glabron","year":1931,"site":"Crookston"}, 11 | {"yield":29.13333,"variety":"Glabron","year":1931,"site":"Grand Rapids"}, 12 | {"yield":29.66667,"variety":"Glabron","year":1931,"site":"Duluth"}, 13 | {"yield":35.13333,"variety":"Svansota","year":1931,"site":"University Farm"}, 14 | {"yield":47.33333,"variety":"Svansota","year":1931,"site":"Waseca"}, 15 | {"yield":25.76667,"variety":"Svansota","year":1931,"site":"Morris"}, 16 | {"yield":40.46667,"variety":"Svansota","year":1931,"site":"Crookston"}, 17 | {"yield":29.66667,"variety":"Svansota","year":1931,"site":"Grand Rapids"}, 18 | {"yield":25.7,"variety":"Svansota","year":1931,"site":"Duluth"}, 19 | {"yield":39.9,"variety":"Velvet","year":1931,"site":"University Farm"}, 20 | {"yield":50.23333,"variety":"Velvet","year":1931,"site":"Waseca"}, 21 | {"yield":26.13333,"variety":"Velvet","year":1931,"site":"Morris"}, 22 | {"yield":41.33333,"variety":"Velvet","year":1931,"site":"Crookston"}, 23 | {"yield":23.03333,"variety":"Velvet","year":1931,"site":"Grand Rapids"}, 24 | {"yield":26.3,"variety":"Velvet","year":1931,"site":"Duluth"}, 25 | {"yield":36.56666,"variety":"Trebi","year":1931,"site":"University Farm"}, 26 | {"yield":63.8333,"variety":"Trebi","year":1931,"site":"Waseca"}, 27 | {"yield":43.76667,"variety":"Trebi","year":1931,"site":"Morris"}, 28 | {"yield":46.93333,"variety":"Trebi","year":1931,"site":"Crookston"}, 29 | {"yield":29.76667,"variety":"Trebi","year":1931,"site":"Grand Rapids"}, 30 | {"yield":33.93333,"variety":"Trebi","year":1931,"site":"Duluth"}, 31 | {"yield":43.26667,"variety":"No. 457","year":1931,"site":"University Farm"}, 32 | {"yield":58.1,"variety":"No. 457","year":1931,"site":"Waseca"}, 33 | {"yield":28.7,"variety":"No. 457","year":1931,"site":"Morris"}, 34 | {"yield":45.66667,"variety":"No. 457","year":1931,"site":"Crookston"}, 35 | {"yield":32.16667,"variety":"No. 457","year":1931,"site":"Grand Rapids"}, 36 | {"yield":33.6,"variety":"No. 457","year":1931,"site":"Duluth"}, 37 | {"yield":36.6,"variety":"No. 462","year":1931,"site":"University Farm"}, 38 | {"yield":65.7667,"variety":"No. 462","year":1931,"site":"Waseca"}, 39 | {"yield":30.36667,"variety":"No. 462","year":1931,"site":"Morris"}, 40 | {"yield":48.56666,"variety":"No. 462","year":1931,"site":"Crookston"}, 41 | {"yield":24.93334,"variety":"No. 462","year":1931,"site":"Grand Rapids"}, 42 | {"yield":28.1,"variety":"No. 462","year":1931,"site":"Duluth"}, 43 | {"yield":32.76667,"variety":"Peatland","year":1931,"site":"University Farm"}, 44 | {"yield":48.56666,"variety":"Peatland","year":1931,"site":"Waseca"}, 45 | {"yield":29.86667,"variety":"Peatland","year":1931,"site":"Morris"}, 46 | {"yield":41.6,"variety":"Peatland","year":1931,"site":"Crookston"}, 47 | {"yield":34.7,"variety":"Peatland","year":1931,"site":"Grand Rapids"}, 48 | {"yield":32,"variety":"Peatland","year":1931,"site":"Duluth"}, 49 | {"yield":24.66667,"variety":"No. 475","year":1931,"site":"University Farm"}, 50 | {"yield":46.76667,"variety":"No. 475","year":1931,"site":"Waseca"}, 51 | {"yield":22.6,"variety":"No. 475","year":1931,"site":"Morris"}, 52 | {"yield":44.1,"variety":"No. 475","year":1931,"site":"Crookston"}, 53 | {"yield":19.7,"variety":"No. 475","year":1931,"site":"Grand Rapids"}, 54 | {"yield":33.06666,"variety":"No. 475","year":1931,"site":"Duluth"}, 55 | {"yield":39.3,"variety":"Wisconsin No. 38","year":1931,"site":"University Farm"}, 56 | {"yield":58.8,"variety":"Wisconsin No. 38","year":1931,"site":"Waseca"}, 57 | {"yield":29.46667,"variety":"Wisconsin No. 38","year":1931,"site":"Morris"}, 58 | {"yield":49.86667,"variety":"Wisconsin No. 38","year":1931,"site":"Crookston"}, 59 | {"yield":34.46667,"variety":"Wisconsin No. 38","year":1931,"site":"Grand Rapids"}, 60 | {"yield":31.6,"variety":"Wisconsin No. 38","year":1931,"site":"Duluth"}, 61 | {"yield":26.9,"variety":"Manchuria","year":1932,"site":"University Farm"}, 62 | {"yield":33.46667,"variety":"Manchuria","year":1932,"site":"Waseca"}, 63 | {"yield":34.36666,"variety":"Manchuria","year":1932,"site":"Morris"}, 64 | {"yield":32.96667,"variety":"Manchuria","year":1932,"site":"Crookston"}, 65 | {"yield":22.13333,"variety":"Manchuria","year":1932,"site":"Grand Rapids"}, 66 | {"yield":22.56667,"variety":"Manchuria","year":1932,"site":"Duluth"}, 67 | {"yield":36.8,"variety":"Glabron","year":1932,"site":"University Farm"}, 68 | {"yield":37.73333,"variety":"Glabron","year":1932,"site":"Waseca"}, 69 | {"yield":35.13333,"variety":"Glabron","year":1932,"site":"Morris"}, 70 | {"yield":26.16667,"variety":"Glabron","year":1932,"site":"Crookston"}, 71 | {"yield":14.43333,"variety":"Glabron","year":1932,"site":"Grand Rapids"}, 72 | {"yield":25.86667,"variety":"Glabron","year":1932,"site":"Duluth"}, 73 | {"yield":27.43334,"variety":"Svansota","year":1932,"site":"University Farm"}, 74 | {"yield":38.5,"variety":"Svansota","year":1932,"site":"Waseca"}, 75 | {"yield":35.03333,"variety":"Svansota","year":1932,"site":"Morris"}, 76 | {"yield":20.63333,"variety":"Svansota","year":1932,"site":"Crookston"}, 77 | {"yield":16.63333,"variety":"Svansota","year":1932,"site":"Grand Rapids"}, 78 | {"yield":22.23333,"variety":"Svansota","year":1932,"site":"Duluth"}, 79 | {"yield":26.8,"variety":"Velvet","year":1932,"site":"University Farm"}, 80 | {"yield":37.4,"variety":"Velvet","year":1932,"site":"Waseca"}, 81 | {"yield":38.83333,"variety":"Velvet","year":1932,"site":"Morris"}, 82 | {"yield":32.06666,"variety":"Velvet","year":1932,"site":"Crookston"}, 83 | {"yield":32.23333,"variety":"Velvet","year":1932,"site":"Grand Rapids"}, 84 | {"yield":22.46667,"variety":"Velvet","year":1932,"site":"Duluth"}, 85 | {"yield":29.06667,"variety":"Trebi","year":1932,"site":"University Farm"}, 86 | {"yield":49.2333,"variety":"Trebi","year":1932,"site":"Waseca"}, 87 | {"yield":46.63333,"variety":"Trebi","year":1932,"site":"Morris"}, 88 | {"yield":41.83333,"variety":"Trebi","year":1932,"site":"Crookston"}, 89 | {"yield":20.63333,"variety":"Trebi","year":1932,"site":"Grand Rapids"}, 90 | {"yield":30.6,"variety":"Trebi","year":1932,"site":"Duluth"}, 91 | {"yield":26.43334,"variety":"No. 457","year":1932,"site":"University Farm"}, 92 | {"yield":42.2,"variety":"No. 457","year":1932,"site":"Waseca"}, 93 | {"yield":43.53334,"variety":"No. 457","year":1932,"site":"Morris"}, 94 | {"yield":34.33333,"variety":"No. 457","year":1932,"site":"Crookston"}, 95 | {"yield":19.46667,"variety":"No. 457","year":1932,"site":"Grand Rapids"}, 96 | {"yield":22.7,"variety":"No. 457","year":1932,"site":"Duluth"}, 97 | {"yield":25.56667,"variety":"No. 462","year":1932,"site":"University Farm"}, 98 | {"yield":44.7,"variety":"No. 462","year":1932,"site":"Waseca"}, 99 | {"yield":47,"variety":"No. 462","year":1932,"site":"Morris"}, 100 | {"yield":30.53333,"variety":"No. 462","year":1932,"site":"Crookston"}, 101 | {"yield":19.9,"variety":"No. 462","year":1932,"site":"Grand Rapids"}, 102 | {"yield":22.5,"variety":"No. 462","year":1932,"site":"Duluth"}, 103 | {"yield":28.06667,"variety":"Peatland","year":1932,"site":"University Farm"}, 104 | {"yield":36.03333,"variety":"Peatland","year":1932,"site":"Waseca"}, 105 | {"yield":43.2,"variety":"Peatland","year":1932,"site":"Morris"}, 106 | {"yield":25.23333,"variety":"Peatland","year":1932,"site":"Crookston"}, 107 | {"yield":26.76667,"variety":"Peatland","year":1932,"site":"Grand Rapids"}, 108 | {"yield":31.36667,"variety":"Peatland","year":1932,"site":"Duluth"}, 109 | {"yield":30,"variety":"No. 475","year":1932,"site":"University Farm"}, 110 | {"yield":41.26667,"variety":"No. 475","year":1932,"site":"Waseca"}, 111 | {"yield":44.23333,"variety":"No. 475","year":1932,"site":"Morris"}, 112 | {"yield":32.13333,"variety":"No. 475","year":1932,"site":"Crookston"}, 113 | {"yield":15.23333,"variety":"No. 475","year":1932,"site":"Grand Rapids"}, 114 | {"yield":27.36667,"variety":"No. 475","year":1932,"site":"Duluth"}, 115 | {"yield":38,"variety":"Wisconsin No. 38","year":1932,"site":"University Farm"}, 116 | {"yield":58.16667,"variety":"Wisconsin No. 38","year":1932,"site":"Waseca"}, 117 | {"yield":47.16667,"variety":"Wisconsin No. 38","year":1932,"site":"Morris"}, 118 | {"yield":35.9,"variety":"Wisconsin No. 38","year":1932,"site":"Crookston"}, 119 | {"yield":20.66667,"variety":"Wisconsin No. 38","year":1932,"site":"Grand Rapids"}, 120 | {"yield":29.33333,"variety":"Wisconsin No. 38","year":1932,"site":"Duluth"}] 121 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/examples/styles.puml: -------------------------------------------------------------------------------- 1 | !theme spacelab 2 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:index.adoc[Home Page] 2 | * xref:source-location.adoc[Diagram Location] 3 | * xref:embedding.adoc[Choices for generated html] 4 | * xref:embeddingblockmacro.adoc[Choices for generated html, using the block macro] 5 | * xref:attributes.adoc[Choices for positional, named, and page/global attributes] 6 | * xref:topic/index.adoc[Similar diagrams on a topic page] 7 | * xref:resolve-antora-resource-ids.adoc[] 8 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/pages/attributes.adoc: -------------------------------------------------------------------------------- 1 | = {page-component-title} 2 | 3 | == Positional, named, and page/global attribute settings for the plantuml block and block macro 4 | 5 | .no set attributes block. 6 | [plantuml] 7 | ---- 8 | include::partial$ab.puml[] 9 | ---- 10 | 11 | .no set attributes block macro. 12 | plantuml::partial$ab.puml[] 13 | 14 | .target named attribute, without `kroki-fetch-diagram` set, as block. 15 | [plantuml, target=ab-block] 16 | ---- 17 | include::partial$ab.puml[] 18 | ---- 19 | 20 | .target named attribute, without `kroki-fetch-diagram` set, as block macro. 21 | plantuml::partial$ab.puml[target=ab-blockmacro] 22 | 23 | .format named attribute, inline, without `kroki-fetch-diagram` set, as block. 24 | [plantuml, format=svg, options=inline] 25 | ---- 26 | include::partial$ab.puml[] 27 | ---- 28 | 29 | .format named attribute, inline, without `kroki-fetch-diagram` set, as block macro. 30 | plantuml::partial$ab.puml[format=svg, options=inline] 31 | 32 | .format named attribute, without `kroki-fetch-diagram` set, as block. 33 | [plantuml, format=svg] 34 | ---- 35 | include::partial$ab.puml[] 36 | ---- 37 | 38 | .format named attribute, without `kroki-fetch-diagram` set, as block macro. 39 | plantuml::partial$ab.puml[format=svg] 40 | 41 | === Simplest 42 | 43 | :kroki-default-format: svg 44 | :kroki-default-options: inline 45 | 46 | [plantuml] 47 | ---- 48 | include::partial$ab.puml[] 49 | ---- 50 | 51 | .no set attributes block macro. 52 | plantuml::partial$ab.puml[] 53 | 54 | // inline + fetch does not work, see https://github.com/ggrossetie/asciidoctor-kroki/issues/88 55 | //// 56 | === With fetched diagrams (kroki-fetch-diagram set) 57 | 58 | :kroki-fetch-diagram: 59 | 60 | .format named attribute, with `kroki-fetch-diagram` set, as block. 61 | [plantuml, format=svg] 62 | ---- 63 | include::partial$ab.puml[] 64 | ---- 65 | 66 | .format named attribute, with `kroki-fetch-diagram` set, as block macro. 67 | plantuml::partial$ab.puml[format=svg] 68 | //// 69 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/pages/embedding.adoc: -------------------------------------------------------------------------------- 1 | = {page-component-title} 2 | :kroki-default-format: svg 3 | 4 | == Choices for the generated html. 5 | 6 | === Explicit block attributes 7 | 8 | `:kroki-fetch-diagram:` is not yet set on this page, so this uses a remote URL to the plantuml server. 9 | 10 | .As svg 11 | [plantuml] 12 | ---- 13 | alice -> bob 14 | bob -> alice 15 | ---- 16 | 17 | `:kroki-fetch-diagram:` is now set on this page, so the next link will be to downloaded diagrams in `_images`. 18 | 19 | :kroki-fetch-diagram: 20 | 21 | .As svg 22 | [plantuml,target=ab-embedded-e1] 23 | ---- 24 | alice -> bobby 25 | bobby -> alice 26 | ---- 27 | 28 | Inline/interactive requires unsetting `kroki-fetch-diagram` via `:kroki-fetch-diagram!:` 29 | 30 | :kroki-fetch-diagram!: 31 | 32 | .As svg inline 33 | [plantuml,options=inline] 34 | ---- 35 | alice -> robert 36 | robert -> alice 37 | ---- 38 | 39 | .As svg interactive 40 | [plantuml,options=interactive] 41 | ---- 42 | alicia -> bob 43 | bob -> alicia 44 | ---- 45 | 46 | === Default page attribute `:kroki-default-options: inline` 47 | 48 | This is not yet implemented 49 | 50 | :kroki-default-options: inline 51 | 52 | .As svg inline from page attribute 53 | [plantuml] 54 | ---- 55 | alice -> robert 56 | robert -> alice 57 | ---- 58 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/pages/embeddingblockmacro.adoc: -------------------------------------------------------------------------------- 1 | = {page-component-title} 2 | :kroki-default-format: svg 3 | 4 | == Choices for the generated html. 5 | 6 | === Explicit block attributes 7 | 8 | `:kroki-fetch-diagram:` is not yet set on this page, so this uses a remote URL to the plantuml server. 9 | 10 | .As svg 11 | plantuml::partial$ab.puml[] 12 | 13 | `:kroki-fetch-diagram:` is now set on this page, so any further links will be to downloaded diagrams in `_images`. 14 | 15 | :kroki-fetch-diagram: 16 | 17 | .As svg 18 | plantuml::partial$ab.puml[target=ab-embedded-em1] 19 | 20 | Inline/interactive requires unsetting `kroki-fetch-diagram` via `:kroki-fetch-diagram!:` 21 | 22 | :kroki-fetch-diagram!: 23 | 24 | .As svg inline 25 | plantuml::partial$ab.puml[options=inline] 26 | 27 | .As svg interactive 28 | plantuml::partial$ab.puml[options=interactive] 29 | 30 | === Default page attribute `:kroki-default-options: inline` 31 | 32 | :kroki-default-options: inline 33 | 34 | .As svg inline from page attribute 35 | plantuml::partial$ab.puml[] 36 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/pages/index.adoc: -------------------------------------------------------------------------------- 1 | = {page-component-title} 2 | 3 | Ways of using https://github.com/ggrossetie/asciidoctor-kroki[asciidoctor-kroki]. 4 | 5 | For this example, you must install `asciidoctor-kroki` so it is accessible to your Antora installation. 6 | If you have installed Antora globally, you can run: 7 | 8 | [source] 9 | ---- 10 | npm install -g asciidoctor-kroki 11 | ---- 12 | 13 | See the `package.json` for an alternate more self-contained installation method. 14 | 15 | The HTML pages in a browser will look rather repetitive. 16 | It is perhaps more interesting to look at the HTML source to see the effects of the different configurations. 17 | 18 | Currently, this example only explores PlantUML source format diagrams. 19 | Except `ditaa`, for which only `png` output is supported, other diagram types should have similar capabilities and configuration. 20 | 21 | xref:source-location.adoc[Choices for source diagram location] 22 | 23 | This page demonstrates the possible locations for the diagram source using a `[plantuml]` block. 24 | 25 | xref:embedding.adoc[Choices for generated HTML] 26 | 27 | This page demonstrates some choices for the style of generated HTML. 28 | Since `data-uri` is not officially supported by Antora, and currently does not work for `png` impages pending an asciidoctor upgrade, there are no `data-uri` examples. 29 | 30 | xref:embeddingblockmacro.adoc[Choices for generated HTML, using the block macro] 31 | 32 | This demonstrates the same choices but using the `plantuml::` block macro with a reference to the diagram source in `partials` 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/pages/resolve-antora-resource-ids.adoc: -------------------------------------------------------------------------------- 1 | = Resolve Antora Resource IDs 2 | 3 | == Vega-Lite 4 | 5 | ..... 6 | [vegalite] 7 | .... 8 | { 9 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 10 | "data": {"url": "example$barley.json"}, 11 | "mark": "bar", 12 | "encoding": { 13 | "x": {"aggregate": "sum", "field": "yield"}, 14 | "y": {"field": "variety"}, 15 | "color": {"field": "site"} 16 | } 17 | } 18 | .... 19 | ..... 20 | 21 | [vegalite] 22 | .... 23 | { 24 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 25 | "data": {"url": "example$barley.json"}, 26 | "mark": "bar", 27 | "encoding": { 28 | "x": {"aggregate": "sum", "field": "yield"}, 29 | "y": {"field": "variety"}, 30 | "color": {"field": "site"} 31 | } 32 | } 33 | .... 34 | 35 | == PlantUML 36 | 37 | ..... 38 | [plantuml] 39 | .... 40 | !include example$styles.puml 41 | 42 | alice -> bob 43 | bob -> alice 44 | .... 45 | 46 | ..... 47 | [plantuml] 48 | .... 49 | !include example$styles.puml 50 | 51 | alice -> bob 52 | bob -> alice 53 | .... 54 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/pages/source-location.adoc: -------------------------------------------------------------------------------- 1 | = {page-component-title} 2 | :kroki-fetch-diagram: 3 | 4 | `:kroki-fetch-diagram:` is set on this page, so all diagrams are downloaded during the build to local files accessed at `_images`. 5 | 6 | == Inline definition 7 | 8 | .... 9 | [plantuml,target=ab-inline-output-svg,format=svg] 10 | ---- 11 | alice -> bob 12 | bob -> alice 13 | ---- 14 | .... 15 | 16 | [plantuml,target=ab-inline-output-svg,format=svg] 17 | ---- 18 | alice -> bob 19 | bob -> alice 20 | ---- 21 | 22 | .... 23 | [plantuml,target=ab-inline-output-png,format=png] 24 | ---- 25 | alice -> bob 26 | bob -> alice 27 | ---- 28 | .... 29 | 30 | [plantuml,target=ab-inline-output-png,format=png] 31 | ---- 32 | alice -> bob 33 | bob -> alice 34 | ---- 35 | 36 | == Include a partial (.adoc extension) 37 | 38 | .... 39 | \include::partial$ab-all.adoc[] 40 | .... 41 | 42 | include::partial$ab-all.adoc[] 43 | 44 | == Include a partial in a plantuml block 45 | 46 | .... 47 | [plantuml,target=ab-partial-1,format=svg] 48 | ---- 49 | \include::partial$ab.puml[] 50 | ---- 51 | .... 52 | 53 | [plantuml,target=ab-partial-1,format=svg] 54 | ---- 55 | include::partial$ab.puml[] 56 | ---- 57 | 58 | == Include a partial using plantuml macro 59 | 60 | .... 61 | plantuml::partial$ab_inc.puml[target=ab-inc-partial-1,format=svg] 62 | .... 63 | 64 | plantuml::partial$ab_inc.puml[target=ab-inc-partial-1,format=svg] 65 | 66 | NOTE: `ab_inc.puml` is using the PlantUML `!include` directive to include another file. 67 | 68 | == Include an example (.puml extension) 69 | 70 | .... 71 | \include::example$ab-all.puml[] 72 | .... 73 | 74 | include::example$ab-all.puml[] 75 | 76 | == Include an example in a plantuml block 77 | 78 | .... 79 | [plantuml,ab-example-1,svg] 80 | ---- 81 | \include::example$ab.puml[] 82 | ---- 83 | .... 84 | 85 | [plantuml,ab-example-1,svg] 86 | ---- 87 | include::example$ab.puml[] 88 | ---- 89 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/pages/topic/index.adoc: -------------------------------------------------------------------------------- 1 | = {page-component-title} 2 | 3 | == Similar links from a topic page. 4 | 5 | `:kroki-fetch-diagram:` is not set on this page, so all diagrams use remote URLs to the plantuml server. 6 | 7 | === Embedded 8 | 9 | .As svg 10 | [plantuml,ab-embedded-svg,svg] 11 | ---- 12 | alice -> bob 13 | bob -> alice 14 | ---- 15 | 16 | .As png 17 | [plantuml,ab-embedded-png,png] 18 | ---- 19 | alice -> bob 20 | bob -> alice 21 | ---- 22 | 23 | === Entire Diagram in a partial 24 | 25 | include::partial$ab-all.adoc[] 26 | 27 | === Diagram contents in a partial 28 | 29 | .As svg 30 | [plantuml,target=ab-partial,format=svg] 31 | ---- 32 | include::partial$ab.puml[] 33 | ---- 34 | 35 | === Entire Diagram in an example 36 | 37 | include::example$ab-all.puml[] 38 | 39 | === Diagram contents in an example 40 | 41 | .As png 42 | [plantuml,ab-example,png] 43 | ---- 44 | include::example$ab.puml[] 45 | ---- 46 | 47 | == C4 Diagram with !include 48 | 49 | [c4plantuml] 50 | ---- 51 | @startuml 52 | !include 53 | 54 | Person(personAlias, "Label", "Optional Description") 55 | Container(containerAlias, "Label", "Technology", "Optional Description") 56 | System(systemAlias, "Label", "Optional Description") 57 | 58 | System_Ext(extSystemAlias, "Label", "Optional Description") 59 | 60 | Rel(personAlias, containerAlias, "Label", "Optional Technology") 61 | 62 | Rel_U(systemAlias, extSystemAlias, "Label", "Optional Technology") 63 | @enduml 64 | ---- 65 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/partials/ab-all.adoc: -------------------------------------------------------------------------------- 1 | [plantuml,ab-partial-all-1,svg] 2 | ---- 3 | alice -> bob 4 | bob -> alice 5 | ---- 6 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/partials/ab.puml: -------------------------------------------------------------------------------- 1 | alice -> bob 2 | bob -> alice 3 | -------------------------------------------------------------------------------- /test/antora/docs/modules/ROOT/partials/ab_inc.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include ab.puml 3 | @enduml 4 | -------------------------------------------------------------------------------- /test/antora/site-remote.yml: -------------------------------------------------------------------------------- 1 | runtime: 2 | cache_dir: ./.cache/antora 3 | 4 | site: 5 | title: Antora x Kroki 6 | url: http://example.com 7 | start_page: antora-kroki::index.adoc 8 | 9 | content: 10 | sources: 11 | - url: https://github.com/ggrossetie/asciidoctor-kroki.git 12 | branches: master 13 | start_path: test/antora/docs 14 | 15 | asciidoc: 16 | extensions: 17 | - ./../../src/asciidoctor-kroki.js 18 | attributes: 19 | allow-uri-read: true 20 | 21 | ui: 22 | bundle: 23 | url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/master/raw/build/ui-bundle.zip?job=bundle-stable 24 | snapshot: true 25 | 26 | output: 27 | dir: ./public 28 | clean: true 29 | -------------------------------------------------------------------------------- /test/antora/site.yml: -------------------------------------------------------------------------------- 1 | runtime: 2 | cache_dir: ./.cache/antora 3 | 4 | site: 5 | title: Antora x Kroki 6 | url: http://example.com 7 | start_page: antora-kroki::index.adoc 8 | 9 | content: 10 | sources: 11 | - url: ./../.. 12 | branches: HEAD 13 | start_path: test/antora/docs 14 | 15 | asciidoc: 16 | extensions: 17 | - ./../../src/asciidoctor-kroki.js 18 | attributes: 19 | allow-uri-read: true 20 | 21 | ui: 22 | bundle: 23 | url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/master/raw/build/ui-bundle.zip?job=bundle-stable 24 | snapshot: true 25 | 26 | output: 27 | dir: ./public 28 | clean: true 29 | -------------------------------------------------------------------------------- /test/antora/test.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it before */ 2 | const fs = require('fs') 3 | const cheerio = require('cheerio') 4 | const chai = require('chai') 5 | const expect = chai.expect 6 | chai.use(require('chai-string')) 7 | chai.use(require('dirty-chai')) 8 | 9 | const generateSite = require('@antora/site-generator-default') 10 | 11 | describe('Antora integration (local)', function () { 12 | this.timeout(90000) 13 | before(async () => { 14 | fs.rmSync(`${__dirname}/public`, { recursive: true, force: true }) 15 | await generateSite([`--playbook=${__dirname}/site.yml`]) 16 | }) 17 | it('should generate a site with diagrams', () => { 18 | const $ = cheerio.load(fs.readFileSync(`${__dirname}/public/antora-kroki/source-location.html`)) 19 | const imageElements = $('img') 20 | expect(imageElements.length).to.equal(7) 21 | imageElements.each((i, imageElement) => { 22 | const src = $(imageElement).attr('src') 23 | expect(src).to.startWith('_images/ab-') 24 | }) 25 | }) 26 | it('should resolve included diagrams when using plantuml::partial$xxx.puml[] macro', async () => { 27 | const $ = cheerio.load(fs.readFileSync(`${__dirname}/public/antora-kroki/source-location.html`)) 28 | const imageElement = $('img[alt*=ab-inc-partial-1]') 29 | expect(imageElement.length).to.equal(1) 30 | const src = imageElement.attr('src') 31 | const diagramContents = fs.readFileSync(`${__dirname}/public/antora-kroki/${src}`).toString() 32 | expect(diagramContents).includes('alice') 33 | expect(diagramContents).includes('bob') 34 | }) 35 | }) 36 | 37 | describe('Antora integration (remote)', function () { 38 | this.timeout(90000) 39 | before(async () => { 40 | fs.rmSync(`${__dirname}/public`, { recursive: true, force: true }) 41 | await generateSite([`--playbook=${__dirname}/site-remote.yml`]) 42 | }) 43 | it('should generate a site with diagrams', () => { 44 | const $ = cheerio.load(fs.readFileSync(`${__dirname}/public/antora-kroki/source-location.html`)) 45 | const imageElements = $('img') 46 | expect(imageElements.length).to.equal(7) 47 | imageElements.each((i, imageElement) => { 48 | const src = $(imageElement).attr('src') 49 | expect(src).to.startWith('_images/ab-') 50 | }) 51 | }) 52 | it('should resolve included diagrams when using plantuml::partial$xxx.puml[] macro', async () => { 53 | const $ = cheerio.load(fs.readFileSync(`${__dirname}/public/antora-kroki/source-location.html`)) 54 | const imageElement = $('img[alt*=ab-inc-partial-1]') 55 | expect(imageElement.length).to.equal(1) 56 | const src = imageElement.attr('src') 57 | const diagramContents = fs.readFileSync(`${__dirname}/public/antora-kroki/${src}`).toString() 58 | expect(diagramContents).includes('alice') 59 | expect(diagramContents).includes('bob') 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/block-attributes.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | const chai = require('chai') 3 | const expect = chai.expect 4 | const dirtyChai = require('dirty-chai') 5 | 6 | chai.use(dirtyChai) 7 | 8 | const asciidoctorKroki = require('../src/asciidoctor-kroki.js') 9 | const asciidoctor = require('@asciidoctor/core')() 10 | 11 | describe('Block attributes', function () { 12 | this.timeout(30000) 13 | describe('When extension is registered', () => { 14 | it('should convert a diagram with an explicit width and height', () => { 15 | const input = ` 16 | [plantuml,alice-bob,svg,width=100%,height=100%] 17 | .... 18 | alice -> bob 19 | .... 20 | ` 21 | const registry = asciidoctor.Extensions.create() 22 | asciidoctorKroki.register(registry) 23 | const html = asciidoctor.convert(input, { extension_registry: registry }) 24 | expect(html).to.equal(`
25 |
26 | alice-bob 27 |
28 |
`) 29 | }) 30 | it('should convert a diagram with a title', () => { 31 | const input = ` 32 | .alice and bob 33 | [plantuml,alice-bob,svg] 34 | .... 35 | alice -> bob 36 | .... 37 | ` 38 | const registry = asciidoctor.Extensions.create() 39 | asciidoctorKroki.register(registry) 40 | const html = asciidoctor.convert(input, { extension_registry: registry }) 41 | expect(html).to.equal(`
42 |
43 | alice and bob 44 |
45 |
Figure 1. alice and bob
46 |
`) 47 | }) 48 | it('should convert a diagram with a caption', () => { 49 | const input = ` 50 | .alice and bob 51 | [plantuml,alice-bob,svg,caption="Figure A. "] 52 | .... 53 | alice -> bob 54 | .... 55 | ` 56 | const registry = asciidoctor.Extensions.create() 57 | asciidoctorKroki.register(registry) 58 | const html = asciidoctor.convert(input, { extension_registry: registry }) 59 | expect(html).to.equal(`
60 |
61 | alice and bob 62 |
63 |
Figure A. alice and bob
64 |
`) 65 | }) 66 | it('should convert a diagram with the float attribute', () => { 67 | const input = ` 68 | [plantuml,alice-bob,svg,float=left] 69 | .... 70 | alice -> bob 71 | .... 72 | ` 73 | const registry = asciidoctor.Extensions.create() 74 | asciidoctorKroki.register(registry) 75 | const html = asciidoctor.convert(input, { extension_registry: registry }) 76 | expect(html).to.equal(`
77 |
78 | alice-bob 79 |
80 |
`) 81 | }) 82 | it('should automatically increment caption if diagrams has title and caption is enabled', () => { 83 | const input = ` 84 | .alice and bob 85 | [plantuml,alice-bob,svg] 86 | .... 87 | alice -> bob 88 | .... 89 | 90 | .dan and andre 91 | [plantuml,dan-andre,svg] 92 | .... 93 | dan -> andre 94 | .... 95 | ` 96 | const registry = asciidoctor.Extensions.create() 97 | asciidoctorKroki.register(registry) 98 | const html = asciidoctor.convert(input, { extension_registry: registry }) 99 | expect(html).to.equal(`
100 |
101 | alice and bob 102 |
103 |
Figure 1. alice and bob
104 |
105 |
106 |
107 | dan and andre 108 |
109 |
Figure 2. dan and andre
110 |
`) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Asciidoctor Kroki Extension tests 5 | 6 | 7 | 8 | 9 |
10 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/browser/run.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, es6 */ 2 | const path = require('node:path') 3 | const puppeteer = require('puppeteer') 4 | 5 | // puppeteer options 6 | const opts = { 7 | headless: 'new', 8 | timeout: 10000, 9 | args: ['--allow-file-access-from-files', '--no-sandbox'] 10 | } 11 | 12 | const log = async (msg) => { 13 | const args = [] 14 | for (let i = 0; i < msg.args().length; ++i) { 15 | args.push(await msg.args()[i].jsonValue()) 16 | } 17 | const type = msg.type() 18 | let log 19 | if (type === 'warning') { 20 | log = console.warn 21 | } else { 22 | log = console[msg.type()] 23 | } 24 | if (args.length === 0) { 25 | log.apply(this, [msg._text]) 26 | } else { 27 | log.apply(this, args) 28 | } 29 | return args 30 | }; 31 | 32 | (async function () { 33 | try { 34 | const browser = await puppeteer.launch(opts) 35 | const page = await browser.newPage() 36 | page.exposeFunction('mochaOpts', () => ({ reporter: 'spec' })) 37 | page.on('console', async (msg) => { 38 | const args = await log(msg) 39 | if (args[0] && typeof args[0] === 'string') { 40 | if (args[0] === '%d failures') { 41 | process.exit(parseInt(args[1])) 42 | } else if (args[0].startsWith('Unable to start the browser tests suite')) { 43 | process.exit(1) 44 | } 45 | } 46 | }) 47 | await page.goto('file://' + path.join(__dirname, 'index.html'), { waitUntil: 'networkidle2' }) 48 | browser.close() 49 | } catch (err) { 50 | console.error('Unable to run tests using Puppeteer', err) 51 | process.exit(1) 52 | } 53 | })().catch((err) => { 54 | console.error('Unable to launch Chrome with Puppeteer', err) 55 | process.exit(1) 56 | }) 57 | -------------------------------------------------------------------------------- /test/browser/test.js: -------------------------------------------------------------------------------- 1 | /* global it, describe, mocha, chai, XMLHttpRequest, AsciidoctorKroki, mochaOpts, pako, base64js */ 2 | import Asciidoctor from '../../node_modules/@asciidoctor/core/dist/browser/asciidoctor.js' 3 | 4 | const httpGet = (uri, encoding = 'utf8') => { 5 | let data = '' 6 | let status = -1 7 | try { 8 | const xhr = new XMLHttpRequest() 9 | xhr.open('GET', uri, false) 10 | if (encoding === 'binary') { 11 | xhr.responseType = 'arraybuffer' 12 | } 13 | xhr.addEventListener('load', function () { 14 | status = this.status 15 | if (status === 200 || status === 0) { 16 | if (encoding === 'binary') { 17 | const arrayBuffer = xhr.response 18 | const byteArray = new Uint8Array(arrayBuffer) 19 | for (let i = 0; i < byteArray.byteLength; i++) { 20 | data += String.fromCharCode(byteArray[i]) 21 | } 22 | } else { 23 | data = this.responseText 24 | } 25 | } 26 | }) 27 | xhr.send() 28 | } catch (e) { 29 | throw new Error(`Error reading file: ${uri}; reason: ${e.message}`) 30 | } 31 | // assume that no data means it doesn't exist 32 | if (status === 404 || !data) { 33 | throw new Error(`No such file: ${uri}`) 34 | } 35 | return data 36 | } 37 | 38 | (async () => { 39 | let reporter 40 | if (typeof mochaOpts === 'function') { 41 | reporter = await mochaOpts().reporter 42 | } else { 43 | reporter = 'html' 44 | } 45 | mocha.setup({ 46 | ui: 'bdd', 47 | checkLeaks: false, 48 | reporter 49 | }) 50 | 51 | const expect = chai.expect 52 | const asciidoctor = Asciidoctor({ runtime: { platform: 'browser' } }) 53 | const parts = window.location.href.split('/') // break the string into an array 54 | parts.pop() 55 | parts.pop() 56 | parts.pop() 57 | const baseDir = parts.join('/') 58 | function encodeText (text) { 59 | const buffer = pako.deflate(text, { level: 9 }) 60 | return base64js.fromByteArray(buffer) 61 | .replace(/\+/g, '-') 62 | .replace(/\//g, '_') 63 | } 64 | 65 | describe('Conversion', () => { 66 | describe('When extension is registered', () => { 67 | it('should convert a diagram to an image', () => { 68 | const input = ` 69 | [plantuml,alice-bob,png,role=sequence] 70 | .... 71 | alice -> bob 72 | .... 73 | ` 74 | const registry = asciidoctor.Extensions.create() 75 | AsciidoctorKroki.register(registry) 76 | const html = asciidoctor.convert(input, { extension_registry: registry }) 77 | expect(html).to.contain('https://kroki.io/plantuml/png/eNpLzMlMTlXQtVNIyk8CABoDA90=') 78 | expect(html).to.contain('
') 79 | }) 80 | it('should convert a diagram with an absolute path to an image', () => { 81 | const input = `plantuml::${baseDir}/test/fixtures/alice.puml[svg,role=sequence]` 82 | const registry = asciidoctor.Extensions.create() 83 | AsciidoctorKroki.register(registry, { 84 | vfs: { 85 | read: (path, encoding = 'utf8') => { 86 | return httpGet(path, encoding) 87 | }, 88 | exists: (_) => { 89 | return false 90 | }, 91 | add: (_) => { 92 | // no-op 93 | } 94 | } 95 | }) 96 | const text = httpGet(`${baseDir}/test/fixtures/alice.puml`, 'utf8') 97 | const html = asciidoctor.convert(input, { extension_registry: registry }) 98 | expect(html).to.contain(`Diagram`) 99 | }).timeout(5000) 100 | it('should convert a diagram with a relative path to an image', () => { 101 | const input = 'plantuml::../fixtures/alice.puml[svg,role=sequence]' 102 | const registry = asciidoctor.Extensions.create() 103 | AsciidoctorKroki.register(registry, { 104 | vfs: { 105 | read: (path, encoding = 'utf8') => { 106 | return httpGet(path, encoding) 107 | }, 108 | exists: (_) => { 109 | return false 110 | }, 111 | add: (_) => { 112 | // no-op 113 | } 114 | } 115 | }) 116 | const text = httpGet(`${baseDir}/test/fixtures/alice.puml`, 'utf8') 117 | const html = asciidoctor.convert(input, { extension_registry: registry }) 118 | expect(html).to.contain(`Diagram`) 119 | }).timeout(5000) 120 | }) 121 | }) 122 | 123 | mocha.run(function (failures) { 124 | if (failures > 0) { 125 | console.error('%d failures', failures) 126 | } 127 | }) 128 | })().catch(err => { 129 | console.error('Unable to start the browser tests suite', { message: err.message }) 130 | }) 131 | -------------------------------------------------------------------------------- /test/fixtures/alice.puml: -------------------------------------------------------------------------------- 1 | alice -> bob -------------------------------------------------------------------------------- /test/fixtures/chart.vlite: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 3 | "description": "Horizontally concatenated charts that show different types of discretizing scales.", 4 | "data": { 5 | "values": [ 6 | {"a": "A", "b": 28}, 7 | {"a": "B", "b": 55}, 8 | {"a": "C", "b": 43}, 9 | {"a": "D", "b": 91}, 10 | {"a": "E", "b": 81}, 11 | {"a": "F", "b": 53}, 12 | {"a": "G", "b": 19}, 13 | {"a": "H", "b": 87}, 14 | {"a": "I", "b": 52} 15 | ] 16 | }, 17 | "hconcat": [ 18 | { 19 | "mark": "circle", 20 | "encoding": { 21 | "y": { 22 | "field": "b", 23 | "type": "nominal", 24 | "sort": null, 25 | "axis": { 26 | "ticks": false, 27 | "domain": false, 28 | "title": null 29 | } 30 | }, 31 | "size": { 32 | "field": "b", 33 | "type": "quantitative", 34 | "scale": { 35 | "type": "quantize" 36 | } 37 | }, 38 | "color": { 39 | "field": "b", 40 | "type": "quantitative", 41 | "scale": { 42 | "type": "quantize", 43 | "zero": true 44 | }, 45 | "legend": { 46 | "title": "Quantize" 47 | } 48 | } 49 | } 50 | }, 51 | { 52 | "mark": "circle", 53 | "encoding": { 54 | "y": { 55 | "field": "b", 56 | "type": "nominal", 57 | "sort": null, 58 | "axis": { 59 | "ticks": false, 60 | "domain": false, 61 | "title": null 62 | } 63 | }, 64 | "size": { 65 | "field": "b", 66 | "type": "quantitative", 67 | "scale": { 68 | "type": "quantile", 69 | "range": [80, 160, 240, 320, 400] 70 | } 71 | }, 72 | "color": { 73 | "field": "b", 74 | "type": "quantitative", 75 | "scale": { 76 | "type": "quantile", 77 | "scheme": "magma" 78 | }, 79 | "legend": { 80 | "format": "d", 81 | "title": "Quantile" 82 | } 83 | } 84 | } 85 | }, 86 | { 87 | "mark": "circle", 88 | "encoding": { 89 | "y": { 90 | "field": "b", 91 | "type": "nominal", 92 | "sort": null, 93 | "axis": { 94 | "ticks": false, 95 | "domain": false, 96 | "title": null 97 | } 98 | }, 99 | "size": { 100 | "field": "b", 101 | "type": "quantitative", 102 | "scale": { 103 | "type": "threshold", 104 | "domain": [30, 70], 105 | "range": [80, 200, 320] 106 | } 107 | }, 108 | "color": { 109 | "field": "b", 110 | "type": "quantitative", 111 | "scale": { 112 | "type": "threshold", 113 | "domain": [30, 70], 114 | "scheme": "viridis" 115 | }, 116 | "legend": { 117 | "title": "Threshold" 118 | } 119 | } 120 | } 121 | } 122 | ], 123 | "resolve": { 124 | "scale": { 125 | "color": "independent", 126 | "size": "independent" 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /test/fixtures/docs/data.adoc: -------------------------------------------------------------------------------- 1 | = Exploring Data 2 | 3 | For this tutorial, we will create visualizations to explore weather data for Seattle, taken from NOAA. 4 | 5 | vegalite::diagrams/weather.vlite[] -------------------------------------------------------------------------------- /test/fixtures/docs/diagrams/data/seattle-weather.csv: -------------------------------------------------------------------------------- 1 | date,precipitation,temp_max,temp_min,wind,weather 2 | 2015-01-01,0.0,5.6,-3.2,1.2,sun 3 | 2015-01-02,1.5,5.6,0.0,2.3,rain 4 | 2015-01-03,0.0,5.0,1.7,1.7,fog 5 | 2015-01-04,10.2,10.6,3.3,4.5,rain 6 | 2015-01-05,8.1,12.2,9.4,6.4,rain 7 | 2015-01-06,0.0,12.2,6.1,1.3,fog 8 | 2015-01-07,0.0,7.8,5.6,1.6,fog 9 | 2015-01-08,0.0,7.8,1.7,2.6,fog 10 | 2015-01-09,0.3,10.0,3.3,0.6,rain 11 | 2015-01-10,5.8,7.8,6.1,0.5,rain 12 | 2015-01-11,1.5,9.4,7.2,1.1,rain 13 | 2015-01-12,0.0,11.1,4.4,1.6,fog 14 | 2015-01-13,0.0,9.4,2.8,2.7,fog 15 | 2015-01-14,0.0,6.1,0.6,2.8,fog 16 | 2015-01-15,9.7,7.8,1.1,3.2,rain 17 | 2015-01-16,0.0,11.7,5.6,4.5,fog 18 | 2015-01-17,26.2,13.3,3.3,2.8,rain 19 | 2015-01-18,21.3,13.9,7.2,6.6,rain 20 | 2015-01-19,0.5,10.0,6.1,2.8,rain 21 | 2015-01-20,0.0,10.0,3.3,3.0,fog 22 | 2015-01-21,0.0,7.2,-0.5,1.3,fog 23 | 2015-01-22,0.8,9.4,6.1,1.3,rain 24 | 2015-01-23,5.8,12.2,8.3,2.6,rain 25 | 2015-01-24,0.5,14.4,11.1,3.3,rain 26 | 2015-01-25,0.0,17.2,7.2,1.4,fog 27 | 2015-01-26,0.0,16.1,6.1,2.2,fog 28 | 2015-01-27,0.8,11.1,8.3,2.0,rain 29 | 2015-01-28,0.0,12.2,5.0,1.8,fog 30 | 2015-01-29,0.0,12.2,3.3,2.9,sun 31 | 2015-01-30,0.0,8.3,1.1,0.8,fog 32 | 2015-01-31,0.0,7.2,3.3,1.9,fog -------------------------------------------------------------------------------- /test/fixtures/docs/diagrams/hello.puml: -------------------------------------------------------------------------------- 1 | !include ./style.puml 2 | 3 | Bob->Alice: Hello -------------------------------------------------------------------------------- /test/fixtures/docs/diagrams/style.puml: -------------------------------------------------------------------------------- 1 | skinparam monochrome true -------------------------------------------------------------------------------- /test/fixtures/docs/diagrams/weather.vlite: -------------------------------------------------------------------------------- 1 | { 2 | "data": {"url": "data/seattle-weather.csv"}, 3 | "mark": "tick", 4 | "encoding": { 5 | "x": {"field": "precipitation", "type": "quantitative"} 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/docs/hello.adoc: -------------------------------------------------------------------------------- 1 | plantuml::diagrams/hello.puml[] -------------------------------------------------------------------------------- /test/fixtures/expected/alice-bluegray.svg: -------------------------------------------------------------------------------- 1 | alicealicebobbob -------------------------------------------------------------------------------- /test/fixtures/expected/alice.svg: -------------------------------------------------------------------------------- 1 | alicealicebobbob -------------------------------------------------------------------------------- /test/fixtures/expected/chart.svg: -------------------------------------------------------------------------------- 1 | 285543918153198752< 3737 – 5555 – 73≥ 73Quantize285543918153198752< 3737 – 5252 – 5555 – 83≥ 83Quantile285543918153198752< 3030 – 70≥ 70Threshold -------------------------------------------------------------------------------- /test/fixtures/fetch/doc.adoc: -------------------------------------------------------------------------------- 1 | :imagesdir: media 2 | :kroki-server-url: https://kroki.io 3 | :kroki-fetch-diagram: 4 | 5 | [plantuml,alice-bob,svg] 6 | .... 7 | alice -> bob 8 | .... -------------------------------------------------------------------------------- /test/fixtures/macro/doc.adoc: -------------------------------------------------------------------------------- 1 | :kroki-server-url: https://kroki.io 2 | 3 | plantuml::../alice.puml[svg,role=sequence] -------------------------------------------------------------------------------- /test/fixtures/plantuml/alice-with-styles.puml: -------------------------------------------------------------------------------- 1 | !include styles/general.iuml 2 | alice -> bob -------------------------------------------------------------------------------- /test/fixtures/plantuml/diagrams/hello-with-base-and-note.puml: -------------------------------------------------------------------------------- 1 | !include base.iuml 2 | !include note.iuml 3 | Bob->Alice: Hello -------------------------------------------------------------------------------- /test/fixtures/plantuml/diagrams/hello-with-style.puml: -------------------------------------------------------------------------------- 1 | !include style.iuml 2 | Bob->Alice: Hello -------------------------------------------------------------------------------- /test/fixtures/plantuml/diagrams/id.puml: -------------------------------------------------------------------------------- 1 | @startuml(id=MY_OWN_ID1) 2 | A -> A : stuff1 3 | B -> B : stuff2 4 | @enduml 5 | 6 | @startuml(id=MY_OWN_ID2) 7 | C -> C : stuff3 8 | D -> D : stuff4 9 | @enduml -------------------------------------------------------------------------------- /test/fixtures/plantuml/diagrams/index.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | A -> A : stuff1 3 | B -> B : stuff2 4 | @enduml 5 | 6 | @startuml 7 | C -> C : stuff3 8 | D -> D : stuff4 9 | @enduml -------------------------------------------------------------------------------- /test/fixtures/plantuml/diagrams/subs.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | A -> A : stuff1 3 | !startsub BASIC 4 | B -> B : stuff2 5 | B -> B : stuff2.1 6 | !endsub 7 | C -> C : stuff3 8 | !startsub BASIC 9 | D -> D : stuff4 10 | D -> D : stuff4.1 11 | !endsub 12 | @enduml -------------------------------------------------------------------------------- /test/fixtures/plantuml/hello.puml: -------------------------------------------------------------------------------- 1 | !include styles/general.iuml 2 | Bob->Alice: Hello -------------------------------------------------------------------------------- /test/fixtures/plantuml/include/base.iuml: -------------------------------------------------------------------------------- 1 | skinparam DefaultFontName "Neucha" 2 | skinparam BackgroundColor black -------------------------------------------------------------------------------- /test/fixtures/plantuml/include/grand-parent.iuml: -------------------------------------------------------------------------------- 1 | !include parent/parent.iuml -------------------------------------------------------------------------------- /test/fixtures/plantuml/include/itself.iuml: -------------------------------------------------------------------------------- 1 | !include itself.iuml -------------------------------------------------------------------------------- /test/fixtures/plantuml/include/parent/child/child.iuml: -------------------------------------------------------------------------------- 1 | !include ../../grand-parent.iuml -------------------------------------------------------------------------------- /test/fixtures/plantuml/include/parent/child/handwritten.iuml: -------------------------------------------------------------------------------- 1 | skinparam Handwritten true 2 | !include ../../base.iuml -------------------------------------------------------------------------------- /test/fixtures/plantuml/include/parent/parent.iuml: -------------------------------------------------------------------------------- 1 | !include child/child.iuml -------------------------------------------------------------------------------- /test/fixtures/plantuml/include/parent/shadow.iuml: -------------------------------------------------------------------------------- 1 | skinparam Shadowing false 2 | !include ../base.iuml -------------------------------------------------------------------------------- /test/fixtures/plantuml/styles/general with spaces.iuml: -------------------------------------------------------------------------------- 1 | skinparam DefaultFontName "Neucha" 2 | skinparam BackgroundColor transparent 3 | skinparam Shadowing false 4 | skinparam Handwritten true -------------------------------------------------------------------------------- /test/fixtures/plantuml/styles/general.iuml: -------------------------------------------------------------------------------- 1 | skinparam DefaultFontName "Neucha" 2 | skinparam BackgroundColor transparent 3 | skinparam Shadowing false 4 | skinparam Handwritten true -------------------------------------------------------------------------------- /test/fixtures/plantuml/styles/general.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam DefaultFontName "Neucha" 3 | skinparam BackgroundColor transparent 4 | skinparam Shadowing false 5 | skinparam Handwritten true 6 | @enduml 7 | 8 | @startuml 9 | skinparam Sequence { 10 | TitleFontSize 12 11 | TitleFontColor #606060 12 | ArrowColor #303030 13 | DividerBackgroundColor #EEEEEE 14 | GroupBackgroundColor #EEEEEE 15 | LifeLineBackgroundColor white 16 | LifeLineBorderColor #303030 17 | ParticipantBackgroundColor #FEFEFE 18 | ParticipantBorderColor #303030 19 | BoxLineColor #303030 20 | BoxBackgroundColor #DDDDDD 21 | } 22 | @enduml -------------------------------------------------------------------------------- /test/fixtures/plantuml/styles/note.iuml: -------------------------------------------------------------------------------- 1 | skinparam Note { 2 | BorderColor #303030 3 | BackgroundColor #CEEEFE 4 | FontSize 12 5 | } -------------------------------------------------------------------------------- /test/fixtures/plantuml/styles/sequence.iuml: -------------------------------------------------------------------------------- 1 | skinparam Sequence { 2 | TitleFontSize 12 3 | TitleFontColor #606060 4 | ArrowColor #303030 5 | DividerBackgroundColor #EEEEEE 6 | GroupBackgroundColor #EEEEEE 7 | LifeLineBackgroundColor white 8 | LifeLineBorderColor #303030 9 | ParticipantBackgroundColor #FEFEFE 10 | ParticipantBorderColor #303030 11 | BoxLineColor #303030 12 | BoxBackgroundColor #DDDDDD 13 | } -------------------------------------------------------------------------------- /test/fixtures/plantuml/styles/style with spaces.iuml: -------------------------------------------------------------------------------- 1 | !include general\ with\ spaces.iuml 2 | !include note.iuml 3 | !include sequence.iuml -------------------------------------------------------------------------------- /test/fixtures/plantuml/styles/style-include-once-general.iuml: -------------------------------------------------------------------------------- 1 | !include_once general.iuml 2 | !include note.iuml 3 | !include sequence.iuml -------------------------------------------------------------------------------- /test/fixtures/plantuml/styles/style.iuml: -------------------------------------------------------------------------------- 1 | !include general.iuml 2 | !include note.iuml 3 | !include sequence.iuml -------------------------------------------------------------------------------- /test/fixtures/simple.bytefield: -------------------------------------------------------------------------------- 1 | (draw-column-headers) 2 | (draw-box "Address" {:span 4}) 3 | (draw-box "Size" {:span 2}) 4 | (draw-box 0 {:span 2}) 5 | (draw-gap "Payload") 6 | (draw-bottom) -------------------------------------------------------------------------------- /test/fixtures/vegalite-data.csv: -------------------------------------------------------------------------------- 1 | a,b,c 2 | 2020-01-05,0.3,C1 3 | 2020-01-15,0.7,C1 4 | 2020-01-05,0.5,C2 5 | 2020-01-15,0.8,C2 -------------------------------------------------------------------------------- /test/kroki-client.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | // @ts-check 3 | const chai = require('chai') 4 | const expect = chai.expect 5 | const dirtyChai = require('dirty-chai') 6 | chai.use(dirtyChai) 7 | 8 | const { readFixture } = require('./utils.js') 9 | const { KrokiClient, KrokiDiagram } = require('../src/kroki-client.js') 10 | const httpClient = require('../src/http/node-http.js') 11 | const asciidoctor = require('@asciidoctor/core')() 12 | 13 | describe('Kroki HTTP client', function () { 14 | this.timeout(30000) 15 | describe('kroki-http-method attribute', () => { 16 | it('should use post method when kroki-http-method value is post', () => { 17 | const doc = asciidoctor.load('', { attributes: { 'kroki-http-method': 'post' } }) 18 | const krokiClient = new KrokiClient(doc, httpClient) 19 | expect(krokiClient.method).to.equal('post') 20 | }) 21 | it('should use get method when kroki-http-method value is get', () => { 22 | const doc = asciidoctor.load('', { attributes: { 'kroki-http-method': 'get' } }) 23 | const krokiClient = new KrokiClient(doc, httpClient) 24 | expect(krokiClient.method).to.equal('get') 25 | }) 26 | it('should use adaptive method when kroki-http-method value is invalid', () => { 27 | const doc = asciidoctor.load('', { attributes: { 'kroki-http-method': 'delete' } }) 28 | const krokiClient = new KrokiClient(doc, httpClient) 29 | expect(krokiClient.method).to.equal('adaptive') 30 | }) 31 | it('should use adaptive method when kroki-http-method is adaptive', () => { 32 | const doc = asciidoctor.load('', { attributes: { 'kroki-http-method': 'adaptive' } }) 33 | const krokiClient = new KrokiClient(doc, httpClient) 34 | expect(krokiClient.method).to.equal('adaptive') 35 | }) 36 | it('should use adaptive method when kroki-http-method is undefined', () => { 37 | const doc = asciidoctor.load('') 38 | const krokiClient = new KrokiClient(doc, httpClient) 39 | expect(krokiClient.method).to.equal('adaptive') 40 | }) 41 | it('should use adaptive method when kroki-http-method is undefined', () => { 42 | const doc = asciidoctor.load('') 43 | const krokiClient = new KrokiClient(doc, httpClient) 44 | expect(krokiClient.method).to.equal('adaptive') 45 | }) 46 | }) 47 | describe('kroki-max-uri-length attribute', () => { 48 | it('should use the default value (4000) when kroki-max-uri-length is undefined', () => { 49 | const doc = asciidoctor.load('') 50 | const krokiClient = new KrokiClient(doc, httpClient) 51 | expect(krokiClient.maxUriLength).to.equal(4000) 52 | }) 53 | it('should use the default value (4000) when kroki-max-uri-length is invalid', () => { 54 | const doc = asciidoctor.load('') 55 | doc.setAttribute('kroki-max-uri-length', 'foo') 56 | const krokiClient = new KrokiClient(doc, httpClient) 57 | expect(krokiClient.maxUriLength).to.equal(4000) 58 | }) 59 | it('should use a custom value when kroki-max-uri-length is a number', () => { 60 | const doc = asciidoctor.load('') 61 | doc.setAttribute('kroki-max-uri-length', '8000') 62 | const krokiClient = new KrokiClient(doc, httpClient) 63 | expect(krokiClient.maxUriLength).to.equal(8000) 64 | }) 65 | }) 66 | describe('Adaptive mode', () => { 67 | it('should get an image with GET request if the URI length is <= 4000', () => { 68 | const doc = asciidoctor.load('') 69 | const krokiClient = new KrokiClient(doc, httpClient) 70 | const krokiDiagram = new KrokiDiagram('vegalite', 'svg', readFixture('chart.vlite'), {}) 71 | const image = krokiClient.getImage(krokiDiagram) 72 | .replace(/\r/, '') 73 | .replace(/\n/, '') 74 | const expected = readFixture('expected', 'chart.svg') 75 | .replace(/\r/, '') 76 | .replace(/\n/, '') 77 | expect(image).to.equal(expected) 78 | }) 79 | it('should get an image with POST request if the URI length is > 4000', () => { 80 | const doc = asciidoctor.load('') 81 | const krokiClient = new KrokiClient(doc, httpClient) 82 | const krokiDiagram = new KrokiDiagram('vegalite', 'svg', readFixture('cars-repeated-charts.vlite'), {}) 83 | const image = krokiClient.getImage(krokiDiagram) 84 | .replace(/\r/, '') 85 | .replace(/\n/, '') 86 | const expected = readFixture('expected', 'cars-repeated-charts.svg') 87 | .replace(/\r/, '') 88 | .replace(/\n/, '') 89 | expect(image).to.equal(expected) 90 | }) 91 | it('should get an image with POST request if the URI length is greater than the value configured', () => { 92 | const doc = asciidoctor.load('') 93 | doc.setAttribute('kroki-max-uri-length', '10') 94 | const krokiClient = new KrokiClient(doc, { 95 | get: (uri) => `GET ${uri}`, 96 | post: (uri, body) => `POST ${uri} - ${body}` 97 | }) 98 | const krokiDiagram = { 99 | type: 'type', 100 | format: 'format', 101 | text: 'text', 102 | opts: {}, 103 | getDiagramUri: () => 'diagram-uri' // length: 11 104 | } 105 | const image = krokiClient.getImage(krokiDiagram) 106 | expect(image).to.equal('POST https://kroki.io/type/format - text') 107 | }) 108 | it('should get an image with GET request if the URI length is lower or equals than the value configured', () => { 109 | const doc = asciidoctor.load('') 110 | doc.setAttribute('kroki-max-uri-length', '11') 111 | const krokiClient = new KrokiClient(doc, { 112 | get: (uri) => `GET ${uri}`, 113 | post: (uri, body) => `POST ${uri} - ${body}` 114 | }) 115 | const krokiDiagram = { 116 | type: 'type', 117 | format: 'format', 118 | text: 'text', 119 | opts: {}, 120 | getDiagramUri: () => 'diagram-uri' // length: 11 121 | } 122 | const image = krokiClient.getImage(krokiDiagram) 123 | expect(image).to.equal('GET diagram-uri') 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /test/node-http.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | const httpClient = require('../src/http/node-http.js') 3 | const { Worker } = require('node:worker_threads') 4 | const ospath = require('node:path') 5 | const chai = require('chai') 6 | const expect = chai.expect 7 | 8 | /** 9 | * @returns {Promise<{}>} 10 | */ 11 | async function startServer (name) { 12 | return new Promise((resolve, reject) => { 13 | const worker = new Worker(ospath.join(__dirname, name)) 14 | worker.on('message', (msg) => { 15 | resolve({ 16 | worker, 17 | port: msg.port 18 | }) 19 | }) 20 | worker.on('error', reject) 21 | }) 22 | } 23 | 24 | describe('Synchronous HTTP client (unxhr)', function () { 25 | it('should return throw error when the server returns a 500', async () => { 26 | const { worker, port } = await startServer('500-server.js') 27 | try { 28 | httpClient.get(`http://localhost:${port}`, {}, 'utf8') 29 | expect.fail('it should throw an error when the server returns a 500') 30 | } catch (err) { 31 | // it should include the response from the server in the error message 32 | expect(err.message).to.contains('500 Something went bad!') 33 | } finally { 34 | await worker.terminate() 35 | } 36 | }) 37 | it('should return throw an error when the server returns an empty response', async () => { 38 | const { worker, port } = await startServer('204-server.js') 39 | try { 40 | httpClient.get(`http://localhost:${port}`, {}, 'utf8') 41 | expect.fail('it should throw an error when the server returns an empty response') 42 | } catch (err) { 43 | expect(err.message).to.contains('server returns an empty response') 44 | } finally { 45 | await worker.terminate() 46 | } 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const ospath = require('path').posix 2 | const fs = require('fs') 3 | 4 | module.exports = { 5 | // Until recursive: true is a stable part of Node 6 | // See: https://stackoverflow.com/questions/18052762/remove-directory-which-is-not-empty 7 | deleteDirWithFiles: function (path) { 8 | if (fs.existsSync(path)) { 9 | fs.readdirSync(path).forEach((file) => { 10 | const curPath = ospath.join(path, file) 11 | fs.unlinkSync(curPath) 12 | }) 13 | fs.rmdirSync(path) 14 | } 15 | }, 16 | fixturePath: (...paths) => ospath.join(__dirname, 'fixtures', ...paths), 17 | readFixture: (...paths) => fs.readFileSync(ospath.join(__dirname, 'fixtures', ...paths), 'utf-8') 18 | } 19 | --------------------------------------------------------------------------------