From f701cac955527b1fef9e31ee749d62f432f18f60 Mon Sep 17 00:00:00 2001 From: Felix van Oost Date: Sat, 21 Oct 2023 18:43:05 -0400 Subject: [PATCH 1/4] Create HTTP GET wrapper function --- Gemfile.lock | 2 ++ jekyll-kroki.gemspec | 1 + lib/jekyll/kroki.rb | 38 ++++++++++++++++++++------------------ 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index def2e59..e7f7fa4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,7 @@ PATH jekyll-kroki (0.1.0) jekyll (~> 4) nokogiri (~> 1.15) + retriable (~> 3.1) GEM remote: https://rubygems.org/ @@ -72,6 +73,7 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) regexp_parser (2.8.2) + retriable (3.1.2) rexml (3.2.6) rouge (4.1.3) rubocop (1.57.1) diff --git a/jekyll-kroki.gemspec b/jekyll-kroki.gemspec index 1a146aa..dac1743 100644 --- a/jekyll-kroki.gemspec +++ b/jekyll-kroki.gemspec @@ -30,6 +30,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "jekyll", ["~> 4"] spec.add_runtime_dependency "nokogiri", ["~> 1.15"] + spec.add_runtime_dependency "retriable", ["~> 3.1"] spec.add_development_dependency "minitest", ["~> 5.0"] spec.add_development_dependency "rake", ["~> 13.0"] diff --git a/lib/jekyll/kroki.rb b/lib/jekyll/kroki.rb index 36be508..fbcd841 100644 --- a/lib/jekyll/kroki.rb +++ b/lib/jekyll/kroki.rb @@ -7,6 +7,7 @@ require "json" require "net/http" require "nokogiri" +require "retriable" require "zlib" module Jekyll @@ -43,21 +44,15 @@ def render(doc) # Renders a diagram description using Kroki. # - # @param [String] The URL of the Kroki instance + # @param [URI::HTTP] The URL of the Kroki instance # @param [String] The diagram description # @param [String] The language of the diagram description # @return [String] The rendered diagram in SVG format def render_diagram(kroki_url, diagram_desc, language) - # Encode the diagram and construct the URI - uri = URI("#{kroki_url}/#{language}/svg/#{encode_diagram(diagram_desc.text)}") + response = http_get(URI("#{kroki_url}/#{language}/svg/#{encode_diagram(diagram_desc.text)}")) + raise StandardError unless response.is_a?(Net::HTTPSuccess) - begin - response = Net::HTTP.get_response(uri) - rescue StandardError => e - raise e.message - else - response.body if response.is_a?(Net::HTTPSuccess) - end + response.body end # Encodes the diagram into Kroki format using deflate + base64. @@ -75,18 +70,25 @@ def encode_diagram(diagram) # configured Kroki instance. For example, Mermaid will still show up as a supported language even if the Mermaid # companion container is not running. # - # @param [String] The URL of the Kroki instance + # @param [URI::HTTP] The URL of the Kroki instance # @return [Array] The supported diagram languages def get_supported_languages(kroki_url) - uri = URI("#{kroki_url}/health") + response = http_get(URI("#{kroki_url}/health")) + raise StandardError unless response.is_a?(Net::HTTPSuccess) - begin - response = Net::HTTP.get_response(uri) - rescue StandardError => e - raise e.message - else - JSON.parse(response.body)["version"].keys if response.is_a?(Net::HTTPSuccess) + JSON.parse(response.body)["version"].keys + end + + # Sends an HTTP GET request and returns the response. + # + # @param [URI] The URI to GET from + # @return [Net::HTTPResponse] The HTTP GET response + def http_get(uri) + Retriable.retriable(tries: 3) do + Net::HTTP.get_response(uri) end + rescue StandardError => e + raise e.message end # Gets the URL of the Kroki instance to use for rendering diagrams. From 89d03cfd7daef69a203923f2732fb904a0c27bf7 Mon Sep 17 00:00:00 2001 From: Felix van Oost Date: Sat, 21 Oct 2023 23:21:41 -0400 Subject: [PATCH 2/4] Use Faraday HTTP client and enable retries --- Gemfile.lock | 12 +++++- jekyll-kroki.gemspec | 7 ++-- lib/jekyll/kroki.rb | 99 +++++++++++++++++++++++++------------------- 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e7f7fa4..9808832 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,9 +2,10 @@ PATH remote: . specs: jekyll-kroki (0.1.0) + faraday (~> 2.7) + faraday-retry (~> 2.2) jekyll (~> 4) nokogiri (~> 1.15) - retriable (~> 3.1) GEM remote: https://rubygems.org/ @@ -19,6 +20,13 @@ GEM eventmachine (>= 0.12.9) http_parser.rb (~> 0) eventmachine (1.2.7) + faraday (2.7.11) + base64 + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) + faraday-retry (2.2.0) + faraday (~> 2.0) ffi (1.16.3) forwardable-extended (2.6.0) google-protobuf (3.24.4-x86_64-linux) @@ -73,7 +81,6 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) regexp_parser (2.8.2) - retriable (3.1.2) rexml (3.2.6) rouge (4.1.3) rubocop (1.57.1) @@ -91,6 +98,7 @@ GEM rubocop-ast (1.29.0) parser (>= 3.2.1.0) ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) safe_yaml (1.0.5) sass-embedded (1.69.4) google-protobuf (~> 3.23) diff --git a/jekyll-kroki.gemspec b/jekyll-kroki.gemspec index dac1743..d32a156 100644 --- a/jekyll-kroki.gemspec +++ b/jekyll-kroki.gemspec @@ -8,8 +8,8 @@ Gem::Specification.new do |spec| spec.authors = ["Felix van Oost"] spec.summary = "A Jekyll plugin to convert diagram descriptions into images using Kroki" - spec.description = "Replaces diagram descriptions written in any Kroki-supported language in HTML files with their - visual representation in SVG format" + spec.description = "Replaces diagram descriptions written in any Kroki-supported language in HTML files generated by + Jekyll with their visual representation in SVG format." spec.homepage = "https://github.com/felixvanoost/jekyll-kroki" spec.license = "MIT" spec.required_ruby_version = ">= 2.6.0" @@ -28,9 +28,10 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] + spec.add_runtime_dependency "faraday", ["~> 2.7"] + spec.add_runtime_dependency "faraday-retry", ["~> 2.2"] spec.add_runtime_dependency "jekyll", ["~> 4"] spec.add_runtime_dependency "nokogiri", ["~> 1.15"] - spec.add_runtime_dependency "retriable", ["~> 3.1"] spec.add_development_dependency "minitest", ["~> 5.0"] spec.add_development_dependency "rake", ["~> 13.0"] diff --git a/lib/jekyll/kroki.rb b/lib/jekyll/kroki.rb index fbcd841..894ff18 100644 --- a/lib/jekyll/kroki.rb +++ b/lib/jekyll/kroki.rb @@ -3,65 +3,73 @@ require_relative "kroki/version" require "base64" +require "faraday" +require "faraday/retry" require "jekyll" -require "json" -require "net/http" require "nokogiri" -require "retriable" require "zlib" module Jekyll # Converts diagram descriptions into images using Kroki class Kroki KROKI_DEFAULT_URL = "https://kroki.io" + HTTP_MAX_RETRIES = 3 class << self # Renders all diagram descriptions written in a Kroki-supported language in an HTML document. # - # @param [Jekyll::Page or Jekyll::Document] The document to render diagrams in - def render(doc) + # @param [Jekyll::Page or Jekyll::Document] The document to embed diagrams in + def embed(doc) # Get the URL of the Kroki instance kroki_url = kroki_url(doc.site.config) - puts "[jekyll-kroki] Rendering diagrams in '#{doc.name}' using '#{kroki_url}'" + puts "[jekyll-kroki] Rendering diagrams in '#{doc.name}' using Kroki instance '#{kroki_url}'" - # Parse the HTML document - parsed_doc = Nokogiri::HTML.parse(doc.output) + # Set up a Faraday connection + connection = setup_connection(kroki_url) + # Parse the HTML document, render and embed the diagrams, then convert it back into HTML + parsed_doc = Nokogiri::HTML(doc.output) + embed_diagrams_in_doc(connection, parsed_doc) + doc.output = parsed_doc.to_html + end + + # Renders all diagram descriptions in any Kroki-supported language and embeds them in an HTML document. + # + # @param [Faraday::Connection] The Faraday connection to use + # @param [Nokogiri::HTML4::Document] The parsed HTML document + def embed_diagrams_in_doc(connection, parsed_doc) # Iterate through every diagram description in each of the supported languages - get_supported_languages(kroki_url).each do |language| + get_supported_languages(connection).each do |language| parsed_doc.css("code[class~='language-#{language}']").each do |diagram_desc| - # Get the rendered diagram using Kroki - rendered_diagram = render_diagram(kroki_url, diagram_desc, language) - - # Replace the diagram description with the SVG representation - diagram_desc.replace(rendered_diagram) + # Replace the diagram description with the SVG representation rendered by Kroki + diagram_desc.replace(render_diagram(connection, diagram_desc, language)) end end - - # Generate the modified HTML document - doc.output = parsed_doc.to_html end # Renders a diagram description using Kroki. # - # @param [URI::HTTP] The URL of the Kroki instance + # @param [Faraday::Connection] The Faraday connection to use # @param [String] The diagram description # @param [String] The language of the diagram description # @return [String] The rendered diagram in SVG format - def render_diagram(kroki_url, diagram_desc, language) - response = http_get(URI("#{kroki_url}/#{language}/svg/#{encode_diagram(diagram_desc.text)}")) - raise StandardError unless response.is_a?(Net::HTTPSuccess) - + def render_diagram(connection, diagram_desc, language) + begin + encoded_diagram = encode_diagram(diagram_desc.text) + response = connection.get("#{language}/svg/#{encoded_diagram}") + rescue Faraday::Error => e + raise e.response[:body] + end response.body end # Encodes the diagram into Kroki format using deflate + base64. # See https://docs.kroki.io/kroki/setup/encode-diagram/. # - # @param [String, #read] The diagram to encode + # @param [String, #read] The diagram description to encode # @return [String] The encoded diagram - def encode_diagram(diagram) - Base64.urlsafe_encode64(Zlib.deflate(diagram)) + def encode_diagram(diagram_desc) + Base64.urlsafe_encode64(Zlib.deflate(diagram_desc)) end # Gets an array of supported diagram languages from the Kroki '/health' endpoint. @@ -70,25 +78,30 @@ def encode_diagram(diagram) # configured Kroki instance. For example, Mermaid will still show up as a supported language even if the Mermaid # companion container is not running. # - # @param [URI::HTTP] The URL of the Kroki instance + # @param [Faraday::Connection] The Faraday connection to use # @return [Array] The supported diagram languages - def get_supported_languages(kroki_url) - response = http_get(URI("#{kroki_url}/health")) - raise StandardError unless response.is_a?(Net::HTTPSuccess) - - JSON.parse(response.body)["version"].keys + def get_supported_languages(connection) + begin + response = connection.get("health") + rescue Faraday::Error => e + raise e.response[:body] + end + response.body["version"].keys end - # Sends an HTTP GET request and returns the response. + # Sets up a new Faraday connection. # - # @param [URI] The URI to GET from - # @return [Net::HTTPResponse] The HTTP GET response - def http_get(uri) - Retriable.retriable(tries: 3) do - Net::HTTP.get_response(uri) + # @param [URI::HTTP] The URL of the Kroki instance + # @return [Faraday::Connection] The Faraday connection + def setup_connection(kroki_url) + retry_options = { max: HTTP_MAX_RETRIES, interval: 0.1, interval_randomness: 0.5, backoff_factor: 2, + exceptions: [Faraday::RequestTimeoutError, Faraday::ConflictError, Faraday::ServerError] } + + Faraday.new(url: kroki_url) do |builder| + builder.request :retry, retry_options + builder.response :json, content_type: /\bjson$/ + builder.response :raise_error end - rescue StandardError => e - raise e.message end # Gets the URL of the Kroki instance to use for rendering diagrams. @@ -106,11 +119,11 @@ def kroki_url(config) end end - # Determines whether a document may contain renderable diagram descriptions - it is in HTML format and is either + # Determines whether a document may contain embeddable diagram descriptions - it is in HTML format and is either # a Jekyll::Page or writeable Jekyll::Document. # - # @param [Jekyll::Page or Jekyll::Document] The document to check for renderability - def renderable?(doc) + # @param [Jekyll::Page or Jekyll::Document] The document to check for embedability + def embeddable?(doc) doc.output_ext == ".html" && (doc.is_a?(Jekyll::Page) || doc.write?) end end @@ -118,5 +131,5 @@ def renderable?(doc) end Jekyll::Hooks.register [:pages, :documents], :post_render do |doc| - Jekyll::Kroki.render(doc) if Jekyll::Kroki.renderable?(doc) + Jekyll::Kroki.embed(doc) if Jekyll::Kroki.embeddable?(doc) end From 09353e93fb53e6cac8fbf4fa1ac49faf367c05c6 Mon Sep 17 00:00:00 2001 From: Felix van Oost Date: Sat, 21 Oct 2023 23:22:26 -0400 Subject: [PATCH 3/4] Remove rubocop_todo file --- .rubocop_todo.yml | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index 0b329fc..0000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,37 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config` -# on 2023-10-18 03:15:03 UTC using RuboCop version 1.57.1. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -# Offense count: 4 -Lint/ShadowedException: - Exclude: - - 'lib/jekyll-kroki.rb' - - 'site/_plugins/jekyll-kroki.rb' - -# Offense count: 2 -Naming/AccessorMethodName: - Exclude: - - 'lib/jekyll-kroki.rb' - - 'site/_plugins/jekyll-kroki.rb' - -# Offense count: 2 -# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. -# CheckDefinitionPathHierarchyRoots: lib, spec, test, src -# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS -Naming/FileName: - Exclude: - - 'lib/jekyll-kroki.rb' - - 'site/_plugins/jekyll-kroki.rb' - -# Offense count: 2 -# Configuration parameters: AllowedConstants. -Style/Documentation: - Exclude: - - 'spec/**/*' - - 'test/**/*' - - 'lib/jekyll-kroki.rb' - - 'site/_plugins/jekyll-kroki.rb' From 6269a515e82e1f8c3a29c6f256acd846de37dd04 Mon Sep 17 00:00:00 2001 From: Felix van Oost Date: Sat, 21 Oct 2023 23:43:25 -0400 Subject: [PATCH 4/4] Don't retry requests with a 409 Conflict response --- lib/jekyll/kroki.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jekyll/kroki.rb b/lib/jekyll/kroki.rb index 894ff18..cb43800 100644 --- a/lib/jekyll/kroki.rb +++ b/lib/jekyll/kroki.rb @@ -95,7 +95,7 @@ def get_supported_languages(connection) # @return [Faraday::Connection] The Faraday connection def setup_connection(kroki_url) retry_options = { max: HTTP_MAX_RETRIES, interval: 0.1, interval_randomness: 0.5, backoff_factor: 2, - exceptions: [Faraday::RequestTimeoutError, Faraday::ConflictError, Faraday::ServerError] } + exceptions: [Faraday::RequestTimeoutError, Faraday::ServerError] } Faraday.new(url: kroki_url) do |builder| builder.request :retry, retry_options