asciidoctor-diagram 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,8 @@ module Asciidoctor
5
5
  module Diagram
6
6
  # @private
7
7
  module PlantUml
8
+ include Which
9
+
8
10
  private
9
11
 
10
12
  JARS = ['plantuml.jar'].map do |jar|
@@ -26,6 +28,11 @@ module Asciidoctor
26
28
  headers['X-PlantUML-Config'] = File.expand_path(config_file, parent.document.attributes['docdir'])
27
29
  end
28
30
 
31
+ dot = which(parent, 'dot', :attr_names => ['dot', 'graphvizdot'], :raise_on_error => false)
32
+ if dot
33
+ headers['X-Graphviz'] = dot
34
+ end
35
+
29
36
  response = Java.send_request(
30
37
  :url => '/plantuml',
31
38
  :body => code,
@@ -11,7 +11,7 @@ module Asciidoctor
11
11
  def self.included(mod)
12
12
  [:png, :svg].each do |f|
13
13
  mod.register_format(f, :image) do |c, p|
14
- CliGenerator.generate_stdin(which(p, 'shaape'), c.to_s) do |tool_path, output_path|
14
+ CliGenerator.generate_stdin(which(p, 'shaape'), f.to_s, c.to_s) do |tool_path, output_path|
15
15
  [tool_path, '-o', output_path, '-t', f.to_s, '-']
16
16
  end
17
17
  end
@@ -8,12 +8,30 @@ module Asciidoctor
8
8
  @offset = 0
9
9
  end
10
10
 
11
+ def read_uint16_be
12
+ uint16 = @data[@offset,2].unpack('n')[0]
13
+ @offset += 2
14
+ uint16
15
+ end
16
+
17
+ def read_uint16_le
18
+ uint16 = @data[@offset,2].unpack('v')[0]
19
+ @offset += 2
20
+ uint16
21
+ end
22
+
11
23
  def read_uint32_be
12
24
  uint32 = @data[@offset,4].unpack('N')[0]
13
25
  @offset += 4
14
26
  uint32
15
27
  end
16
28
 
29
+ def read_uint32_le
30
+ uint32 = @data[@offset,4].unpack('V')[0]
31
+ @offset += 4
32
+ uint32
33
+ end
34
+
17
35
  def read_string(length, encoding = Encoding::ASCII_8BIT)
18
36
  str = @data[@offset,length]
19
37
  @offset += length
@@ -1,50 +1,39 @@
1
1
  require 'tempfile'
2
+ require 'open3'
2
3
 
3
4
  module Asciidoctor
4
5
  module Diagram
5
6
  # @private
6
7
  module CliGenerator
7
- def self.generate_stdin(tool, code)
8
+ def self.generate_stdin(tool, format, code)
8
9
  tool_name = File.basename(tool)
9
10
 
10
- target_file = Tempfile.new(tool_name)
11
+ target_file = Tempfile.new([tool_name, ".#{format}"])
11
12
  begin
12
13
  target_file.close
13
14
 
14
- args = yield tool, target_file.path
15
+ opts = yield tool, target_file.path
15
16
 
16
- IO.popen(args, "w") do |io|
17
- io.write code
18
- end
19
- result_code = $?
20
-
21
- raise "#{tool_name} image generation failed" unless result_code == 0
22
-
23
- File.binread(target_file.path)
17
+ generate(opts, target_file, :stdin_data => code)
24
18
  ensure
25
19
  target_file.unlink
26
20
  end
27
21
  end
28
22
 
29
- def self.generate_file(tool, code)
23
+ def self.generate_file(tool, format, code)
30
24
  tool_name = File.basename(tool)
31
25
 
32
- source_file = Tempfile.new(tool_name)
26
+ source_file = Tempfile.new([tool_name, ".#{format}"])
33
27
  begin
34
28
  File.write(source_file.path, code)
35
29
 
36
- target_file = Tempfile.new(tool_name)
30
+ target_file = Tempfile.new([tool_name, ".#{format}"])
37
31
  begin
38
32
  target_file.close
39
33
 
40
- args = yield tool, source_file.path, target_file.path
41
-
42
- system(*args)
43
- result_code = $?
44
-
45
- raise "#{tool_name} image generation failed" unless result_code == 0
34
+ opts = yield tool, source_file.path, target_file.path
46
35
 
47
- File.binread(target_file.path)
36
+ generate(opts, target_file)
48
37
  ensure
49
38
  target_file.unlink
50
39
  end
@@ -52,6 +41,37 @@ module Asciidoctor
52
41
  source_file.unlink
53
42
  end
54
43
  end
44
+
45
+ def self.generate(opts, target_file, open3_opts = {})
46
+ case opts
47
+ when Array
48
+ args = opts
49
+ out_file = nil
50
+ when Hash
51
+ args = opts[:args]
52
+ out_file = opts[:out_file]
53
+ else
54
+ raise "Block passed to generate_file should return an Array or a Hash"
55
+ end
56
+
57
+ run_cli(*args, open3_opts)
58
+
59
+ if out_file
60
+ File.rename(out_file, target_file.path)
61
+ end
62
+
63
+ File.binread(target_file.path)
64
+ end
65
+
66
+ def self.run_cli(*args)
67
+ stdout, stderr, status = Open3.capture3(*args)
68
+
69
+ if status != 0
70
+ raise "#{File.basename(args[0])} failed: #{stdout.empty? ? stderr : stdout}"
71
+ end
72
+
73
+ stdout
74
+ end
55
75
  end
56
76
  end
57
77
  end
@@ -0,0 +1,21 @@
1
+ require_relative 'binaryio'
2
+
3
+ module Asciidoctor
4
+ module Diagram
5
+ # @private
6
+ module GIF
7
+ GIF87A_SIGNATURE = 'GIF87a'.force_encoding(Encoding::ASCII_8BIT)
8
+ GIF89A_SIGNATURE = 'GIF89a'.force_encoding(Encoding::ASCII_8BIT)
9
+
10
+ def self.get_image_size(data)
11
+ bio = BinaryIO.new(data)
12
+ gif_signature = bio.read_string(6)
13
+ raise "Invalid GIF signature" unless gif_signature == GIF87A_SIGNATURE || gif_signature == GIF89A_SIGNATURE
14
+
15
+ width = bio.read_uint16_le
16
+ height = bio.read_uint16_le
17
+ [width, height]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -6,7 +6,7 @@ module Asciidoctor
6
6
  module Java
7
7
  def self.classpath
8
8
  @classpath ||= [
9
- File.expand_path(File.join('../..', 'asciidoctor-diagram-java-1.3.7.jar'), File.dirname(__FILE__))
9
+ File.expand_path(File.join('../..', 'asciidoctor-diagram-java-1.3.8.jar'), File.dirname(__FILE__))
10
10
  ]
11
11
  end
12
12
 
@@ -0,0 +1,25 @@
1
+ require 'rbconfig'
2
+
3
+ module Asciidoctor
4
+ module Diagram
5
+ module Platform
6
+ def self.os
7
+ @os ||= (
8
+ host_os = RbConfig::CONFIG['host_os']
9
+ case host_os
10
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
11
+ :windows
12
+ when /darwin|mac os/
13
+ :macosx
14
+ when /linux/
15
+ :linux
16
+ when /solaris|bsd/
17
+ :unix
18
+ else
19
+ raise Error::WebDriverError, "unknown os: #{host_os.inspect}"
20
+ end
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -26,9 +26,9 @@ module Asciidoctor
26
26
  private
27
27
 
28
28
  START_TAG_REGEX = /<svg[^>]*>/
29
- WIDTH_REGEX = /width="(?<value>\d+)(?<unit>[a-zA-Z]+)"/
30
- HEIGHT_REGEX = /height="(?<value>\d+)(?<unit>[a-zA-Z]+)"/
31
- VIEWBOX_REGEX = /viewBox="\d+ \d+ (?<width>\d+) (?<height>\d+)"/
29
+ WIDTH_REGEX = /width="(?<value>\d+(?:\.\d+)?)(?<unit>[a-zA-Z]+)?"/
30
+ HEIGHT_REGEX = /height="(?<value>\d+(?:\.\d+)?)(?<unit>[a-zA-Z]+)?"/
31
+ VIEWBOX_REGEX = /viewBox="\d+(?:\.\d+)? \d+(?:\.\d+)? (?<width>\d+(?:\.\d+)?) (?<height>\d+(?:\.\d+)?)"/
32
32
 
33
33
  def self.to_px_factor(unit)
34
34
  case unit
@@ -20,13 +20,19 @@ module Asciidoctor
20
20
 
21
21
  cmd_var = '@' + attr_names[0]
22
22
 
23
- cmd_path = instance_variable_get(cmd_var)
24
- unless cmd_path
23
+ already_defined = instance_variable_defined?(cmd_var)
24
+
25
+ if already_defined
26
+ cmd_path = instance_variable_get(cmd_var)
27
+ else
25
28
  cmd_path = attr_names.map { |attr_name| parent_block.document.attributes[attr_name] }.find { |attr| !attr.nil? }
26
29
  cmd_path = ::Asciidoctor::Diagram::Which.which(cmd, :path => options[:path]) unless cmd_path && File.executable?(cmd_path)
27
- raise "Could not find the '#{cmd}' executable in PATH; add it to the PATH or specify its location using the '#{attr_names[0]}' document attribute" unless cmd_path
28
30
  instance_variable_set(cmd_var, cmd_path)
31
+ if cmd_path.nil? && options.fetch(:raise_on_error, true)
32
+ raise "Could not find the '#{cmd}' executable in PATH; add it to the PATH or specify its location using the '#{attr_names[0]}' document attribute"
33
+ end
29
34
  end
35
+
30
36
  cmd_path
31
37
  end
32
38
  end
@@ -1,5 +1,5 @@
1
1
  module Asciidoctor
2
2
  module Diagram
3
- VERSION = "1.3.2"
3
+ VERSION = "1.4.0"
4
4
  end
5
5
  end
@@ -1,5 +1,6 @@
1
1
  require_relative '../extensions'
2
2
  require_relative '../util/cli_generator'
3
+ require_relative '../util/platform'
3
4
  require_relative '../util/which'
4
5
 
5
6
  module Asciidoctor
@@ -11,17 +12,26 @@ module Asciidoctor
11
12
  def self.included(mod)
12
13
  [:png, :svg].each do |f|
13
14
  mod.register_format(f, :image) do |c, p|
14
- if /darwin/ =~ RUBY_PLATFORM
15
- wavedrom = which(p, 'WaveDromEditor.app', :attr_names => ['wavedrom'], :path => ['/Applications'])
16
- if wavedrom
17
- wavedrom = File.join(wavedrom, 'Contents/MacOS/nwjs')
15
+ wavedrom_cli = which(p, 'wavedrom', :raise_on_error => false)
16
+ phantomjs = which(p, 'phantomjs', :raise_on_error => false)
17
+
18
+ if wavedrom_cli && phantomjs
19
+ CliGenerator.generate_file(wavedrom_cli, f.to_s, c.to_s) do |tool_path, input_path, output_path|
20
+ [phantomjs, tool_path, '-i', input_path, "-#{f.to_s[0]}", output_path]
18
21
  end
19
22
  else
20
- wavedrom = which(p, 'WaveDromEditor', :attr_names => ['wavedrom'])
21
- end
23
+ if ::Asciidoctor::Diagram::Platform.os == :macosx
24
+ wavedrom = which(p, 'WaveDromEditor.app', :attr_names => ['wavedrom'], :path => ['/Applications'])
25
+ if wavedrom
26
+ wavedrom = File.join(wavedrom, 'Contents/MacOS/nwjs')
27
+ end
28
+ else
29
+ wavedrom = which(p, 'WaveDromEditor', :attr_names => ['wavedrom'])
30
+ end
22
31
 
23
- CliGenerator.generate_file(wavedrom, c.to_s) do |tool_path, input_path, output_path|
24
- [tool_path, 'source', input_path, f.to_s, output_path]
32
+ CliGenerator.generate_file(wavedrom, f.to_s, c.to_s) do |tool_path, input_path, output_path|
33
+ [tool_path, 'source', input_path, f.to_s, output_path]
34
+ end
25
35
  end
26
36
  end
27
37
  end
data/spec/ditaa_spec.rb CHANGED
@@ -159,7 +159,7 @@ Doc Writer <doc@example.com>
159
159
 
160
160
  == First Section
161
161
 
162
- [ditaa, {opts}]
162
+ [ditaa, test, png, {opts}]
163
163
  ----
164
164
  +--------+ +-------+ +-------+
165
165
  | | --+ ditaa +--> | |
data/spec/man.jpg ADDED
Binary file
data/spec/meme_spec.rb ADDED
@@ -0,0 +1,67 @@
1
+ require_relative 'test_helper'
2
+
3
+ describe Asciidoctor::Diagram::MemeBlockMacroProcessor do
4
+ it "should generate PNG images when format is set to 'png'" do
5
+ FileUtils.cp(
6
+ File.expand_path('man.jpg', File.dirname(__FILE__)),
7
+ File.expand_path('man.jpg', Dir.getwd)
8
+ )
9
+
10
+ doc = <<-eos
11
+ = Hello, PlantUML!
12
+ Doc Writer <doc@example.com>
13
+
14
+ == First Section
15
+
16
+ meme::man.jpg[I don't always // write unit tests, but when I do // they generate memes, format=png, options=noupcase]
17
+ eos
18
+
19
+ d = Asciidoctor.load StringIO.new(doc)
20
+ expect(d).to_not be_nil
21
+
22
+ b = d.find { |b| b.context == :image }
23
+ expect(b).to_not be_nil
24
+
25
+ expect(b.content_model).to eq :empty
26
+
27
+ target = b.attributes['target']
28
+ expect(target).to_not be_nil
29
+ expect(target).to match /\.png$/
30
+ expect(File.exists?(target)).to be true
31
+
32
+ expect(b.attributes['width']).to_not be_nil
33
+ expect(b.attributes['height']).to_not be_nil
34
+ end
35
+
36
+ it "should generate GIF images when format is set to 'gif'" do
37
+ FileUtils.cp(
38
+ File.expand_path('man.jpg', File.dirname(__FILE__)),
39
+ File.expand_path('man.jpg', Dir.getwd)
40
+ )
41
+
42
+ doc = <<-eos
43
+ = Hello, PlantUML!
44
+ Doc Writer <doc@example.com>
45
+
46
+ == First Section
47
+
48
+ meme::man.jpg[I don't always // write unit tests, but when I do // they generate memes, format=gif, options=noupcase]
49
+ eos
50
+
51
+ d = Asciidoctor.load StringIO.new(doc)
52
+ expect(d).to_not be_nil
53
+
54
+ b = d.find { |b| b.context == :image }
55
+ expect(b).to_not be_nil
56
+
57
+ expect(b.content_model).to eq :empty
58
+
59
+ target = b.attributes['target']
60
+ expect(target).to_not be_nil
61
+ expect(target).to match /\.gif/
62
+ expect(File.exists?(target)).to be true
63
+
64
+ expect(b.attributes['width']).to_not be_nil
65
+ expect(b.attributes['height']).to_not be_nil
66
+ end
67
+ end
@@ -0,0 +1,277 @@
1
+ require_relative 'test_helper'
2
+
3
+ code = <<-eos
4
+ graph LR
5
+ A[Square Rect] -- Link text --> B((Circle))
6
+ A --> C(Round Rect)
7
+ B --> D{Rhombus}
8
+ C --> D
9
+ eos
10
+
11
+ describe Asciidoctor::Diagram::MermaidBlockMacroProcessor do
12
+ it "should generate PNG images when format is set to 'png'" do
13
+ File.write('mermaid.txt', code)
14
+
15
+ doc = <<-eos
16
+ = Hello, Mermaid!
17
+ Doc Writer <doc@example.com>
18
+
19
+ == First Section
20
+
21
+ mermaid::mermaid.txt[format="png"]
22
+ eos
23
+
24
+ d = Asciidoctor.load StringIO.new(doc)
25
+ expect(d).to_not be_nil
26
+
27
+ b = d.find { |b| b.context == :image }
28
+ expect(b).to_not be_nil
29
+
30
+ expect(b.content_model).to eq :empty
31
+
32
+ target = b.attributes['target']
33
+ expect(target).to_not be_nil
34
+ expect(target).to match /\.png$/
35
+ expect(File.exists?(target)).to be true
36
+
37
+ expect(b.attributes['width']).to_not be_nil
38
+ expect(b.attributes['height']).to_not be_nil
39
+ end
40
+
41
+ it "should generate SVG images when format is set to 'svg'" do
42
+ File.write('mermaid.txt', code)
43
+
44
+ doc = <<-eos
45
+ = Hello, Mermaid!
46
+ Doc Writer <doc@example.com>
47
+
48
+ == First Section
49
+
50
+ mermaid::mermaid.txt[format="svg"]
51
+ eos
52
+
53
+ d = Asciidoctor.load StringIO.new(doc)
54
+ expect(d).to_not be_nil
55
+
56
+ b = d.find { |b| b.context == :image }
57
+ expect(b).to_not be_nil
58
+
59
+ expect(b.content_model).to eq :empty
60
+
61
+ target = b.attributes['target']
62
+ expect(target).to_not be_nil
63
+ expect(target).to match /\.svg/
64
+ expect(File.exists?(target)).to be true
65
+
66
+ expect(b.attributes['width']).to_not be_nil
67
+ expect(b.attributes['height']).to_not be_nil
68
+ end
69
+ end
70
+
71
+ describe Asciidoctor::Diagram::MermaidBlockProcessor do
72
+ it "should generate PNG images when format is set to 'png'" do
73
+ doc = <<-eos
74
+ = Hello, Mermaid!
75
+ Doc Writer <doc@example.com>
76
+
77
+ == First Section
78
+
79
+ [mermaid, format="png"]
80
+ ----
81
+ #{code}
82
+ ----
83
+ eos
84
+
85
+ d = Asciidoctor.load StringIO.new(doc)
86
+ expect(d).to_not be_nil
87
+
88
+ b = d.find { |b| b.context == :image }
89
+ expect(b).to_not be_nil
90
+
91
+ expect(b.content_model).to eq :empty
92
+
93
+ target = b.attributes['target']
94
+ expect(target).to_not be_nil
95
+ expect(target).to match /\.png$/
96
+ expect(File.exists?(target)).to be true
97
+
98
+ expect(b.attributes['width']).to_not be_nil
99
+ expect(b.attributes['height']).to_not be_nil
100
+ end
101
+
102
+ it "should generate SVG images when format is set to 'svg'" do
103
+ doc = <<-eos
104
+ = Hello, Mermaid!
105
+ Doc Writer <doc@example.com>
106
+
107
+ == First Section
108
+
109
+ [mermaid, format="svg"]
110
+ ----
111
+ #{code}
112
+ ----
113
+ eos
114
+
115
+ d = Asciidoctor.load StringIO.new(doc)
116
+ expect(d).to_not be_nil
117
+
118
+ b = d.find { |b| b.context == :image }
119
+ expect(b).to_not be_nil
120
+
121
+ expect(b.content_model).to eq :empty
122
+
123
+ target = b.attributes['target']
124
+ expect(target).to_not be_nil
125
+ expect(target).to match /\.svg/
126
+ expect(File.exists?(target)).to be true
127
+
128
+ expect(b.attributes['width']).to_not be_nil
129
+ expect(b.attributes['height']).to_not be_nil
130
+ end
131
+
132
+ it "should raise an error when when format is set to an invalid value" do
133
+ doc = <<-eos
134
+ = Hello, Mermaid!
135
+ Doc Writer <doc@example.com>
136
+
137
+ == First Section
138
+
139
+ [mermaid, format="foobar"]
140
+ ----
141
+ ----
142
+ eos
143
+
144
+ expect { Asciidoctor.load StringIO.new(doc) }.to raise_error /support.*format/i
145
+ end
146
+
147
+ it "should not regenerate images when source has not changed" do
148
+ File.write('mermaid.txt', code)
149
+
150
+ doc = <<-eos
151
+ = Hello, Mermaid!
152
+ Doc Writer <doc@example.com>
153
+
154
+ == First Section
155
+
156
+ mermaid::mermaid.txt
157
+
158
+ [mermaid, format="png"]
159
+ ----
160
+ #{code}
161
+ ----
162
+ eos
163
+
164
+ d = Asciidoctor.load StringIO.new(doc)
165
+ b = d.find { |b| b.context == :image }
166
+ expect(b).to_not be_nil
167
+ target = b.attributes['target']
168
+ mtime1 = File.mtime(target)
169
+
170
+ sleep 1
171
+
172
+ d = Asciidoctor.load StringIO.new(doc)
173
+
174
+ mtime2 = File.mtime(target)
175
+
176
+ expect(mtime2).to eq mtime1
177
+ end
178
+
179
+ it "should handle two block macros with the same source" do
180
+ File.write('mermaid.txt', code)
181
+
182
+ doc = <<-eos
183
+ = Hello, Mermaid!
184
+ Doc Writer <doc@example.com>
185
+
186
+ == First Section
187
+
188
+ mermaid::mermaid.txt[]
189
+ mermaid::mermaid.txt[]
190
+ eos
191
+
192
+ Asciidoctor.load StringIO.new(doc)
193
+ expect(File.exists?('mermaid.png')).to be true
194
+ end
195
+
196
+ it "should respect target attribute in block macros" do
197
+ File.write('mermaid.txt', code)
198
+
199
+ doc = <<-eos
200
+ = Hello, Mermaid!
201
+ Doc Writer <doc@example.com>
202
+
203
+ == First Section
204
+
205
+ mermaid::mermaid.txt["foobar"]
206
+ mermaid::mermaid.txt["foobaz"]
207
+ eos
208
+
209
+ Asciidoctor.load StringIO.new(doc)
210
+ expect(File.exists?('foobar.png')).to be true
211
+ expect(File.exists?('foobaz.png')).to be true
212
+ expect(File.exists?('mermaid.png')).to be false
213
+ end
214
+
215
+ it "should respect the sequenceConfig attribute" do
216
+ seq_diag = <<-eos
217
+ sequenceDiagram
218
+ Alice->>John: Hello John, how are you?
219
+ John-->>Alice: Great!
220
+ eos
221
+
222
+ seq_config = <<-eos
223
+ {
224
+ "diagramMarginX": 0,
225
+ "diagramMarginY": 0,
226
+ "actorMargin": 0,
227
+ "boxMargin": 0,
228
+ "boxTextMargin": 0,
229
+ "noteMargin": 0,
230
+ "messageMargin": 0
231
+ }
232
+ eos
233
+ File.write('seqconfig.txt', seq_config)
234
+
235
+ File.write('mermaid.txt', seq_diag)
236
+
237
+ doc = <<-eos
238
+ = Hello, Mermaid!
239
+ Doc Writer <doc@example.com>
240
+
241
+ == First Section
242
+
243
+ mermaid::mermaid.txt["with_config", sequenceConfig="seqconfig.txt"]
244
+ mermaid::mermaid.txt["without_config"]
245
+ eos
246
+
247
+ Asciidoctor.load StringIO.new(doc)
248
+ expect(File.exists?('with_config.png')).to be true
249
+ expect(File.exists?('without_config.png')).to be true
250
+ expect(File.size('with_config.png')).to_not be File.size('without_config.png')
251
+ end
252
+
253
+ it "should respect the width attribute" do
254
+ seq_diag = <<-eos
255
+ sequenceDiagram
256
+ Alice->>John: Hello John, how are you?
257
+ John-->>Alice: Great!
258
+ eos
259
+
260
+ File.write('mermaid.txt', seq_diag)
261
+
262
+ doc = <<-eos
263
+ = Hello, Mermaid!
264
+ Doc Writer <doc@example.com>
265
+
266
+ == First Section
267
+
268
+ mermaid::mermaid.txt["with_width", width="700"]
269
+ mermaid::mermaid.txt["without_width"]
270
+ eos
271
+
272
+ Asciidoctor.load StringIO.new(doc)
273
+ expect(File.exists?('with_width.png')).to be true
274
+ expect(File.exists?('without_width.png')).to be true
275
+ expect(File.size('with_width.png')).to_not be File.size('without_width.png')
276
+ end
277
+ end