Skip to content

Commit

Permalink
resolves #145 fetch and save diagrams on local disk
Browse files Browse the repository at this point in the history
  • Loading branch information
ggrossetie committed Oct 9, 2020
1 parent 87a78b2 commit eeacab3
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 10 deletions.
Empty file.
1 change: 1 addition & 0 deletions ruby/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pkg/
.asciidoctor/kroki
7 changes: 5 additions & 2 deletions ruby/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ Metrics/MethodLength:
Max: 50

Metrics/CyclomaticComplexity:
Max : 10
Max: 10

Metrics/PerceivedComplexity:
Max: 10

Metrics/AbcSize:
Max: 25
Max: 30
172 changes: 164 additions & 8 deletions ruby/lib/asciidoctor/extensions/asciidoctor_kroki/extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
require 'stringio'
require 'zlib'
require 'digest'
require 'fileutils'

# Asciidoctor extensions
#
Expand All @@ -29,9 +31,11 @@ def process(parent, reader, attrs)
class KrokiBlockMacroProcessor < Asciidoctor::Extensions::BlockMacroProcessor
use_dsl

name_positional_attributes 'format'

def process(parent, target, attrs)
diagram_type = @name
target = parent.apply_subs(target, ['attributes'])
target = parent.apply_subs(target, [:attributes])
diagram_text = read(target)
KrokiProcessor.process(self, parent, attrs, diagram_type, diagram_text)
end
Expand All @@ -49,6 +53,8 @@ def read(target)
# Internal processor
#
class KrokiProcessor
TEXT_FORMATS = %w[txt atxt utxt].freeze

class << self
def process(processor, parent, attrs, diagram_type, diagram_text)
doc = parent.document
Expand All @@ -64,11 +70,18 @@ def process(processor, parent, attrs, diagram_type, diagram_text)
role = attrs['role']
format = get_format(doc, attrs, diagram_type)
attrs['role'] = get_role(format, role)
attrs['alt'] = get_alt(attrs)
attrs['target'] = create_image_src(doc, diagram_type, format, diagram_text)
attrs['format'] = format
block = processor.create_image_block(parent, attrs)
block.title = title
kroki_diagram = KrokiDiagram.new(diagram_type, format, diagram_text)
kroki_client = KrokiClient.new(doc, KrokiHttpClient)
if TEXT_FORMATS.include?(format)
text_content = kroki_client.text_content(kroki_diagram)
block = processor.create_block(parent, 'literal', text_content, attrs)
else
attrs['alt'] = get_alt(attrs)
attrs['target'] = create_image_src(doc, kroki_diagram, kroki_client)
block = processor.create_image_block(parent, attrs)
end
block.title = title if title
block.assign_caption(caption, 'figure')
block
end
Expand Down Expand Up @@ -119,14 +132,157 @@ def get_format(doc, attrs, diagram_type)
format
end

def create_image_src(doc, type, format, text)
data = Base64.urlsafe_encode64(Zlib::Deflate.deflate(text, 9))
"#{server_url(doc)}/#{type}/#{format}/#{data}"
def create_image_src(doc, kroki_diagram, kroki_client)
if doc.attr('kroki-fetch-diagram')
kroki_diagram.save(doc, kroki_client)
else
kroki_diagram.get_diagram_uri(server_url(doc))
end
end

def server_url(doc)
doc.attr('kroki-server-url') || 'https://kroki.io'
end
end
end

# Kroki diagram
#
class KrokiDiagram
attr_reader :type
attr_reader :text
attr_reader :format

def initialize(type, format, text)
@text = text
@type = type
@format = format
end

def get_diagram_uri(server_url)
"#{server_url}/#{@type}/#{@format}/#{encode}"
end

def encode
Base64.urlsafe_encode64(Zlib::Deflate.deflate(@text, 9))
end

def save(doc, kroki_client)
dir_path = dir_path(doc)
diagram_url = get_diagram_uri(kroki_client.server_url)
diagram_name = "diag-#{Digest::SHA256.hexdigest diagram_url}.#{@format}"
file_path = File.join(dir_path, diagram_name)
encoding = if @format == 'txt' || @format == 'atxt' || @format == 'utxt'
'utf8'
elsif @format == 'svg'
'binary'
else
'binary'
end
# file is either (already) on the file system or we should read it from Kroki
contents = File.exist?(file_path) ? read(file_path) : kroki_client.get_image(self, encoding)
FileUtils.mkdir_p(dir_path)
if encoding == 'binary'
File.binwrite(file_path, contents)
else
File.write(file_path, contents)
end
diagram_name
end

def read(target)
if target.start_with?('http://') || target.start_with?('https://')
require 'open-uri'
URI.open(target, &:read)
else
File.open(target, &:read)
end
end

def dir_path(doc)
images_output_dir = doc.attr('imagesoutdir')
out_dir = doc.attr('outdir')
to_dir = doc.attr('to_dir')
base_dir = doc.base_dir
images_dir = doc.attr('imagesdir', '')
if images_output_dir
images_output_dir
elsif out_dir
File.join(out_dir, images_dir)
elsif to_dir
File.join(to_dir, images_dir)
else
File.join(base_dir, images_dir)
end
end
end

# Kroki client
#
class KrokiClient
SUPPORTED_HTTP_METHODS = %w[get post adaptive].freeze

def initialize(doc, http_client)
@max_uri_length = 4096
@http_client = http_client
method = doc.attr('kroki-http-method', 'adaptive').downcase
if SUPPORTED_HTTP_METHODS.include?(method)
@method = method
else
puts "Invalid value '#{method}' for kroki-http-method attribute. The value must be either: 'get', 'post' or 'adaptive'. Proceeding using: 'adaptive'."
@method = 'adaptive'
end
@doc = doc
end

def text_content(kroki_diagram)
get_image(kroki_diagram, 'utf-8')
end

def get_image(kroki_diagram, encoding)
type = kroki_diagram.type
format = kroki_diagram.format
text = kroki_diagram.text
if @method == 'adaptive' || @method == 'get'
uri = kroki_diagram.get_diagram_uri(server_url)
if uri.length > @max_uri_length
# The request URI is longer than 4096.
if @method == 'get'
# The request might be rejected by the server with a 414 Request-URI Too Large.
# Consider using the attribute kroki-http-method with the value 'adaptive'.
@http_client.get(uri, encoding)
else
@http_client.post("#{server_url}/#{type}/#{format}", text, encoding)
end
else
@http_client.get(uri, encoding)
end
else
@http_client.post("#{server_url}/#{type}/#{format}", text, encoding)
end
end

def server_url
@doc.attr('kroki-server-url', 'https://kroki.io')
end
end

# Kroki HTTP client
#
class KrokiHttpClient
require 'net/http'
require 'uri'
require 'json'

class << self
def get(uri, _)
::OpenURI.open_uri(uri, &:read)
end

def post(uri, data, _)
res = ::Net::HTTP.request_post(uri, data)
res.body
end
end
end
end
26 changes: 26 additions & 0 deletions ruby/spec/asciidoctor_kroki_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,32 @@
<div class="content">
<img src="https://kroki.io/plantuml/png/eNorzs7MK0gsSsxVyM3Py0_OKMrPTVUoKSpN5YrJS8zJTE5V0LVTSMpPslLISM3JyQcArVsRHA==" alt="Diagram">
</div>
</div>)
end
it 'should create SVG diagram in imagesdir if kroki-fetch-diagram is set' do
input = <<~'ADOC'
:imagesdir: .asciidoctor/kroki
plantuml::spec/fixtures/alice.puml[svg,role=sequence]
ADOC
output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false)
(expect output).to eql %(<div class="imageblock sequence kroki-format-svg kroki">
<div class="content">
<img src=".asciidoctor/kroki/diag-f6acdc206506b6ca7badd3fe722f252af992871426e580c8361ff4d47c2c7d9b.svg" alt="Diagram">
</div>
</div>)
end
it 'should create PNG diagram in imagesdir if kroki-fetch-diagram is set' do
input = <<~'ADOC'
:imagesdir: .asciidoctor/kroki
plantuml::spec/fixtures/alice.puml[png,role=sequence]
ADOC
output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false)
(expect output).to eql %(<div class="imageblock sequence kroki-format-png kroki">
<div class="content">
<img src=".asciidoctor/kroki/diag-d4f314b2d4e75cc08aa4f8c2c944f7bf78321895d8ec5f665b42476d4e67e610.png" alt="Diagram">
</div>
</div>)
end
end
Expand Down
1 change: 1 addition & 0 deletions ruby/spec/fixtures/alice.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
alice -> bob: hello

0 comments on commit eeacab3

Please sign in to comment.