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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +22 -0
  3. data/README.adoc +7 -6
  4. data/examples/Gemfile +3 -0
  5. data/examples/README.adoc +6 -0
  6. data/examples/build_example.rb +9 -0
  7. data/examples/design.adoc +78 -0
  8. data/examples/features.adoc +164 -0
  9. data/lib/asciidoctor-diagram.rb +1 -0
  10. data/lib/asciidoctor-diagram/ditaa.rb +1 -0
  11. data/lib/asciidoctor-diagram/ditaa/extension.rb +23 -50
  12. data/lib/asciidoctor-diagram/ditaa/generator.rb +31 -0
  13. data/lib/asciidoctor-diagram/graphviz.rb +8 -0
  14. data/lib/asciidoctor-diagram/graphviz/extension.rb +41 -0
  15. data/lib/asciidoctor-diagram/plantuml.rb +1 -0
  16. data/lib/asciidoctor-diagram/plantuml/extension.rb +33 -71
  17. data/lib/asciidoctor-diagram/plantuml/generator.rb +50 -0
  18. data/lib/asciidoctor-diagram/{binaryio.rb → util/binaryio.rb} +0 -0
  19. data/lib/asciidoctor-diagram/util/diagram.rb +198 -0
  20. data/lib/asciidoctor-diagram/{java.rb → util/java.rb} +0 -0
  21. data/lib/asciidoctor-diagram/{java_jruby.rb → util/java_jruby.rb} +0 -0
  22. data/lib/asciidoctor-diagram/{java_rjb.rb → util/java_rjb.rb} +0 -0
  23. data/lib/asciidoctor-diagram/{png.rb → util/png.rb} +0 -0
  24. data/lib/asciidoctor-diagram/util/svg.rb +46 -0
  25. data/lib/asciidoctor-diagram/version.rb +1 -1
  26. data/spec/ditaa_spec.rb +65 -0
  27. data/spec/graphviz_spec.rb +134 -0
  28. data/spec/plantuml_spec.rb +38 -0
  29. data/spec/test_helper.rb +1 -0
  30. metadata +34 -28
  31. data/.gitignore +0 -19
  32. data/.travis.yml +0 -10
  33. data/Gemfile +0 -4
  34. data/asciidoctor-diagram.gemspec +0 -30
  35. data/ditaamini-license.txt +0 -165
  36. data/lib/asciidoctor-diagram/diagram.rb +0 -111
  37. data/lib/asciidoctor-diagram/svg.rb +0 -18
  38. 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
@@ -4,4 +4,5 @@ require_relative 'version'
4
4
  Asciidoctor::Extensions.register do
5
5
  require_relative 'plantuml/extension'
6
6
  block :plantuml, Asciidoctor::Diagram::PlantUmlBlock
7
+ block_macro :plantuml, Asciidoctor::Diagram::PlantUmlBlockMacro
7
8
  end
@@ -1,87 +1,49 @@
1
- require_relative '../diagram'
2
- require_relative '../java'
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
- PLANTUML_JAR_PATH = File.expand_path File.join('../..', 'plantuml.jar'), File.dirname(__FILE__)
6
+ module PlantUmlBase
7
+ include PlantUmlGenerator
9
8
 
10
- class PlantUmlBlock < DiagramBlock
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 generate_image(parent, diagram_code, format)
25
- flags = get_default_flags(parent)
26
- if format == :svg
27
- flags << "-tsvg"
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
- plantuml(diagram_code, *flags)
31
- end
32
-
33
- def generate_text(parent, diagram_code)
34
- flags = get_default_flags(parent)
35
- flags << '-tutxt'
36
-
37
- plantuml(diagram_code, *flags)
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
- private
41
-
42
- Java.classpath << PLANTUML_JAR_PATH
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
- bos = Java.java.io.ByteArrayOutputStream.new
63
- ps = Java.java.io.PrintStream.new(bos)
64
- source_reader.generateImage(ps, 0, option.getFileFormatOption())
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
- def get_default_flags(parent)
70
- flags = []
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 code_checksum(code)
82
- md5 = Digest::MD5.new
83
- md5 << code
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
@@ -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
@@ -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