jekyll-kroki 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39c2c3f9463c5788864d00c4ed80e9aa826a1aa1d1380dbff2be92407958f0e5
4
- data.tar.gz: 38340a5a1deaa705a0b7cde596e5eb007da3d84526b2641e9cad6e9f38e86809
3
+ metadata.gz: 4f2efa9a6bac231d6efdac15ccf493e387cdb1a9890f1b774ddf839b8fb0b599
4
+ data.tar.gz: f51d26aa98904cd284f8bea24113f6225fdc5511651c0b7a1d432898f47397fa
5
5
  SHA512:
6
- metadata.gz: dbedea8bbf380c047c91cee38bc7fade0c9a6a0e3aa8735b7a18e3361219cb796fb3b89b17583db402f0b03bf083f13892d53a7677cc904d12f6be6990bfee98
7
- data.tar.gz: 6c3cf11ca513c6b16331547bcd69003b77dd62c3a09b46c52e440684a5001f8c47318962037faa3a0bce5cf1e99c2dfe4a2f5f4e00f4bc0eb893ef0fb174efa6
6
+ metadata.gz: 1b4685d93410a2d8ea9ce9a3c97049f28ea2d4fdfcea144357ae388e0c59af9d93fdadb8c872ea80d7b48510358bd9cfedd980ae72cdfef7f586ce6ac49d915b
7
+ data.tar.gz: 3158b7e07f81b144d249161984b0874c48a383187a3c82c564a818864cc3401b9a5938d2b5e3632a1ed3993acb3f356d42861f6b8ed1d014ce109405537b2ef7
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Ruby",
3
- "image": "mcr.microsoft.com/devcontainers/ruby:3.4-bullseye",
3
+ "image": "mcr.microsoft.com/devcontainers/ruby:4.0-bullseye",
4
4
  "customizations": {
5
5
  "vscode": {
6
6
  "extensions": [
data/.rubocop.yml CHANGED
@@ -5,7 +5,7 @@ plugins:
5
5
 
6
6
  AllCops:
7
7
  NewCops: enable
8
- TargetRubyVersion: 2.7
8
+ TargetRubyVersion: 3.0
9
9
 
10
10
  Gemspec/DevelopmentDependencies:
11
11
  EnforcedStyle: gemspec
data/README.md CHANGED
@@ -2,9 +2,11 @@
2
2
  [![Gem Version](https://badge.fury.io/rb/jekyll-kroki.svg)](https://badge.fury.io/rb/jekyll-kroki)
3
3
 
4
4
  # jekyll-kroki
5
+
5
6
  A [Jekyll](https://jekyllrb.com/) plugin to convert diagram descriptions into images using [Kroki](https://kroki.io/).
6
7
 
7
8
  ## Installation
9
+
8
10
  Add the `jekyll-kroki` Gem to the `:jekyll_plugins` group of your site's Gemfile:
9
11
 
10
12
  ```ruby
@@ -14,6 +16,7 @@ end
14
16
  ```
15
17
 
16
18
  ## Usage
19
+
17
20
  Kroki supports over 25 popular diagram scripting languages, including Blockdiag, D2, GraphViz, Mermaid, and PlantUML. The [examples](https://kroki.io/examples.html) page and complete list of [supported diagram languages](https://kroki.io/#support) provide a taste of what's possible.
18
21
 
19
22
  In Markdown, simply write your diagram descriptions inside a fenced code block with the language specified:
@@ -28,27 +31,32 @@ Kroki --> Jekyll: Rendered diagram in SVG format
28
31
  ```
29
32
  ````
30
33
 
31
- When Jekyll builds your site, the `jekyll-kroki` plugin will encode the diagrams, send them to the Kroki server for rendering, then replace the diagram descriptions in the generated HTML with the rendered images in SVG format:
34
+ When Jekyll builds your site, the `jekyll-kroki` plugin encodes the diagram descriptions, renders them as SVG images using the Kroki server, then embeds them in the generated HTML:
32
35
 
33
36
  ![sample-diagram](https://github.com/felixvanoost/jekyll-kroki/assets/10233016/244d2ec4-b09b-4a5f-8164-3851574c3dd2)
34
37
 
35
- The site remains fully static as the images are directly embedded in the HTML files served by Jekyll. Jekyll only depends on the Kroki server - which can also be run locally - during the build stage, and all of the client-side processing that is normally used to render diagrams into images is eliminated.
38
+ Since the images are rendered and embedded at build-time, the Jekyll site remains completely static and doesn't depend on access to the Kroki server later on. This also eliminates all of the client-side processing that is typically used to render diagrams into images.
36
39
 
37
40
  ### Advantages
38
41
 
39
42
  #### Consistent syntax
43
+
40
44
  Instead of using Liquid tags, `jekyll-kroki` leverages the same Markdown fenced code block syntax used by both [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) and [GitHub](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams) to display diagrams. Besides being more consistent, this means that diagram descriptions in Markdown files can be displayed consistently as images across the GitLab/GitHub UI and on GitLab/GitHub Pages sites generated using Jekyll. GitLab currently supports Mermaid and PlantUML, while GitHub only supports Mermaid.
41
45
 
42
46
  #### Seamless GitLab integration
47
+
43
48
  Self-managed GitLab instances can additionally enable the [Kroki integration](https://docs.gitlab.com/ee/administration/integration/kroki.html), which adds support for all the same diagram scripting languages used by `jekyll-kroki`. Furthermore, by pointing both GitLab and `jekyll-kroki` to the same Kroki instance, you can guarantee that diagrams are generated using identical versions of the diagram libraries.
44
49
 
45
50
  #### Speed
46
- The server-side nature of Kroki means that you don't have to deal with installing or updating any diagram library dependencies on your machine. Jekyll sites that are generated in CI/CD pipelines will thus build faster.
51
+
52
+ The server-side nature of Kroki means that you don't have to deal with installing or updating any diagram library dependencies on your machine. Jekyll sites that are generated in CI/CD pipelines can bypass these steps and will thus build faster.
47
53
 
48
54
  #### Flexibility
49
- Kroki is available either as a free service or self-hosted using Docker. Organisations that frequently build large Jekyll sites with many diagrams or want maximum control over their data have the option of running their own Kroki instance to provide consistency and use compute resources efficiently.
55
+
56
+ Kroki is available either as a free service or self-hosted using Docker. Organisations that frequently build large Jekyll sites with many diagrams or want maximum control over their data can choose to operate their own Kroki instance to ensure consistency and use compute resources efficiently. For individuals, you can also opt to run Kroki locally.
50
57
 
51
58
  ### Configuration
59
+
52
60
  You can specify the following parameters in the Jekyll `_config.yml` file:
53
61
 
54
62
  | Parameter | Default value | Description |
@@ -69,6 +77,7 @@ kroki:
69
77
  ```
70
78
 
71
79
  ### Security
80
+
72
81
  Embedding diagrams as SVGs directly within HTML files can be dangerous. You should only use a Kroki instance that you trust (or run your own!). For additional security, you can configure a [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) using custom Webrick headers in the Jekyll `_config.yml` file:
73
82
 
74
83
  ```yaml
@@ -78,4 +87,5 @@ webrick:
78
87
  ```
79
88
 
80
89
  ## Contributing
90
+
81
91
  Bug reports and pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ class Kroki
5
+ # Reads, validates, and exposes the jekyll-kroki configuration from the Jekyll site config.
6
+ class Config
7
+ DEFAULT_KROKI_URL = "https://kroki.io"
8
+ DEFAULT_HTTP_RETRIES = 3
9
+ DEFAULT_HTTP_TIMEOUT = 15
10
+ DEFAULT_MAX_CONCURRENT_DOCS = 8
11
+
12
+ attr_reader :kroki_url, :http_retries, :http_timeout, :max_concurrent_docs
13
+
14
+ # @param [Hash] The Jekyll site configuration.
15
+ # @raise [TypeError] If any parameter has an incorrect type.
16
+ # @raise [ArgumentError] If any parameter is out of the valid range.
17
+ def initialize(site_config)
18
+ kroki_config = site_config.fetch("kroki", {})
19
+
20
+ @kroki_url = parse_url(kroki_config)
21
+ @http_retries = parse_integer(kroki_config, "http_retries", DEFAULT_HTTP_RETRIES, min: 0)
22
+ @http_timeout = parse_integer(kroki_config, "http_timeout", DEFAULT_HTTP_TIMEOUT, min: 0)
23
+ @max_concurrent_docs = parse_integer(kroki_config, "max_concurrent_docs", DEFAULT_MAX_CONCURRENT_DOCS, min: 1)
24
+
25
+ freeze
26
+ end
27
+
28
+ private
29
+
30
+ def parse_url(kroki_config)
31
+ param_name = "url"
32
+ raw = kroki_config.fetch(param_name, DEFAULT_KROKI_URL)
33
+ uri = URI.parse(raw)
34
+ raise TypeError, "'#{param_name}' is not a valid HTTP URL" unless uri.is_a?(URI::HTTP)
35
+
36
+ uri
37
+ end
38
+
39
+ def parse_integer(kroki_config, param_name, default, min:)
40
+ value = kroki_config.fetch(param_name, default)
41
+ raise TypeError, "'#{param_name}' must be an integer" unless value.is_a?(Integer)
42
+ raise ArgumentError, "'#{param_name}' must be >= #{min}" if value < min
43
+
44
+ value
45
+ end
46
+ end
47
+ end
48
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  class Kroki
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/lib/jekyll/kroki.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "kroki/config"
3
4
  require_relative "kroki/version"
4
5
 
5
6
  require "async"
6
7
  require "async/semaphore"
7
8
  require "base64"
9
+ require "concurrent-ruby"
10
+ require "digest"
8
11
  require "faraday"
9
12
  require "faraday/retry"
10
13
  require "httpx/adapters/faraday"
@@ -15,10 +18,6 @@ require "zlib"
15
18
  module Jekyll
16
19
  # Converts diagram descriptions into images using Kroki.
17
20
  class Kroki
18
- DEFAULT_KROKI_URL = "https://kroki.io"
19
- DEFAULT_HTTP_RETRIES = 3
20
- DEFAULT_HTTP_TIMEOUT = 15
21
- DEFAULT_MAX_CONCURRENT_DOCS = 8
22
21
  EXPECTED_HTML_TAGS = %w[code div].freeze
23
22
  HTTP_RETRY_INTERVAL_BACKOFF_FACTOR = 2
24
23
  HTTP_RETRY_INTERVAL_RANDOMNESS = 0.5
@@ -27,27 +26,26 @@ module Jekyll
27
26
  graphviz mermaid nomnoml nwdiag packetdiag pikchr plantuml rackdiag seqdiag structurizr
28
27
  svgbob symbolator tikz umlet vega vegalite wavedrom wireviz].freeze
29
28
 
29
+ @diagram_cache = Concurrent::Map.new
30
+
30
31
  class << self
31
32
  # Renders and embeds all diagram descriptions in the given Jekyll site using Kroki.
32
33
  #
33
34
  # @param [Jekyll::Site] The Jekyll site to embed diagrams in.
34
35
  def embed_site(site)
35
- kroki_url = get_kroki_url(site.config)
36
- http_retries = get_http_retries(site.config)
37
- http_timeout = get_http_timeout(site.config)
38
- connection = setup_connection(kroki_url, http_retries, http_timeout)
36
+ config = Config.new(site.config)
37
+ connection = setup_connection(config.kroki_url, config.http_retries, config.http_timeout)
39
38
 
40
- max_concurrent_docs = get_max_concurrent_docs(site.config)
41
- rendered_diag = embed_docs_in_site(site, connection, max_concurrent_docs)
39
+ rendered_diag = embed_docs_in_site(site, connection, config.max_concurrent_docs)
42
40
  unless rendered_diag.zero?
43
- puts "[jekyll-kroki] Rendered #{rendered_diag} diagrams using Kroki instance at '#{kroki_url}'"
41
+ puts "[jekyll-kroki] Rendered #{rendered_diag} diagrams using Kroki instance at '#{config.kroki_url}'"
44
42
  end
45
43
  rescue StandardError => e
46
- exit(e)
44
+ fatal_error(e)
47
45
  end
48
46
 
49
47
  # Renders the diagram descriptions in all Jekyll pages and documents in the given Jekyll site. Pages / documents
50
- # are rendered concurrently up to the limit defined by DEFAULT_MAX_CONCURRENT_DOCS.
48
+ # are rendered concurrently up to the limit defined by max_concurrent_docs.
51
49
  #
52
50
  # @param [Jekyll::Site] The Jekyll site to embed diagrams in.
53
51
  # @param [Faraday::Connection] The Faraday connection to use.
@@ -70,8 +68,8 @@ module Jekyll
70
68
  rendered_diag
71
69
  end
72
70
 
73
- # Renders the supported diagram descriptions in a single document asynchronously, respecting the concurrency limit
74
- # imposed by the provided semaphore.
71
+ # Renders the supported diagram descriptions in a single document. Multiple documents can be rendered concurrently
72
+ # up to the limit imposed by the given semaphore.
75
73
  #
76
74
  # @param [Async::Task] The parent async task to spawn a child task from.
77
75
  # @param [Async::Semaphore] A semaphore to limit concurrency.
@@ -87,8 +85,8 @@ module Jekyll
87
85
  end
88
86
  end
89
87
 
90
- # Renders the supported diagram descriptions in a single document and embeds them as inline SVGs in the HTML
91
- # source.
88
+ # Renders the supported diagram descriptions in a single document sequentially and embeds them as inline SVGs in
89
+ # the HTML source.
92
90
  #
93
91
  # @param [Faraday::Connection] The Faraday connection to use.
94
92
  # @param [Jekyll::Page, Jekyll::Document] The document to process.
@@ -113,26 +111,37 @@ module Jekyll
113
111
  rendered_diag
114
112
  end
115
113
 
116
- # Renders a single diagram description using Kroki.
114
+ # Renders a single diagram description using Kroki. The rendered diagram is cached to avoid redundant HTTP
115
+ # requests across documents, using the diagram language and the SHA1 of the diagram description as the key.
117
116
  #
118
117
  # @param [Faraday::Connection] The Faraday connection to use.
119
118
  # @param [String] The diagram description.
120
119
  # @param [String] The language of the diagram description.
121
120
  # @return [String] The rendered diagram in SVG format.
122
121
  def render_diagram(connection, diagram_desc, language)
123
- begin
124
- response = connection.get("#{language}/svg/#{encode_diagram(diagram_desc.text)}")
125
- rescue Faraday::Error => e
126
- raise e.message
122
+ diagram_text = diagram_desc.text
123
+ cache_key = "#{language}:#{Digest::SHA1.hexdigest(diagram_text)}"
124
+ @diagram_cache.compute_if_absent(cache_key) do
125
+ begin
126
+ response = connection.get("#{language}/svg/#{encode_diagram(diagram_text)}")
127
+ rescue Faraday::Error => e
128
+ raise e.message
129
+ end
130
+ validate_content_type(response)
131
+ sanitise_diagram(response.body)
127
132
  end
133
+ end
134
+
135
+ # Validates that the Kroki response has the expected SVG content type.
136
+ #
137
+ # @param [Faraday::Response] The response to validate.
138
+ def validate_content_type(response)
128
139
  expected_content_type = "image/svg+xml"
129
140
  returned_content_type = response.headers[:content_type]
130
- if returned_content_type != expected_content_type
131
- raise "Kroki returned an incorrect content type: " \
132
- "expected '#{expected_content_type}', received '#{returned_content_type}'"
141
+ return if returned_content_type == expected_content_type
133
142
 
134
- end
135
- sanitise_diagram(response.body)
143
+ raise "Kroki returned an incorrect content type: " \
144
+ "expected '#{expected_content_type}', received '#{returned_content_type}'"
136
145
  end
137
146
 
138
147
  # Sanitises a rendered diagram. Only <script> elements are removed, which is the most minimal / naive
@@ -175,45 +184,10 @@ module Jekyll
175
184
  end
176
185
  end
177
186
 
178
- # Gets the URL of the Kroki instance to use for rendering diagrams.
179
- #
180
- # @param The Jekyll site configuration.
181
- # @return [URI::HTTP] The URL of the Kroki instance.
182
- def get_kroki_url(config)
183
- url = config.fetch("kroki", {}).fetch("url", DEFAULT_KROKI_URL)
184
- raise TypeError, "'url' is not a valid HTTP URL" unless URI.parse(url).is_a?(URI::HTTP)
185
-
186
- URI(url)
187
- end
188
-
189
- # Gets the number of HTTP retries.
190
- #
191
- # @param The Jekyll site configuration.
192
- # @return [Integer] The number of HTTP retries.
193
- def get_http_retries(config)
194
- config.fetch("kroki", {}).fetch("http_retries", DEFAULT_HTTP_RETRIES)
195
- end
196
-
197
- # Gets the HTTP timeout value.
198
- #
199
- # @param The Jekyll site configuration.
200
- # @return [Integer] The HTTP timeout value in seconds.
201
- def get_http_timeout(config)
202
- config.fetch("kroki", {}).fetch("http_timeout", DEFAULT_HTTP_TIMEOUT)
203
- end
204
-
205
- # Gets the maximum number of documents to render concurrently.
206
- #
207
- # @param The Jekyll site configuration.
208
- # @return [Integer] The maximum number of documents to render concurrently.
209
- def get_max_concurrent_docs(config)
210
- config.fetch("kroki", {}).fetch("max_concurrent_docs", DEFAULT_MAX_CONCURRENT_DOCS)
211
- end
212
-
213
187
  # Determines whether a document may contain embeddable diagram descriptions; it is in HTML format and is either
214
188
  # a Jekyll::Page or writeable Jekyll::Document.
215
189
  #
216
- # @param [Jekyll::Page or Jekyll::Document] The document to check for embeddability.
190
+ # @param [Jekyll::Page or Jekyll::Document] The document to check for embeddable diagrams.
217
191
  def embeddable?(doc)
218
192
  doc.output_ext == ".html" && (doc.is_a?(Jekyll::Page) || doc.write?)
219
193
  end
@@ -226,7 +200,7 @@ module Jekyll
226
200
  # calling method. To specify the calling method's caller, pass in 2.
227
201
  #
228
202
  # Source: https://www.mslinn.com/ruby/2200-crash-exit.html
229
- def exit(error, caller_index = 1)
203
+ def fatal_error(error, caller_index = 1)
230
204
  raise error
231
205
  rescue StandardError => e
232
206
  file, line_number, caller = e.backtrace[caller_index].split(":")
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-kroki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felix van Oost
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.25'
26
+ - !ruby/object:Gem::Dependency
27
+ name: concurrent-ruby
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: faraday
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -190,14 +204,13 @@ files:
190
204
  - LICENSE
191
205
  - README.md
192
206
  - Rakefile
193
- - jekyll-kroki.gemspec
194
207
  - lib/jekyll/kroki.rb
208
+ - lib/jekyll/kroki/config.rb
195
209
  - lib/jekyll/kroki/version.rb
196
210
  homepage: https://github.com/felixvanoost/jekyll-kroki
197
211
  licenses:
198
212
  - MIT
199
213
  metadata:
200
- homepage_uri: https://github.com/felixvanoost/jekyll-kroki
201
214
  source_code_uri: https://github.com/felixvanoost/jekyll-kroki
202
215
  rubygems_mfa_required: 'true'
203
216
  rdoc_options: []
@@ -207,14 +220,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
207
220
  requirements:
208
221
  - - ">="
209
222
  - !ruby/object:Gem::Version
210
- version: 2.7.0
223
+ version: 3.0.0
211
224
  required_rubygems_version: !ruby/object:Gem::Requirement
212
225
  requirements:
213
226
  - - ">="
214
227
  - !ruby/object:Gem::Version
215
228
  version: '0'
216
229
  requirements: []
217
- rubygems_version: 3.6.7
230
+ rubygems_version: 3.6.9
218
231
  specification_version: 4
219
232
  summary: A Jekyll plugin to convert diagram descriptions into images using Kroki
220
233
  test_files: []
data/jekyll-kroki.gemspec DELETED
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/jekyll/kroki/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "jekyll-kroki"
7
- spec.version = Jekyll::Kroki::VERSION
8
- spec.authors = ["Felix van Oost"]
9
-
10
- spec.summary = "A Jekyll plugin to convert diagram descriptions into images using Kroki"
11
- spec.description = "A Jekyll plugin to convert diagram descriptions written in over 25 popular diagram scripting
12
- languages into images using Kroki"
13
- spec.homepage = "https://github.com/felixvanoost/jekyll-kroki"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.7.0"
16
-
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = spec.homepage
19
- spec.metadata["rubygems_mfa_required"] = "true"
20
-
21
- # Load the files that are versioned in Git into the RubyGem.
22
- spec.files = Dir.chdir(__dir__) do
23
- `git ls-files -z`.split("\x0").reject do |f|
24
- (File.expand_path(f) == __FILE__) ||
25
- f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
26
- end
27
- end
28
- spec.bindir = "exe"
29
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
- spec.require_paths = ["lib"]
31
-
32
- spec.add_dependency "async", ["~> 2.25"]
33
- spec.add_dependency "faraday", ["~> 2.7"]
34
- spec.add_dependency "faraday-retry", ["~> 2.2"]
35
- spec.add_dependency "httpx", ["~> 1.1"]
36
- spec.add_dependency "jekyll", ["~> 4"]
37
- spec.add_dependency "nokogiri", ["~> 1.15"]
38
-
39
- spec.add_development_dependency "minitest", ["~> 5.0"]
40
- spec.add_development_dependency "rake", ["~> 13.0"]
41
- spec.add_development_dependency "rubocop", ["~> 1.21"]
42
- spec.add_development_dependency "rubocop-minitest", ["~> 0.38"]
43
- spec.add_development_dependency "rubocop-performance", ["~> 1.25"]
44
- spec.add_development_dependency "rubocop-rake", ["~> 0.7"]
45
- end