Class: Jekyll::Kroki

Inherits:
Object
  • Object
show all
Defined in:
lib/jekyll/kroki.rb,
lib/jekyll/kroki/config.rb,
lib/jekyll/kroki/version.rb

Overview

Converts diagram descriptions into images using Kroki.

Defined Under Namespace

Classes: Config

Constant Summary collapse

SUPPORTED_LANGUAGES =
%w[actdiag blockdiag bpmn bytefield c4plantuml d2 dbml diagramsnet ditaa erd excalidraw
graphviz mermaid nomnoml nwdiag packetdiag pikchr plantuml rackdiag seqdiag structurizr
svgbob symbolator tikz umlet vega vegalite wavedrom wireviz].freeze
EXPECTED_HTML_TAGS =
%w[code div].freeze
DIAGRAM_SELECTOR =
SUPPORTED_LANGUAGES.flat_map do |language|
  EXPECTED_HTML_TAGS.map { |tag| "#{tag}[class~='language-#{language}']" }
end.join(", ").freeze
HTTP_RETRY_INTERVAL_BACKOFF_FACTOR =
2
HTTP_RETRY_INTERVAL_RANDOMNESS =
0.5
HTTP_RETRY_INTERVAL =
0.1
VERSION =
"1.0.0"

Class Method Summary collapse

Class Method Details

.embed_docs_in_site(site, connection, max_concurrent_docs) ⇒ Integer

Renders the diagram descriptions in all Jekyll pages and documents in the given Jekyll site. Pages / documents are rendered concurrently up to the limit defined by max_concurrent_docs.

Parameters:

  • The (Jekyll::Site)

    Jekyll site to embed diagrams in.

  • The (Faraday::Connection)

    Faraday connection to use.

  • The (Integer)

    maximum number of documents to render concurrently.

Returns:

  • (Integer)

    The number of successfully rendered diagrams.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/jekyll/kroki.rb', line 58

def embed_docs_in_site(site, connection, max_concurrent_docs)
  semaphore = Async::Semaphore.new(max_concurrent_docs)

  Async do
    (site.pages + site.documents).filter_map do |doc|
      next unless embeddable?(doc)

      semaphore.async do
        embed_single_doc(connection, doc)
      rescue StandardError => e
        Jekyll.logger.error "[jekyll-kroki] Failed to render diagram in '#{doc.relative_path}': #{e.message}"
        0
      end
    end.sum(&:wait)
  end.wait
end

.embed_single_doc(connection, doc) ⇒ Integer

Renders the supported diagram descriptions in a single document sequentially and embeds them as inline SVGs in the HTML source. Returns without modifying the document if no supported diagram descriptions are found.

Parameters:

  • The (Faraday::Connection)

    Faraday connection to use.

  • The (Jekyll::Page, Jekyll::Document)

    document to process.

Returns:

  • (Integer)

    The number of successfully rendered diagrams.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/jekyll/kroki.rb', line 81

def embed_single_doc(connection, doc)
  parsed_doc = Nokogiri::HTML(doc.output)
  nodes = parsed_doc.css(DIAGRAM_SELECTOR)
  return 0 if nodes.empty?

  nodes.each do |node|
    # Extract the diagram language from the class list.
    language = node["class"].split.grep(/\Alanguage-/).first.delete_prefix("language-")
    node.replace(render_diagram(connection, node.text, language))
  end

  # Convert the document back to HTML.
  doc.output = parsed_doc.to_html
  nodes.size
end

.embed_site(site) ⇒ Object

Renders and embeds all diagram descriptions in the given Jekyll site using Kroki.

Parameters:

  • The (Jekyll::Site)

    Jekyll site to embed diagrams in.



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/jekyll/kroki.rb', line 39

def embed_site(site)
  config = Config.new(site.config)
  connection = setup_connection(config.kroki_url, config.http_retries, config.http_timeout)

  rendered_diag = embed_docs_in_site(site, connection, config.max_concurrent_docs)
  return unless rendered_diag.positive?

  Jekyll.logger.info(
    "[jekyll-kroki] Rendered #{rendered_diag} diagrams using Kroki instance at '#{config.kroki_url}'"
  )
end

.embeddable?(doc) ⇒ Boolean

Determines whether a document may contain embeddable diagram descriptions; it is in HTML format and is either a Jekyll::Page or writeable Jekyll::Document.

Parameters:

  • The (Jekyll::Page or Jekyll::Document)

    document to check for embeddable diagrams.

Returns:

  • (Boolean)


172
173
174
# File 'lib/jekyll/kroki.rb', line 172

def embeddable?(doc)
  doc.output_ext == ".html" && (doc.is_a?(Jekyll::Page) || doc.write?)
end

.encode_diagram(diagram_desc) ⇒ String

Encodes the diagram into Kroki format using deflate + base64. See https://docs.kroki.io/kroki/setup/encode-diagram/.

Parameters:

  • The (String)

    diagram description to encode.

Returns:

  • (String)

    The encoded diagram.



144
145
146
# File 'lib/jekyll/kroki.rb', line 144

def encode_diagram(diagram_desc)
  Base64.urlsafe_encode64(Zlib.deflate(diagram_desc, Zlib::BEST_COMPRESSION))
end

.render_diagram(connection, diagram_text, language) ⇒ String

Renders a single diagram description using Kroki. The rendered diagram is cached to avoid redundant HTTP requests across documents, using the diagram language and the SHA1 of the diagram description as the key.

Parameters:

  • The (Faraday::Connection)

    Faraday connection to use.

  • The (String)

    diagram description.

  • The (String)

    language of the diagram description.

Returns:

  • (String)

    The rendered diagram in SVG format.



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/jekyll/kroki.rb', line 104

def render_diagram(connection, diagram_text, language)
  cache_key = "#{language}:#{Digest::SHA1.hexdigest(diagram_text)}"
  @diagram_cache.compute_if_absent(cache_key) do
    response = connection.get("#{language}/svg/#{encode_diagram(diagram_text)}")
    validate_content_type(response)
    sanitise_diagram(response.body)
  rescue Faraday::BadRequestError => e
    kroki_message = e.response_body.to_s.strip
    raise e, (kroki_message.empty? ? e.message : kroki_message)
  end
end

.sanitise_diagram(diagram_svg) ⇒ String

Sanitises a rendered diagram. Only