Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle HTTP retries #4

Merged
merged 4 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 0 additions & 37 deletions .rubocop_todo.yml

This file was deleted.

10 changes: 10 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ PATH
remote: .
specs:
jekyll-kroki (0.1.0)
faraday (~> 2.7)
faraday-retry (~> 2.2)
jekyll (~> 4)
nokogiri (~> 1.15)

Expand All @@ -18,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)
Expand Down Expand Up @@ -89,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)
Expand Down
6 changes: 4 additions & 2 deletions jekyll-kroki.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -28,6 +28,8 @@ 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"]

Expand Down
99 changes: 57 additions & 42 deletions lib/jekyll/kroki.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,73 @@
require_relative "kroki/version"

require "base64"
require "faraday"
require "faraday/retry"
require "jekyll"
require "json"
require "net/http"
require "nokogiri"
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 [String] 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)
# Encode the diagram and construct the URI
uri = URI("#{kroki_url}/#{language}/svg/#{encode_diagram(diagram_desc.text)}")

def render_diagram(connection, diagram_desc, language)
begin
response = Net::HTTP.get_response(uri)
rescue StandardError => e
raise e.message
else
response.body if response.is_a?(Net::HTTPSuccess)
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.
Expand All @@ -75,17 +78,29 @@ 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 [Faraday::Connection] The Faraday connection to use
# @return [Array] The supported diagram languages
def get_supported_languages(kroki_url)
uri = URI("#{kroki_url}/health")

def get_supported_languages(connection)
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)
response = connection.get("health")
rescue Faraday::Error => e
raise e.response[:body]
end
response.body["version"].keys
end

# Sets up a new Faraday connection.
#
# @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::ServerError] }

Faraday.new(url: kroki_url) do |builder|
builder.request :retry, retry_options
builder.response :json, content_type: /\bjson$/
builder.response :raise_error
end
end

Expand All @@ -104,17 +119,17 @@ 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
end
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