asciidoctor-diagram 1.3.2 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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