asciidoctor-diagram 1.0.1 → 1.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +22 -0
- data/README.adoc +7 -6
- data/examples/Gemfile +3 -0
- data/examples/README.adoc +6 -0
- data/examples/build_example.rb +9 -0
- data/examples/design.adoc +78 -0
- data/examples/features.adoc +164 -0
- data/lib/asciidoctor-diagram.rb +1 -0
- data/lib/asciidoctor-diagram/ditaa.rb +1 -0
- data/lib/asciidoctor-diagram/ditaa/extension.rb +23 -50
- data/lib/asciidoctor-diagram/ditaa/generator.rb +31 -0
- data/lib/asciidoctor-diagram/graphviz.rb +8 -0
- data/lib/asciidoctor-diagram/graphviz/extension.rb +41 -0
- data/lib/asciidoctor-diagram/plantuml.rb +1 -0
- data/lib/asciidoctor-diagram/plantuml/extension.rb +33 -71
- data/lib/asciidoctor-diagram/plantuml/generator.rb +50 -0
- data/lib/asciidoctor-diagram/{binaryio.rb → util/binaryio.rb} +0 -0
- data/lib/asciidoctor-diagram/util/diagram.rb +198 -0
- data/lib/asciidoctor-diagram/{java.rb → util/java.rb} +0 -0
- data/lib/asciidoctor-diagram/{java_jruby.rb → util/java_jruby.rb} +0 -0
- data/lib/asciidoctor-diagram/{java_rjb.rb → util/java_rjb.rb} +0 -0
- data/lib/asciidoctor-diagram/{png.rb → util/png.rb} +0 -0
- data/lib/asciidoctor-diagram/util/svg.rb +46 -0
- data/lib/asciidoctor-diagram/version.rb +1 -1
- data/spec/ditaa_spec.rb +65 -0
- data/spec/graphviz_spec.rb +134 -0
- data/spec/plantuml_spec.rb +38 -0
- data/spec/test_helper.rb +1 -0
- metadata +34 -28
- data/.gitignore +0 -19
- data/.travis.yml +0 -10
- data/Gemfile +0 -4
- data/asciidoctor-diagram.gemspec +0 -30
- data/ditaamini-license.txt +0 -165
- data/lib/asciidoctor-diagram/diagram.rb +0 -111
- data/lib/asciidoctor-diagram/svg.rb +0 -18
- data/plantuml-license.txt +0 -202
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'asciidoctor/extensions'
|
2
|
+
require_relative 'version'
|
3
|
+
|
4
|
+
Asciidoctor::Extensions.register do
|
5
|
+
require_relative 'graphviz/extension'
|
6
|
+
block :graphviz, Asciidoctor::Diagram::GraphvizBlock
|
7
|
+
block_macro :graphviz, Asciidoctor::Diagram::GraphvizBlockMacro
|
8
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../util/diagram'
|
2
|
+
require_relative '../plantuml/generator'
|
3
|
+
|
4
|
+
module Asciidoctor
|
5
|
+
module Diagram
|
6
|
+
module GraphvizBase
|
7
|
+
include PlantUmlGenerator
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def register_formats
|
12
|
+
register_format(:png, :image) do |c, p|
|
13
|
+
plantuml(p, c, 'dot')
|
14
|
+
end
|
15
|
+
register_format(:svg, :image) do |c, p|
|
16
|
+
plantuml(p, c, 'dot', '-tsvg')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class GraphvizBlock < Asciidoctor::Extensions::BlockProcessor
|
22
|
+
include DiagramProcessorBase
|
23
|
+
include GraphvizBase
|
24
|
+
|
25
|
+
def initialize(context, document, opts = {})
|
26
|
+
super
|
27
|
+
register_formats()
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class GraphvizBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor
|
32
|
+
include DiagramProcessorBase
|
33
|
+
include GraphvizBase
|
34
|
+
|
35
|
+
def initialize(context, document, opts = {})
|
36
|
+
super
|
37
|
+
register_formats()
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,87 +1,49 @@
|
|
1
|
-
require_relative '../diagram'
|
2
|
-
require_relative '
|
3
|
-
require_relative '../png'
|
4
|
-
require_relative '../svg'
|
1
|
+
require_relative '../util/diagram'
|
2
|
+
require_relative 'generator'
|
5
3
|
|
6
4
|
module Asciidoctor
|
7
5
|
module Diagram
|
8
|
-
|
6
|
+
module PlantUmlBase
|
7
|
+
include PlantUmlGenerator
|
9
8
|
|
10
|
-
|
11
|
-
option :contexts, [:listing, :literal, :open]
|
12
|
-
option :content_model, :simple
|
13
|
-
option :pos_attrs, ['target', 'format']
|
14
|
-
option :default_attrs, {'format' => 'png'}
|
15
|
-
|
16
|
-
def name
|
17
|
-
"PlantUml"
|
18
|
-
end
|
19
|
-
|
20
|
-
def allowed_formats
|
21
|
-
@allowed_formats ||= [:svg, :png, :txt]
|
22
|
-
end
|
9
|
+
private
|
23
10
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
11
|
+
def register_formats(document)
|
12
|
+
config_args = []
|
13
|
+
config = document.attributes['plantumlconfig']
|
14
|
+
if config
|
15
|
+
config_args += ['-config', File.expand_path(config, document.attributes['docdir'])]
|
28
16
|
end
|
29
17
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
18
|
+
register_format(:png, :image) do |c, p|
|
19
|
+
plantuml(p, c, 'uml', *config_args)
|
20
|
+
end
|
21
|
+
register_format(:svg, :image) do |c, p|
|
22
|
+
plantuml(p, c, 'uml', '-tsvg', *config_args)
|
23
|
+
end
|
24
|
+
register_format(:txt, :literal) do |c, p|
|
25
|
+
plantuml(p, c, 'uml', '-tutxt', *config_args)
|
26
|
+
end
|
38
27
|
end
|
28
|
+
end
|
39
29
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
def plantuml(code, *flags)
|
45
|
-
Java.load
|
46
|
-
|
47
|
-
code = "@startuml\n#{code}\n@enduml" unless code.index '@startuml'
|
48
|
-
|
49
|
-
# When the -pipe command line flag is used, PlantUML calls System.exit which kills our process. In order
|
50
|
-
# to avoid this we call some lower level components of PlantUML directly.
|
51
|
-
# This snippet of code corresponds approximately with net.sourceforge.plantuml.Run#managePipe
|
52
|
-
cmd = ['-charset', 'UTF-8', '-failonerror']
|
53
|
-
cmd += flags
|
54
|
-
|
55
|
-
option = Java.net.sourceforge.plantuml.Option.new(Java.array_to_java_array(cmd, :string))
|
56
|
-
source_reader = Java.net.sourceforge.plantuml.SourceStringReader.new(
|
57
|
-
Java.net.sourceforge.plantuml.preproc.Defines.new(),
|
58
|
-
code,
|
59
|
-
option.getConfig()
|
60
|
-
)
|
30
|
+
class PlantUmlBlock < Asciidoctor::Extensions::BlockProcessor
|
31
|
+
include DiagramProcessorBase
|
32
|
+
include PlantUmlBase
|
61
33
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
ps.close
|
66
|
-
Java.string_from_java_bytes(bos.toByteArray)
|
34
|
+
def initialize(context, document, opts = {})
|
35
|
+
super
|
36
|
+
register_formats(document)
|
67
37
|
end
|
38
|
+
end
|
68
39
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
document = parent.document
|
73
|
-
config = document.attributes['plantumlconfig']
|
74
|
-
if config
|
75
|
-
flags += ['-config', File.expand_path(config, document.attributes['docdir'])]
|
76
|
-
end
|
77
|
-
|
78
|
-
flags
|
79
|
-
end
|
40
|
+
class PlantUmlBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor
|
41
|
+
include DiagramProcessorBase
|
42
|
+
include PlantUmlBase
|
80
43
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
md5.hexdigest
|
44
|
+
def initialize(context, document, opts = {})
|
45
|
+
super
|
46
|
+
register_formats(document)
|
85
47
|
end
|
86
48
|
end
|
87
49
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative '../util/java'
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
module Diagram
|
5
|
+
module PlantUmlGenerator
|
6
|
+
private
|
7
|
+
|
8
|
+
PLANTUML_JAR_PATH = File.expand_path File.join('../..', 'plantuml.jar'), File.dirname(__FILE__)
|
9
|
+
Java.classpath << PLANTUML_JAR_PATH
|
10
|
+
|
11
|
+
def plantuml(parent, code, tag, *flags)
|
12
|
+
unless @graphvizdot
|
13
|
+
@graphvizdot = parent.document.attributes['graphvizdot']
|
14
|
+
@graphvizdot = which('dot') unless @graphvizdot && File.executable?(@graphvizdot)
|
15
|
+
raise "Could not find the Graphviz 'dot' executable in PATH; add it to the PATH or specify its location using the 'graphvizdot' document attribute" unless @graphvizdot
|
16
|
+
end
|
17
|
+
|
18
|
+
Java.load
|
19
|
+
|
20
|
+
code = "@start#{tag}\n#{code}\n@end#{tag}" unless code.index "@start#{tag}"
|
21
|
+
|
22
|
+
flags += ['-charset', 'UTF-8', '-failonerror', '-graphvizdot', @graphvizdot]
|
23
|
+
|
24
|
+
option = Java.net.sourceforge.plantuml.Option.new(Java.array_to_java_array(flags, :string))
|
25
|
+
source_reader = Java.net.sourceforge.plantuml.SourceStringReader.new(
|
26
|
+
Java.net.sourceforge.plantuml.preproc.Defines.new(),
|
27
|
+
code,
|
28
|
+
option.getConfig()
|
29
|
+
)
|
30
|
+
|
31
|
+
bos = Java.java.io.ByteArrayOutputStream.new
|
32
|
+
ps = Java.java.io.PrintStream.new(bos)
|
33
|
+
source_reader.generateImage(ps, 0, option.getFileFormatOption())
|
34
|
+
ps.close
|
35
|
+
Java.string_from_java_bytes(bos.toByteArray)
|
36
|
+
end
|
37
|
+
|
38
|
+
def which(cmd)
|
39
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
40
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
41
|
+
exts.each { |ext|
|
42
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
43
|
+
return exe if File.executable? exe
|
44
|
+
}
|
45
|
+
end
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
File without changes
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'asciidoctor/extensions'
|
2
|
+
require 'digest'
|
3
|
+
require 'json'
|
4
|
+
require_relative 'java'
|
5
|
+
require_relative 'png'
|
6
|
+
require_relative 'svg'
|
7
|
+
|
8
|
+
module Asciidoctor
|
9
|
+
module Diagram
|
10
|
+
module DiagramProcessorBase
|
11
|
+
IMAGE_PARAMS = {
|
12
|
+
:svg => {
|
13
|
+
:encoding => Encoding::UTF_8,
|
14
|
+
:decoder => SVG
|
15
|
+
},
|
16
|
+
:png => {
|
17
|
+
:encoding => Encoding::ASCII_8BIT,
|
18
|
+
:decoder => PNG
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.option :pos_attrs, ['target', 'format']
|
24
|
+
|
25
|
+
if base.ancestors.include?(Asciidoctor::Extensions::BlockProcessor)
|
26
|
+
base.option :contexts, [:listing, :literal, :open]
|
27
|
+
base.option :content_model, :simple
|
28
|
+
|
29
|
+
base.instance_eval do
|
30
|
+
alias_method :process, :process_block
|
31
|
+
end
|
32
|
+
else
|
33
|
+
base.instance_eval do
|
34
|
+
alias_method :process, :process_macro
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
def process_macro(parent, target, attributes)
|
41
|
+
source = FileSource.new(File.expand_path(target, parent.document.attributes['docdir']))
|
42
|
+
attributes['target'] = File.basename(target, File.extname(target))
|
43
|
+
|
44
|
+
generate_block(parent, source, attributes)
|
45
|
+
end
|
46
|
+
|
47
|
+
def process_block(parent, reader, attributes)
|
48
|
+
generate_block(parent, ReaderSource.new(reader), attributes)
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate_block(parent, source, attributes)
|
52
|
+
format = attributes.delete('format') || @default_format
|
53
|
+
format = format.to_sym if format.respond_to?(:to_sym)
|
54
|
+
|
55
|
+
raise "Format undefined" unless format
|
56
|
+
|
57
|
+
generator_info = formats[format]
|
58
|
+
|
59
|
+
raise "#{self.class.name} does not support output format #{format}" unless generator_info
|
60
|
+
|
61
|
+
case generator_info[:type]
|
62
|
+
when :image
|
63
|
+
create_image_block(parent, source, attributes, format, generator_info)
|
64
|
+
when :literal
|
65
|
+
create_literal_block(parent, source, attributes, generator_info)
|
66
|
+
else
|
67
|
+
raise "Unsupported output format: #{format}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
#
|
74
|
+
# Registers a supported format. The first registered format becomes the default format for the block processor.
|
75
|
+
#
|
76
|
+
# +format+ is a symbol with the format name
|
77
|
+
# +type+ is a symbol and should be either :image or :literal
|
78
|
+
# +block+ is a block that produces the diagrams from code. The block receives the parent asciidoc block and the diagram code as arguments
|
79
|
+
#
|
80
|
+
def register_format(format, type, &block)
|
81
|
+
unless @default_format
|
82
|
+
@default_format = format
|
83
|
+
end
|
84
|
+
|
85
|
+
formats[format] = {
|
86
|
+
:type => type,
|
87
|
+
:generator => block
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def formats
|
92
|
+
@formats ||= {}
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_image_block(parent, source, attributes, format, generator_info)
|
96
|
+
target = attributes.delete('target')
|
97
|
+
|
98
|
+
image_name = "#{target || source.checksum}.#{format}"
|
99
|
+
image_dir = File.expand_path(parent.document.attributes['imagesdir'] || '', parent.document.attributes['docdir'])
|
100
|
+
image_file = File.expand_path(image_name, image_dir)
|
101
|
+
|
102
|
+
if source.newer_than?(image_file)
|
103
|
+
cache_file = File.expand_path("#{image_name}.cache", image_dir)
|
104
|
+
|
105
|
+
if File.exists? cache_file
|
106
|
+
metadata = File.open(cache_file, 'r') { |f| JSON.load f }
|
107
|
+
else
|
108
|
+
metadata = nil
|
109
|
+
end
|
110
|
+
|
111
|
+
unless File.exists?(image_file) && metadata && metadata['checksum'] == source.checksum
|
112
|
+
params = IMAGE_PARAMS[format]
|
113
|
+
|
114
|
+
result = generator_info[:generator].call(source.code, parent)
|
115
|
+
|
116
|
+
result.force_encoding(params[:encoding])
|
117
|
+
|
118
|
+
metadata = {'checksum' => source.checksum}
|
119
|
+
metadata['width'], metadata['height'] = params[:decoder].get_image_size(result)
|
120
|
+
|
121
|
+
File.open(image_file, 'w') { |f| f.write result }
|
122
|
+
File.open(cache_file, 'w') { |f| JSON.dump(metadata, f) }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
attributes['target'] = image_name
|
127
|
+
attributes['width'] ||= metadata['width'] if metadata['width']
|
128
|
+
attributes['height'] ||= metadata['height'] if metadata['height']
|
129
|
+
attributes['alt'] ||= if (title_text = attributes['title'])
|
130
|
+
title_text
|
131
|
+
elsif target
|
132
|
+
(File.basename target, (File.extname target) || '').tr '_-', ' '
|
133
|
+
else
|
134
|
+
'Diagram'
|
135
|
+
end
|
136
|
+
|
137
|
+
Asciidoctor::Block.new parent, :image, :content_model => :empty, :attributes => attributes
|
138
|
+
end
|
139
|
+
|
140
|
+
def create_literal_block(parent, source, attributes, generator_info)
|
141
|
+
attributes.delete('target')
|
142
|
+
|
143
|
+
result = generator_info[:generator].call(source.code, parent)
|
144
|
+
|
145
|
+
result.force_encoding(Encoding::UTF_8)
|
146
|
+
Asciidoctor::Block.new parent, :literal, :code => result, :attributes => attributes
|
147
|
+
end
|
148
|
+
|
149
|
+
def code_checksum(code)
|
150
|
+
md5 = Digest::MD5.new
|
151
|
+
md5 << code
|
152
|
+
md5.hexdigest
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Source
|
157
|
+
def newer_than?(image)
|
158
|
+
true
|
159
|
+
end
|
160
|
+
|
161
|
+
def checksum
|
162
|
+
@checksum ||= compute_checksum(code)
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def compute_checksum(code)
|
168
|
+
md5 = Digest::MD5.new
|
169
|
+
md5 << code
|
170
|
+
md5.hexdigest
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class ReaderSource < Source
|
175
|
+
def initialize(reader)
|
176
|
+
@reader = reader
|
177
|
+
end
|
178
|
+
|
179
|
+
def code
|
180
|
+
@code ||= @reader.lines.join
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
class FileSource < Source
|
185
|
+
def initialize(file_name)
|
186
|
+
@file_name = file_name
|
187
|
+
end
|
188
|
+
|
189
|
+
def code
|
190
|
+
@code ||= File.read(@file_name)
|
191
|
+
end
|
192
|
+
|
193
|
+
def newer_than?(image)
|
194
|
+
!File.exists?(image) || File.mtime(@file_name) > File.mtime(image)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative 'binaryio'
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
module Diagram
|
5
|
+
module SVG
|
6
|
+
def self.get_image_size(data)
|
7
|
+
get_from_style(data) || get_from_viewport(data)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
SVG_STYLE_REGEX = /style\s*=\s*"width:(?<width>\d+)px;height:(?<height>\d+)px/
|
13
|
+
|
14
|
+
def self.get_from_style(data)
|
15
|
+
match_data = SVG_STYLE_REGEX.match(data)
|
16
|
+
if match_data
|
17
|
+
[match_data[:width].to_i, match_data[:height].to_i]
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
SVG_VIEWPORT_REGEX = /<svg width="(?<width>\d+)(?<width_unit>[a-zA-Z]+)" height="(?<height>\d+)(?<height_unit>[a-zA-Z]+)"/
|
24
|
+
|
25
|
+
def self.get_from_viewport(data)
|
26
|
+
match_data = SVG_VIEWPORT_REGEX.match(data)
|
27
|
+
if match_data
|
28
|
+
width = match_data[:width].to_i * to_px_factor(match_data[:width_unit])
|
29
|
+
height = match_data[:height].to_i * to_px_factor(match_data[:height_unit])
|
30
|
+
[width.to_i, height.to_i]
|
31
|
+
else
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.to_px_factor(unit)
|
37
|
+
case unit
|
38
|
+
when 'pt'
|
39
|
+
1.33
|
40
|
+
else
|
41
|
+
1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|