asciidoctor-kroki 0.1.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e516cef89499dde5cea46152f3926f6b91c8f81c9e8ff57884dac15d1877fd1c
4
- data.tar.gz: 919d391cf9ca8ad118ead138a8338434b7ffa6d6e15e6678a65a084aadbedd15
3
+ metadata.gz: dfa0c461bdb74571b618747d791e059125dc09a5e67dd6886861101fe972237f
4
+ data.tar.gz: d882ff8e33c787b3a1578eb7d11b590411e18f594eaec188d7f06ffc182a3b11
5
5
  SHA512:
6
- metadata.gz: f96a43e66d73647ff2593e74ef695526fa52c6a9cf453c22d34d11bcc035bf8e5bb4256d43eab35f5e8290b32f5f02c0df064a975b7c3164efd83e8d96c320f3
7
- data.tar.gz: 1536eb019a5b9fe3bd939ee64b6bd26252dd5096185f919c15536b6405f436baee91ca852a5519c2414b9ceee2215fad1228b28d8995a1da85ac583e7d5b0c86
6
+ metadata.gz: f72b7293f3fb8c6d37b456c39d4e2691c0b65e1cb908d5f3f29d23466f0c51135b9977dd0977a2ccac7adb667b4fc5071a4a5fc59bed6a3304154bddc8efda3d
7
+ data.tar.gz: 1e93fbf9df1cab9282f7743f76331ea6caddfdce150fff36fb7aa7222d561d089c31101ec0f16a939f5aa2c0c7dd5a09206059700974b76ae6fadcfc0150acd3
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  pkg/
2
+ .asciidoctor/kroki
data/.rubocop.yml CHANGED
@@ -1,14 +1,26 @@
1
1
  Style/Encoding:
2
2
  Enabled: false
3
3
 
4
+ Layout/EndOfLine:
5
+ EnforcedStyle: lf
6
+
4
7
  Metrics/LineLength:
5
- Max: 160
8
+ Max: 180
9
+
10
+ Metrics/ClassLength:
11
+ Max: 150
6
12
 
7
13
  Metrics/MethodLength:
8
14
  Max: 50
9
15
 
10
16
  Metrics/CyclomaticComplexity:
11
- Max : 10
17
+ Max: 10
18
+
19
+ Metrics/PerceivedComplexity:
20
+ Max: 10
12
21
 
13
22
  Metrics/AbcSize:
14
- Max: 25
23
+ Max: 30
24
+
25
+ Metrics/ParameterLists:
26
+ Max: 7
data/Gemfile.lock CHANGED
@@ -1,62 +1,19 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- asciidoctor-kroki (0.1.2)
4
+ asciidoctor-kroki (0.4.0)
5
5
  asciidoctor (~> 2.0)
6
- asciidoctor-pdf (= 1.5.3)
7
6
 
8
7
  GEM
9
8
  remote: https://rubygems.org/
10
9
  specs:
11
- Ascii85 (1.0.3)
12
- addressable (2.7.0)
13
- public_suffix (>= 2.0.2, < 5.0)
14
- afm (0.2.2)
15
- asciidoctor (2.0.10)
16
- asciidoctor-pdf (1.5.3)
17
- asciidoctor (>= 1.5.3, < 3.0.0)
18
- concurrent-ruby (~> 1.1.0)
19
- prawn (~> 2.2.0)
20
- prawn-icon (~> 2.5.0)
21
- prawn-svg (~> 0.30.0)
22
- prawn-table (~> 0.2.0)
23
- prawn-templates (~> 0.1.0)
24
- safe_yaml (~> 1.0.0)
25
- thread_safe (~> 0.3.0)
26
- treetop (~> 1.6.0)
27
- ttfunk (~> 1.5.0, >= 1.5.1)
10
+ asciidoctor (2.0.11)
28
11
  ast (2.4.1)
29
- concurrent-ruby (1.1.6)
30
- css_parser (1.7.1)
31
- addressable
32
12
  diff-lcs (1.3)
33
- hashery (2.1.2)
34
13
  jaro_winkler (1.5.4)
35
14
  parallel (1.19.1)
36
15
  parser (2.7.1.3)
37
16
  ast (~> 2.4.0)
38
- pdf-core (0.7.0)
39
- pdf-reader (2.4.0)
40
- Ascii85 (~> 1.0.0)
41
- afm (~> 0.2.1)
42
- hashery (~> 2.0)
43
- ruby-rc4
44
- ttfunk
45
- polyglot (0.3.5)
46
- prawn (2.2.2)
47
- pdf-core (~> 0.7.0)
48
- ttfunk (~> 1.5)
49
- prawn-icon (2.5.0)
50
- prawn (>= 1.1.0, < 3.0.0)
51
- prawn-svg (0.30.0)
52
- css_parser (~> 1.6)
53
- prawn (>= 0.11.1, < 3)
54
- prawn-table (0.2.2)
55
- prawn (>= 1.3.0, < 3.0.0)
56
- prawn-templates (0.1.2)
57
- pdf-reader (~> 2.0)
58
- prawn (~> 2.2)
59
- public_suffix (4.0.5)
60
17
  rainbow (3.0.0)
61
18
  rake (12.3.3)
62
19
  rspec (3.8.0)
@@ -80,12 +37,6 @@ GEM
80
37
  ruby-progressbar (~> 1.7)
81
38
  unicode-display_width (>= 1.4.0, < 1.7)
82
39
  ruby-progressbar (1.10.1)
83
- ruby-rc4 (0.1.5)
84
- safe_yaml (1.0.5)
85
- thread_safe (0.3.6)
86
- treetop (1.6.10)
87
- polyglot (~> 0.3)
88
- ttfunk (1.5.1)
89
40
  unicode-display_width (1.6.1)
90
41
 
91
42
  PLATFORMS
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'asciidoctor-kroki'
5
- s.version = '0.1.2'
5
+ s.version = '0.4.0'
6
6
  s.summary = 'Asciidoctor extension to convert diagrams to images using Kroki'
7
7
  s.description = 'An extension for Asciidoctor to convert diagrams to images using https://kroki.io'
8
8
 
@@ -19,7 +19,6 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ['lib']
20
20
 
21
21
  s.add_runtime_dependency 'asciidoctor', '~> 2.0'
22
- s.add_runtime_dependency 'asciidoctor-pdf', '1.5.3'
23
22
 
24
23
  s.add_development_dependency 'rake', '~> 12.3.2'
25
24
  s.add_development_dependency 'rspec', '~> 3.8.0'
@@ -4,8 +4,7 @@ require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
4
4
  require_relative 'asciidoctor_kroki/extension'
5
5
 
6
6
  Asciidoctor::Extensions.register do
7
- names = %w[plantuml ditaa graphviz blockdiag seqdiag actdiag nwdiag packetdiag rackdiag c4plantuml erd mermaid nomnoml svgbob umlet vega vegalite wavedrom]
8
- names.each do |name|
7
+ ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.each do |name|
9
8
  block_macro ::AsciidoctorExtensions::KrokiBlockMacroProcessor, name
10
9
  block ::AsciidoctorExtensions::KrokiBlockProcessor, name
11
10
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
4
- require 'stringio'
5
- require 'zlib'
6
4
 
7
5
  # Asciidoctor extensions
8
6
  #
@@ -17,11 +15,24 @@ module AsciidoctorExtensions
17
15
  on_context :listing, :literal
18
16
  name_positional_attributes 'target', 'format'
19
17
 
18
+ # @param name [String] name of the block macro (optional)
19
+ # @param config [Hash] a config hash (optional)
20
+ # @param logger [Logger] a logger used to log warning and errors (optional)
21
+ #
22
+ def initialize(name = nil, config = {}, logger: ::Asciidoctor::LoggerManager.logger)
23
+ super(name, config)
24
+ @logger = logger
25
+ end
26
+
20
27
  def process(parent, reader, attrs)
21
28
  diagram_type = @name
22
29
  diagram_text = reader.string
23
- KrokiProcessor.process(self, parent, attrs, diagram_type, diagram_text)
30
+ KrokiProcessor.process(self, parent, attrs, diagram_type, diagram_text, @logger)
24
31
  end
32
+
33
+ protected
34
+
35
+ attr_reader :logger
25
36
  end
26
37
 
27
38
  # A block macro extension that converts a diagram into an image.
@@ -29,11 +40,50 @@ module AsciidoctorExtensions
29
40
  class KrokiBlockMacroProcessor < Asciidoctor::Extensions::BlockMacroProcessor
30
41
  use_dsl
31
42
 
43
+ name_positional_attributes 'format'
44
+
45
+ # @param name [String] name of the block macro (optional)
46
+ # @param config [Hash] a config hash (optional)
47
+ # @param logger [Logger] a logger used to log warning and errors (optional)
48
+ #
49
+ def initialize(name = nil, config = {}, logger: ::Asciidoctor::LoggerManager.logger)
50
+ super(name, config)
51
+ @logger = logger
52
+ end
53
+
32
54
  def process(parent, target, attrs)
33
55
  diagram_type = @name
34
- target = parent.apply_subs(target, ['attributes'])
35
- diagram_text = read(target)
36
- KrokiProcessor.process(self, parent, attrs, diagram_type, diagram_text)
56
+ target = parent.apply_subs(target, [:attributes])
57
+
58
+ unless read_allowed?(target)
59
+ link = create_inline(parent, :anchor, target, type: :link, target: target)
60
+ return create_block(parent, :paragraph, link.convert, {}, content_model: :raw)
61
+ end
62
+
63
+ unless (path = resolve_target_path(target))
64
+ logger.error "#{diagram_type} block macro not found: #{target}."
65
+ create_block(parent, 'paragraph', unresolved_block_macro_message(diagram_type, target), {})
66
+ end
67
+
68
+ begin
69
+ diagram_text = read(path)
70
+ rescue => e # rubocop:disable RescueStandardError
71
+ logger.error "Failed to read #{diagram_type} file: #{path}. #{e}."
72
+ return create_block(parent, 'paragraph', unresolved_block_macro_message(diagram_type, path), {})
73
+ end
74
+ KrokiProcessor.process(self, parent, attrs, diagram_type, diagram_text, @logger)
75
+ end
76
+
77
+ protected
78
+
79
+ attr_reader :logger
80
+
81
+ def resolve_target_path(target)
82
+ target
83
+ end
84
+
85
+ def read_allowed?(_target)
86
+ true
37
87
  end
38
88
 
39
89
  def read(target)
@@ -44,15 +94,49 @@ module AsciidoctorExtensions
44
94
  File.open(target, &:read)
45
95
  end
46
96
  end
97
+
98
+ def unresolved_block_macro_message(name, target)
99
+ "Unresolved block macro - #{name}::#{target}[]"
100
+ end
101
+ end
102
+
103
+ # Kroki API
104
+ #
105
+ module Kroki
106
+ SUPPORTED_DIAGRAM_NAMES = %w[
107
+ actdiag
108
+ blockdiag
109
+ bpmn
110
+ bytefield
111
+ c4plantuml
112
+ ditaa
113
+ erd
114
+ excalidraw
115
+ graphviz
116
+ mermaid
117
+ nomnoml
118
+ nwdiag
119
+ packetdiag
120
+ plantuml
121
+ rackdiag
122
+ seqdiag
123
+ svgbob
124
+ umlet
125
+ vega
126
+ vegalite
127
+ wavedrom
128
+ ].freeze
47
129
  end
48
130
 
49
131
  # Internal processor
50
132
  #
51
133
  class KrokiProcessor
134
+ TEXT_FORMATS = %w[txt atxt utxt].freeze
135
+
52
136
  class << self
53
- def process(processor, parent, attrs, diagram_type, diagram_text)
137
+ def process(processor, parent, attrs, diagram_type, diagram_text, logger)
54
138
  doc = parent.document
55
- diagram_text = prepend_plantuml_config(diagram_text, diagram_type, doc)
139
+ diagram_text = prepend_plantuml_config(diagram_text, diagram_type, doc, logger)
56
140
  # If "subs" attribute is specified, substitute accordingly.
57
141
  # Be careful not to specify "specialcharacters" or your diagram code won't be valid anymore!
58
142
  if (subs = attrs['subs'])
@@ -64,23 +148,35 @@ module AsciidoctorExtensions
64
148
  role = attrs['role']
65
149
  format = get_format(doc, attrs, diagram_type)
66
150
  attrs['role'] = get_role(format, role)
67
- attrs['alt'] = get_alt(attrs)
68
- attrs['target'] = create_image_src(doc, diagram_type, format, diagram_text)
69
151
  attrs['format'] = format
70
- block = processor.create_image_block(parent, attrs)
71
- block.title = title
152
+ kroki_diagram = KrokiDiagram.new(diagram_type, format, diagram_text)
153
+ kroki_client = KrokiClient.new(server_url(doc), http_method(doc), KrokiHttpClient, logger, max_uri_length(doc))
154
+ if TEXT_FORMATS.include?(format)
155
+ text_content = kroki_client.text_content(kroki_diagram)
156
+ block = processor.create_block(parent, 'literal', text_content, attrs)
157
+ else
158
+ attrs['alt'] = get_alt(attrs)
159
+ attrs['target'] = create_image_src(doc, kroki_diagram, kroki_client)
160
+ block = processor.create_image_block(parent, attrs)
161
+ end
162
+ block.title = title if title
72
163
  block.assign_caption(caption, 'figure')
73
164
  block
74
165
  end
75
166
 
76
167
  private
77
168
 
78
- def prepend_plantuml_config(diagram_text, diagram_type, doc)
79
- if diagram_type == :plantuml && doc.attr?('kroki-plantuml-include')
80
- # TODO: this behaves different than the JS version
81
- # The file should be added by !include #{plantuml_include}" once we have a preprocessor for ruby
82
- config = File.read(doc.attr('kroki-plantuml-include'))
83
- diagram_text = config + '\n' + diagram_text
169
+ def prepend_plantuml_config(diagram_text, diagram_type, doc, logger)
170
+ if diagram_type == :plantuml && doc.safe < ::Asciidoctor::SafeMode::SECURE && doc.attr?('kroki-plantuml-include')
171
+ # REMIND: this behaves different than the JS version
172
+ # Once we have a preprocessor for Ruby, the value should be added in the diagram source as "!include #{plantuml_include}"
173
+ plantuml_include_path = doc.normalize_system_path(doc.attr('kroki-plantuml-include'))
174
+ if ::File.readable? plantuml_include_path
175
+ config = File.read(plantuml_include_path)
176
+ diagram_text = config + "\n" + diagram_text
177
+ else
178
+ logger.warn "Unable to read plantuml-include. File not found or not readable: #{plantuml_include_path}."
179
+ end
84
180
  end
85
181
  diagram_text
86
182
  end
@@ -119,13 +215,169 @@ module AsciidoctorExtensions
119
215
  format
120
216
  end
121
217
 
122
- def create_image_src(doc, type, format, text)
123
- data = Base64.urlsafe_encode64(Zlib::Deflate.deflate(text, 9))
124
- "#{server_url(doc)}/#{type}/#{format}/#{data}"
218
+ def create_image_src(doc, kroki_diagram, kroki_client)
219
+ if doc.attr('kroki-fetch-diagram') && doc.safe < ::Asciidoctor::SafeMode::SECURE
220
+ kroki_diagram.save(output_dir_path(doc), kroki_client)
221
+ else
222
+ kroki_diagram.get_diagram_uri(server_url(doc))
223
+ end
125
224
  end
126
225
 
127
226
  def server_url(doc)
128
- doc.attr('kroki-server-url') || 'https://kroki.io'
227
+ doc.attr('kroki-server-url', 'https://kroki.io')
228
+ end
229
+
230
+ def http_method(doc)
231
+ doc.attr('kroki-http-method', 'adaptive').downcase
232
+ end
233
+
234
+ def max_uri_length(doc)
235
+ doc.attr('kroki-max-uri-length', '4000').to_i
236
+ end
237
+
238
+ def output_dir_path(doc)
239
+ images_dir = doc.attr('imagesdir', '')
240
+ if (images_output_dir = doc.attr('imagesoutdir'))
241
+ images_output_dir
242
+ elsif (out_dir = doc.attr('outdir'))
243
+ File.join(out_dir, images_dir)
244
+ elsif (to_dir = doc.attr('to_dir'))
245
+ File.join(to_dir, images_dir)
246
+ else
247
+ File.join(doc.base_dir, images_dir)
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ # Kroki diagram
254
+ #
255
+ class KrokiDiagram
256
+ require 'fileutils'
257
+ require 'zlib'
258
+ require 'digest'
259
+
260
+ attr_reader :type
261
+ attr_reader :text
262
+ attr_reader :format
263
+
264
+ def initialize(type, format, text)
265
+ @text = text
266
+ @type = type
267
+ @format = format
268
+ end
269
+
270
+ def get_diagram_uri(server_url)
271
+ _join_uri_segments(server_url, @type, @format, encode)
272
+ end
273
+
274
+ def encode
275
+ Base64.urlsafe_encode64(Zlib::Deflate.deflate(@text, 9))
276
+ end
277
+
278
+ def save(output_dir_path, kroki_client)
279
+ diagram_url = get_diagram_uri(kroki_client.server_url)
280
+ diagram_name = "diag-#{Digest::SHA256.hexdigest diagram_url}.#{@format}"
281
+ file_path = File.join(output_dir_path, diagram_name)
282
+ encoding = if @format == 'txt' || @format == 'atxt' || @format == 'utxt'
283
+ 'utf8'
284
+ elsif @format == 'svg'
285
+ 'binary'
286
+ else
287
+ 'binary'
288
+ end
289
+ # file is either (already) on the file system or we should read it from Kroki
290
+ contents = File.exist?(file_path) ? File.open(file_path, &:read) : kroki_client.get_image(self, encoding)
291
+ FileUtils.mkdir_p(output_dir_path)
292
+ if encoding == 'binary'
293
+ File.binwrite(file_path, contents)
294
+ else
295
+ File.write(file_path, contents)
296
+ end
297
+ diagram_name
298
+ end
299
+
300
+ private
301
+
302
+ def _join_uri_segments(base, *uris)
303
+ segments = []
304
+ # remove trailing slashes
305
+ segments.push(base.gsub(%r{[/]+$}, ''))
306
+ segments.concat(uris.map do |uri|
307
+ # remove leading and trailing slashes
308
+ uri.to_s
309
+ .gsub(%r{^[/]+}, '')
310
+ .gsub(%r{[/]+$}, '')
311
+ end)
312
+ segments.join('/')
313
+ end
314
+ end
315
+
316
+ # Kroki client
317
+ #
318
+ class KrokiClient
319
+ attr_reader :server_url
320
+ attr_reader :method
321
+ attr_reader :max_uri_length
322
+
323
+ SUPPORTED_HTTP_METHODS = %w[get post adaptive].freeze
324
+
325
+ def initialize(server_url, http_method, http_client, logger = ::Asciidoctor::LoggerManager.logger, max_uri_length = 4000)
326
+ @server_url = server_url
327
+ @max_uri_length = max_uri_length
328
+ @http_client = http_client
329
+ method = (http_method || 'adaptive').downcase
330
+ if SUPPORTED_HTTP_METHODS.include?(method)
331
+ @method = method
332
+ else
333
+ logger.warn "Invalid value '#{method}' for kroki-http-method attribute. The value must be either: 'get', 'post' or 'adaptive'. Proceeding using: 'adaptive'."
334
+ @method = 'adaptive'
335
+ end
336
+ end
337
+
338
+ def text_content(kroki_diagram)
339
+ get_image(kroki_diagram, 'utf-8')
340
+ end
341
+
342
+ def get_image(kroki_diagram, encoding)
343
+ type = kroki_diagram.type
344
+ format = kroki_diagram.format
345
+ text = kroki_diagram.text
346
+ if @method == 'adaptive' || @method == 'get'
347
+ uri = kroki_diagram.get_diagram_uri(server_url)
348
+ if uri.length > @max_uri_length
349
+ # The request URI is longer than the max URI length.
350
+ if @method == 'get'
351
+ # The request might be rejected by the server with a 414 Request-URI Too Large.
352
+ # Consider using the attribute kroki-http-method with the value 'adaptive'.
353
+ @http_client.get(uri, encoding)
354
+ else
355
+ @http_client.post("#{@server_url}/#{type}/#{format}", text, encoding)
356
+ end
357
+ else
358
+ @http_client.get(uri, encoding)
359
+ end
360
+ else
361
+ @http_client.post("#{@server_url}/#{type}/#{format}", text, encoding)
362
+ end
363
+ end
364
+ end
365
+
366
+ # Kroki HTTP client
367
+ #
368
+ class KrokiHttpClient
369
+ require 'net/http'
370
+ require 'uri'
371
+ require 'json'
372
+
373
+ class << self
374
+ def get(uri, _)
375
+ ::OpenURI.open_uri(uri, 'r', &:read)
376
+ end
377
+
378
+ def post(uri, data, _)
379
+ res = ::Net::HTTP.request_post(uri, data)
380
+ res.body
129
381
  end
130
382
  end
131
383
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec_helper'
4
+ require 'asciidoctor'
5
+ require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki'
6
+ require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki/extension'
7
+
8
+ describe ::AsciidoctorExtensions::KrokiBlockMacroProcessor do
9
+ context 'convert to html5' do
10
+ it 'should catch exception if target is not readable' do
11
+ input = <<~'ADOC'
12
+ plantuml::spec/fixtures/missing.puml[svg,role=sequence]
13
+ ADOC
14
+ output = Asciidoctor.convert(input, standalone: false)
15
+ (expect output).to eql %(<div class="paragraph">
16
+ <p>Unresolved block macro - plantuml::spec/fixtures/missing.puml[]</p>
17
+ </div>)
18
+ end
19
+ end
20
+ context 'using a custom block macro' do
21
+ it 'should disallow read' do
22
+ # noinspection RubyClassModuleNamingConvention
23
+ class DisallowReadKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor
24
+ def read_allowed?(_target)
25
+ false
26
+ end
27
+ end
28
+ registry = Asciidoctor::Extensions.create do
29
+ block_macro DisallowReadKrokiBlockMacroProcessor, 'plantuml'
30
+ end
31
+ input = <<~'ADOC'
32
+ plantuml::spec/fixtures/alice.puml[svg,role=sequence]
33
+ ADOC
34
+ output = Asciidoctor.convert(input, standalone: false, extension_registry: registry)
35
+ (expect output).to eql %(<div class="paragraph">
36
+ <p><a href="spec/fixtures/alice.puml">spec/fixtures/alice.puml</a></p>
37
+ </div>)
38
+ end
39
+ it 'should allow read if target is not a URI' do
40
+ # noinspection RubyClassModuleNamingConvention
41
+ class DisallowUriReadKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor
42
+ def read_allowed?(target)
43
+ return false if ::Asciidoctor::Helpers.uriish?(target)
44
+
45
+ true
46
+ end
47
+ end
48
+ registry = Asciidoctor::Extensions.create do
49
+ block_macro DisallowUriReadKrokiBlockMacroProcessor, 'plantuml'
50
+ end
51
+ input = <<~'ADOC'
52
+ plantuml::https://domain.org/alice.puml[svg,role=sequence]
53
+
54
+ plantuml::file://path/to/alice.puml[svg,role=sequence]
55
+
56
+ plantuml::spec/fixtures/alice.puml[svg,role=sequence]
57
+ ADOC
58
+ output = Asciidoctor.convert(input, standalone: false, extension_registry: registry)
59
+ (expect output).to eql %(<div class="paragraph">
60
+ <p><a href="https://domain.org/alice.puml">https://domain.org/alice.puml</a></p>
61
+ </div>
62
+ <div class="paragraph">
63
+ <p><a href="file://path/to/alice.puml">file://path/to/alice.puml</a></p>
64
+ </div>
65
+ <div class="imageblock sequence kroki-format-svg kroki">
66
+ <div class="content">
67
+ <img src="https://kroki.io/plantuml/svg/eNpLzMlMTlXQtVNIyk-yUshIzcnJ5wIAQ-AGVQ==" alt="Diagram">
68
+ </div>
69
+ </div>)
70
+ end
71
+ it 'should override the resolve target method' do
72
+ # noinspection RubyClassModuleNamingConvention
73
+ class FixtureResolveTargetKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor
74
+ def resolve_target_path(target)
75
+ "spec/fixtures/#{target}"
76
+ end
77
+ end
78
+ registry = Asciidoctor::Extensions.create do
79
+ block_macro FixtureResolveTargetKrokiBlockMacroProcessor, 'plantuml'
80
+ end
81
+ input = <<~'ADOC'
82
+ plantuml::alice.puml[svg,role=sequence]
83
+ ADOC
84
+ output = Asciidoctor.convert(input, standalone: false, extension_registry: registry)
85
+ (expect output).to eql %(<div class="imageblock sequence kroki-format-svg kroki">
86
+ <div class="content">
87
+ <img src="https://kroki.io/plantuml/svg/eNpLzMlMTlXQtVNIyk-yUshIzcnJ5wIAQ-AGVQ==" alt="Diagram">
88
+ </div>
89
+ </div>)
90
+ end
91
+ it 'should display unresolved block macro message when the traget cannot be resolved' do
92
+ # noinspection RubyClassModuleNamingConvention
93
+ class UnresolvedTargetKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor
94
+ def resolve_target_path(_target)
95
+ nil
96
+ end
97
+ end
98
+ registry = Asciidoctor::Extensions.create do
99
+ block_macro UnresolvedTargetKrokiBlockMacroProcessor, 'plantuml'
100
+ end
101
+ input = <<~'ADOC'
102
+ plantuml::alice.puml[svg,role=sequence]
103
+ ADOC
104
+ output = Asciidoctor.convert(input, standalone: false, extension_registry: registry)
105
+ (expect output).to eql %(<div class="paragraph">
106
+ <p>Unresolved block macro - plantuml::[]</p>
107
+ </div>)
108
+ end
109
+ it 'should override the unresolved block macro message' do
110
+ # noinspection RubyClassModuleNamingConvention
111
+ class CustomUnresolvedTargetMessageKrokiBlockMacroProcessor < ::AsciidoctorExtensions::KrokiBlockMacroProcessor
112
+ def unresolved_block_macro_message(name, target)
113
+ "*[ERROR: #{name}::#{target}[] - unresolved block macro]*"
114
+ end
115
+ end
116
+ registry = Asciidoctor::Extensions.create do
117
+ block_macro CustomUnresolvedTargetMessageKrokiBlockMacroProcessor, 'plantuml'
118
+ end
119
+ input = <<~'ADOC'
120
+ plantuml::spec/fixtures/missing.puml[svg,role=sequence]
121
+ ADOC
122
+ output = Asciidoctor.convert(input, standalone: false, extension_registry: registry)
123
+ (expect output).to eql %(<div class="paragraph">
124
+ <p><strong>[ERROR: plantuml::spec/fixtures/missing.puml[] - unresolved block macro]</strong></p>
125
+ </div>)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec_helper'
4
+ require 'asciidoctor'
5
+ require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki'
6
+
7
+ describe ::AsciidoctorExtensions::KrokiClient do
8
+ it 'should use adaptive method when http method is invalid' do
9
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
10
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'patch', kroki_http_client)
11
+ expect(kroki_client.method).to eq('adaptive')
12
+ end
13
+ it 'should use post method when http method is post' do
14
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
15
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'POST', kroki_http_client)
16
+ expect(kroki_client.method).to eq('post')
17
+ end
18
+ it 'should use get method when http method is get' do
19
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
20
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'get', kroki_http_client)
21
+ expect(kroki_client.method).to eq('get')
22
+ end
23
+ it 'should use 4000 as the default max URI length' do
24
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
25
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'get', kroki_http_client)
26
+ expect(kroki_client.max_uri_length).to eq(4000)
27
+ end
28
+ it 'should use a custom value as max URI length' do
29
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
30
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'get', kroki_http_client, nil, 8000)
31
+ expect(kroki_client.max_uri_length).to eq(8000)
32
+ end
33
+ it 'should get an image with POST request if the URI length is greater than the value configured' do
34
+ kroki_http_client = Class.new do
35
+ class << self
36
+ def get(uri, _)
37
+ "GET #{uri}"
38
+ end
39
+
40
+ def post(uri, data, _)
41
+ "POST #{uri} - #{data}"
42
+ end
43
+ end
44
+ end
45
+ kroki_diagram = Class.new do
46
+ attr_reader :type, :text, :format
47
+
48
+ def initialize(type, format, text)
49
+ @text = text
50
+ @type = type
51
+ @format = format
52
+ end
53
+
54
+ def get_diagram_uri(_)
55
+ 'diagram-uri'
56
+ end
57
+ end.new('type', 'format', 'text')
58
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'adaptive', kroki_http_client, nil, 10)
59
+ result = kroki_client.get_image(kroki_diagram, 'utf8')
60
+ expect(result).to eq('POST http://localhost:8000/type/format - text')
61
+ end
62
+ it 'should get an image with GET request if the URI length is lower or equals than the value configured' do
63
+ kroki_http_client = Class.new do
64
+ class << self
65
+ def get(uri, _)
66
+ "GET #{uri}"
67
+ end
68
+
69
+ def post(uri, data, _)
70
+ "POST #{uri} - #{data}"
71
+ end
72
+ end
73
+ end
74
+ kroki_diagram = Class.new do
75
+ attr_reader :type, :text, :format
76
+
77
+ def initialize(type, format, text)
78
+ @text = text
79
+ @type = type
80
+ @format = format
81
+ end
82
+
83
+ def get_diagram_uri(_)
84
+ 'diagram-uri'
85
+ end
86
+ end.new('type', 'format', 'text')
87
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'adaptive', kroki_http_client, nil, 11)
88
+ result = kroki_client.get_image(kroki_diagram, 'utf8')
89
+ expect(result).to eq('GET diagram-uri')
90
+ end
91
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec_helper'
4
+ require 'asciidoctor'
5
+ require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki'
6
+
7
+ describe ::AsciidoctorExtensions::KrokiDiagram do
8
+ it 'should compute a diagram URI' do
9
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}')
10
+ diagram_uri = kroki_diagram.get_diagram_uri('http://localhost:8000')
11
+ expect(diagram_uri).to eq('http://localhost:8000/vegalite/png/eNqrrgUAAXUA-Q==')
12
+ end
13
+ it 'should compute a diagram URI with a trailing slashes' do
14
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}')
15
+ diagram_uri = kroki_diagram.get_diagram_uri('https://my.domain.org/kroki/')
16
+ expect(diagram_uri).to eq('https://my.domain.org/kroki/vegalite/png/eNqrrgUAAXUA-Q==')
17
+ end
18
+ it 'should compute a diagram URI with trailing slashes' do
19
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}')
20
+ diagram_uri = kroki_diagram.get_diagram_uri('https://my-server/kroki//')
21
+ expect(diagram_uri).to eq('https://my-server/kroki/vegalite/png/eNqrrgUAAXUA-Q==')
22
+ end
23
+ it 'should encode a diagram text definition' do
24
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'txt', ' alice -> bob: hello')
25
+ diagram_definition_encoded = kroki_diagram.encode
26
+ expect(diagram_definition_encoded).to eq('eNpTSMzJTE5V0LVTSMpPslLISM3JyQcAQAwGaw==')
27
+ end
28
+ it 'should fetch a diagram from Kroki and save it to disk' do
29
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'txt', ' alice -> bob: hello')
30
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
31
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('https://kroki.io', 'get', kroki_http_client)
32
+ output_dir_path = "#{__dir__}/../.asciidoctor/kroki"
33
+ diagram_name = kroki_diagram.save(output_dir_path, kroki_client)
34
+ diagram_path = File.join(output_dir_path, diagram_name)
35
+ expect(File.exist?(diagram_path)).to be_truthy, "expected diagram to be saved at #{diagram_path}"
36
+ content = <<-TXT.chomp
37
+ ,-----. ,---.
38
+ |alice| |bob|
39
+ `--+--' `-+-'
40
+ | hello |
41
+ |-------------->|
42
+ ,--+--. ,-+-.
43
+ |alice| |bob|
44
+ `-----' `---'
45
+ TXT
46
+ expect(File.read(diagram_path).split("\n").map(&:rstrip).join("\n")).to eq(content)
47
+ end
48
+ it 'should fetch a diagram from Kroki with the same definition only once' do
49
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'png', ' guillaume -> dan: hello')
50
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
51
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('https://kroki.io', 'get', kroki_http_client)
52
+ output_dir_path = "#{__dir__}/../.asciidoctor/kroki"
53
+ # make sure that we are doing only one GET request
54
+ expect(kroki_http_client).to receive(:get).once
55
+ diagram_name = kroki_diagram.save(output_dir_path, kroki_client)
56
+ diagram_path = File.join(output_dir_path, diagram_name)
57
+ expect(File.exist?(diagram_path)).to be_truthy, "expected diagram to be saved at #{diagram_path}"
58
+ # calling again... should read the file from disk (and not do a GET request)
59
+ kroki_diagram.save(output_dir_path, kroki_client)
60
+ end
61
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rspec_helper'
3
4
  require 'asciidoctor'
4
5
  require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki'
5
6
 
@@ -33,19 +34,116 @@ describe ::AsciidoctorExtensions::KrokiBlockProcessor do
33
34
  </div>
34
35
  </div>)
35
36
  end
36
- it 'should include the plantuml-include file' do
37
+ it 'should include the plantuml-include file when safe mode is safe' do
37
38
  input = <<~'ADOC'
38
39
  [plantuml]
39
40
  ....
40
41
  alice -> bob: hello
41
42
  ....
42
43
  ADOC
43
- output = Asciidoctor.convert(input, attributes: { 'env-idea' => '', 'kroki-plantuml-include' => 'spec/fixtures/config.puml' }, standalone: false)
44
+ output = Asciidoctor.convert(input, attributes: { 'env-idea' => '', 'kroki-plantuml-include' => 'spec/fixtures/config.puml' }, standalone: false, safe: :safe)
44
45
  (expect output).to eql %(<div class="imageblock kroki">
45
46
  <div class="content">
46
- <img src="https://kroki.io/plantuml/png/eNorzs7MK0gsSsxVyM3Py0_OKMrPTVUoKSpN5YrJS8zJTE5V0LVTSMpPslLISM3JyQcArVsRHA==" alt="Diagram">
47
+ <img src="https://kroki.io/plantuml/png/eNorzs7MK0gsSsxVyM3Py0_OKMrPTVUoKSpN5eJKzMlMTlXQtVNIyk-yUshIzcnJBwCT9xBc" alt="Diagram">
47
48
  </div>
48
49
  </div>)
49
50
  end
51
+ it 'should normalize plantuml-include path when safe mode is safe' do
52
+ input = <<~'ADOC'
53
+ [plantuml]
54
+ ....
55
+ alice -> bob: hello
56
+ ....
57
+ ADOC
58
+ output = Asciidoctor.convert(input, attributes: { 'env-idea' => '', 'kroki-plantuml-include' => '../../../spec/fixtures/config.puml' }, standalone: false, safe: :safe)
59
+ (expect output).to eql %(<div class="imageblock kroki">
60
+ <div class="content">
61
+ <img src="https://kroki.io/plantuml/png/eNorzs7MK0gsSsxVyM3Py0_OKMrPTVUoKSpN5eJKzMlMTlXQtVNIyk-yUshIzcnJBwCT9xBc" alt="Diagram">
62
+ </div>
63
+ </div>)
64
+ end
65
+ it 'should not include file which reside outside of the parent directory of the source when safe mode is safe' do
66
+ input = <<~'ADOC'
67
+ [plantuml]
68
+ ....
69
+ alice -> bob: hello
70
+ ....
71
+ ADOC
72
+ output = Asciidoctor.convert(input, attributes: { 'env-idea' => '', 'kroki-plantuml-include' => '/etc/passwd' }, standalone: false, safe: :safe)
73
+ (expect output).to eql %(<div class="imageblock kroki">
74
+ <div class="content">
75
+ <img src="https://kroki.io/plantuml/png/eNpLzMlMTlXQtVNIyk-yUshIzcnJBwA9iwZL" alt="Diagram">
76
+ </div>
77
+ </div>)
78
+ end
79
+ it 'should not include file when safe mode is secure' do
80
+ input = <<~'ADOC'
81
+ [plantuml]
82
+ ....
83
+ alice -> bob: hello
84
+ ....
85
+ ADOC
86
+ output = Asciidoctor.convert(input, attributes: { 'env-idea' => '', 'kroki-plantuml-include' => 'spec/fixtures/config.puml' }, standalone: false, safe: :secure)
87
+ (expect output).to eql %(<div class="imageblock kroki">
88
+ <div class="content">
89
+ <img src="https://kroki.io/plantuml/png/eNpLzMlMTlXQtVNIyk-yUshIzcnJBwA9iwZL" alt="Diagram">
90
+ </div>
91
+ </div>)
92
+ end
93
+ it 'should create SVG diagram in imagesdir if kroki-fetch-diagram is set' do
94
+ input = <<~'ADOC'
95
+ :imagesdir: .asciidoctor/kroki
96
+
97
+ plantuml::spec/fixtures/alice.puml[svg,role=sequence]
98
+ ADOC
99
+ output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false, safe: :safe)
100
+ (expect output).to eql %(<div class="imageblock sequence kroki-format-svg kroki">
101
+ <div class="content">
102
+ <img src=".asciidoctor/kroki/diag-f6acdc206506b6ca7badd3fe722f252af992871426e580c8361ff4d47c2c7d9b.svg" alt="Diagram">
103
+ </div>
104
+ </div>)
105
+ end
106
+ it 'should not fetch diagram when safe mode is secure' do
107
+ input = <<~'ADOC'
108
+ :imagesdir: .asciidoctor/kroki
109
+
110
+ plantuml::spec/fixtures/alice.puml[svg,role=sequence]
111
+ ADOC
112
+ output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false)
113
+ (expect output).to eql %(<div class="imageblock sequence kroki-format-svg kroki">
114
+ <div class="content">
115
+ <img src="https://kroki.io/plantuml/svg/eNpLzMlMTlXQtVNIyk-yUshIzcnJ5wIAQ-AGVQ==" alt="Diagram">
116
+ </div>
117
+ </div>)
118
+ end
119
+ it 'should create PNG diagram in imagesdir if kroki-fetch-diagram is set' do
120
+ input = <<~'ADOC'
121
+ :imagesdir: .asciidoctor/kroki
122
+
123
+ plantuml::spec/fixtures/alice.puml[png,role=sequence]
124
+ ADOC
125
+ output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false, safe: :safe)
126
+ (expect output).to eql %(<div class="imageblock sequence kroki-format-png kroki">
127
+ <div class="content">
128
+ <img src=".asciidoctor/kroki/diag-d4f314b2d4e75cc08aa4f8c2c944f7bf78321895d8ec5f665b42476d4e67e610.png" alt="Diagram">
129
+ </div>
130
+ </div>)
131
+ end
132
+ end
133
+ end
134
+
135
+ describe ::AsciidoctorExtensions::Kroki do
136
+ it 'should return the list of supported diagrams' do
137
+ diagram_names = ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES
138
+ expect(diagram_names).to include('vegalite', 'plantuml', 'bytefield', 'bpmn', 'excalidraw', 'wavedrom')
139
+ end
140
+ it 'should register the extension for the list of supported diagrams' do
141
+ doc = Asciidoctor::Document.new
142
+ registry = Asciidoctor::Extensions::Registry.new
143
+ registry.activate doc
144
+ ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.each do |name|
145
+ expect(registry.find_block_extension(name)).to_not be_nil, "expected block extension named '#{name}' to be registered"
146
+ expect(registry.find_block_macro_extension(name)).to_not be_nil, "expected block macro extension named '#{name}' to be registered "
147
+ end
50
148
  end
51
149
  end
@@ -0,0 +1 @@
1
+ alice -> bob: hello
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.configure do |config|
4
+ config.before(:suite) do
5
+ FileUtils.rm(Dir.glob("#{__dir__}/../.asciidoctor/kroki/diag-*"))
6
+ end
7
+ config.after(:suite) do
8
+ FileUtils.rm(Dir.glob("#{__dir__}/../.asciidoctor/kroki/diag-*")) unless ENV['DEBUG']
9
+ end
10
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoctor-kroki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillaume Grossetie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-06 00:00:00.000000000 Z
11
+ date: 2021-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
- - !ruby/object:Gem::Dependency
28
- name: asciidoctor-pdf
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - '='
32
- - !ruby/object:Gem::Version
33
- version: 1.5.3
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - '='
39
- - !ruby/object:Gem::Version
40
- version: 1.5.3
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rake
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -87,6 +73,7 @@ executables: []
87
73
  extensions: []
88
74
  extra_rdoc_files: []
89
75
  files:
76
+ - ".asciidoctor/kroki/.gitkeep"
90
77
  - ".gitignore"
91
78
  - ".rubocop.yml"
92
79
  - ".ruby-version"
@@ -98,9 +85,14 @@ files:
98
85
  - lib/asciidoctor/extensions/asciidoctor_kroki.rb
99
86
  - lib/asciidoctor/extensions/asciidoctor_kroki/extension.rb
100
87
  - spec/.rubocop.yml
88
+ - spec/asciidoctor_kroki_block_macro_spec.rb
89
+ - spec/asciidoctor_kroki_client_spec.rb
90
+ - spec/asciidoctor_kroki_diagram_spec.rb
101
91
  - spec/asciidoctor_kroki_spec.rb
92
+ - spec/fixtures/alice.puml
102
93
  - spec/fixtures/config.puml
103
94
  - spec/require_spec.rb
95
+ - spec/rspec_helper.rb
104
96
  - tasks/bundler.rake
105
97
  - tasks/lint.rake
106
98
  - tasks/rspec.rake
@@ -131,9 +123,14 @@ specification_version: 4
131
123
  summary: Asciidoctor extension to convert diagrams to images using Kroki
132
124
  test_files:
133
125
  - spec/.rubocop.yml
126
+ - spec/asciidoctor_kroki_block_macro_spec.rb
127
+ - spec/asciidoctor_kroki_client_spec.rb
128
+ - spec/asciidoctor_kroki_diagram_spec.rb
134
129
  - spec/asciidoctor_kroki_spec.rb
130
+ - spec/fixtures/alice.puml
135
131
  - spec/fixtures/config.puml
136
132
  - spec/require_spec.rb
133
+ - spec/rspec_helper.rb
137
134
  - tasks/bundler.rake
138
135
  - tasks/lint.rake
139
136
  - tasks/rspec.rake