asciidoctor-diagram 1.5.18 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of asciidoctor-diagram might be problematic. Click here for more details.

Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +66 -0
  3. data/README.adoc +98 -23
  4. data/examples/features.adoc +2 -2
  5. data/lib/asciidoctor-diagram.rb +8 -0
  6. data/lib/asciidoctor-diagram/a2s/converter.rb +55 -0
  7. data/lib/asciidoctor-diagram/a2s/extension.rb +6 -52
  8. data/lib/asciidoctor-diagram/blockdiag/converter.rb +37 -0
  9. data/lib/asciidoctor-diagram/blockdiag/extension.rb +9 -116
  10. data/lib/asciidoctor-diagram/bpmn.rb +7 -0
  11. data/lib/asciidoctor-diagram/bpmn/converter.rb +62 -0
  12. data/lib/asciidoctor-diagram/bpmn/extension.rb +14 -0
  13. data/lib/asciidoctor-diagram/bytefield.rb +7 -0
  14. data/lib/asciidoctor-diagram/bytefield/converter.rb +26 -0
  15. data/lib/asciidoctor-diagram/bytefield/extension.rb +14 -0
  16. data/lib/asciidoctor-diagram/diagram_converter.rb +19 -0
  17. data/lib/asciidoctor-diagram/diagram_processor.rb +345 -0
  18. data/lib/asciidoctor-diagram/diagram_source.rb +306 -0
  19. data/lib/asciidoctor-diagram/ditaa/converter.rb +86 -0
  20. data/lib/asciidoctor-diagram/ditaa/extension.rb +6 -71
  21. data/lib/asciidoctor-diagram/dpic.rb +7 -0
  22. data/lib/asciidoctor-diagram/dpic/converter.rb +30 -0
  23. data/lib/asciidoctor-diagram/dpic/extension.rb +14 -0
  24. data/lib/asciidoctor-diagram/erd/converter.rb +31 -0
  25. data/lib/asciidoctor-diagram/erd/extension.rb +6 -35
  26. data/lib/asciidoctor-diagram/gnuplot.rb +7 -0
  27. data/lib/asciidoctor-diagram/gnuplot/converter.rb +63 -0
  28. data/lib/asciidoctor-diagram/gnuplot/extension.rb +14 -0
  29. data/lib/asciidoctor-diagram/graphviz/converter.rb +32 -0
  30. data/lib/asciidoctor-diagram/graphviz/extension.rb +6 -35
  31. data/lib/asciidoctor-diagram/http/converter.rb +99 -0
  32. data/lib/asciidoctor-diagram/http/server.rb +127 -0
  33. data/lib/asciidoctor-diagram/lilypond.rb +7 -0
  34. data/lib/asciidoctor-diagram/lilypond/converter.rb +54 -0
  35. data/lib/asciidoctor-diagram/lilypond/extension.rb +14 -0
  36. data/lib/asciidoctor-diagram/meme/converter.rb +122 -0
  37. data/lib/asciidoctor-diagram/meme/extension.rb +5 -107
  38. data/lib/asciidoctor-diagram/mermaid/converter.rb +179 -0
  39. data/lib/asciidoctor-diagram/mermaid/extension.rb +6 -159
  40. data/lib/asciidoctor-diagram/msc/converter.rb +35 -0
  41. data/lib/asciidoctor-diagram/msc/extension.rb +6 -36
  42. data/lib/asciidoctor-diagram/nomnoml/converter.rb +25 -0
  43. data/lib/asciidoctor-diagram/nomnoml/extension.rb +6 -28
  44. data/lib/asciidoctor-diagram/pikchr.rb +7 -0
  45. data/lib/asciidoctor-diagram/pikchr/converter.rb +26 -0
  46. data/lib/asciidoctor-diagram/pikchr/extension.rb +14 -0
  47. data/lib/asciidoctor-diagram/plantuml/converter.rb +117 -0
  48. data/lib/asciidoctor-diagram/plantuml/extension.rb +10 -119
  49. data/lib/asciidoctor-diagram/shaape/converter.rb +25 -0
  50. data/lib/asciidoctor-diagram/shaape/extension.rb +6 -28
  51. data/lib/asciidoctor-diagram/smcat.rb +7 -0
  52. data/lib/asciidoctor-diagram/smcat/converter.rb +44 -0
  53. data/lib/asciidoctor-diagram/smcat/extension.rb +14 -0
  54. data/lib/asciidoctor-diagram/svgbob/converter.rb +49 -0
  55. data/lib/asciidoctor-diagram/svgbob/extension.rb +6 -28
  56. data/lib/asciidoctor-diagram/symbolator.rb +7 -0
  57. data/lib/asciidoctor-diagram/symbolator/converter.rb +23 -0
  58. data/lib/asciidoctor-diagram/symbolator/extension.rb +14 -0
  59. data/lib/asciidoctor-diagram/syntrax/converter.rb +55 -0
  60. data/lib/asciidoctor-diagram/syntrax/extension.rb +6 -51
  61. data/lib/asciidoctor-diagram/tikz/converter.rb +56 -0
  62. data/lib/asciidoctor-diagram/tikz/extension.rb +6 -60
  63. data/lib/asciidoctor-diagram/umlet/converter.rb +24 -0
  64. data/lib/asciidoctor-diagram/umlet/extension.rb +6 -28
  65. data/lib/asciidoctor-diagram/util/cli.rb +14 -3
  66. data/lib/asciidoctor-diagram/util/cli_generator.rb +19 -1
  67. data/lib/asciidoctor-diagram/util/gif.rb +2 -2
  68. data/lib/asciidoctor-diagram/util/java.rb +1 -1
  69. data/lib/asciidoctor-diagram/util/java_socket.rb +7 -9
  70. data/lib/asciidoctor-diagram/util/pdf.rb +2 -2
  71. data/lib/asciidoctor-diagram/util/png.rb +2 -2
  72. data/lib/asciidoctor-diagram/util/svg.rb +38 -19
  73. data/lib/asciidoctor-diagram/util/which.rb +0 -29
  74. data/lib/asciidoctor-diagram/vega/converter.rb +47 -0
  75. data/lib/asciidoctor-diagram/vega/extension.rb +6 -44
  76. data/lib/asciidoctor-diagram/version.rb +1 -1
  77. data/lib/asciidoctor-diagram/wavedrom/converter.rb +50 -0
  78. data/lib/asciidoctor-diagram/wavedrom/extension.rb +6 -46
  79. data/lib/ditaa-1.3.15.jar +0 -0
  80. data/lib/ditaamini-0.12.jar +0 -0
  81. data/lib/plantuml-1.3.15.jar +0 -0
  82. data/lib/plantuml.jar +0 -0
  83. data/lib/server-1.3.15.jar +0 -0
  84. data/spec/bpmn-example.xml +44 -0
  85. data/spec/bpmn_spec.rb +96 -0
  86. data/spec/bytefield_spec.rb +230 -0
  87. data/spec/ditaa_spec.rb +32 -0
  88. data/spec/dpic_spec.rb +74 -0
  89. data/spec/gnuplot_spec.rb +478 -0
  90. data/spec/lilypond_spec.rb +151 -0
  91. data/spec/mermaid_spec.rb +33 -1
  92. data/spec/pikchr_spec.rb +106 -0
  93. data/spec/plantuml_spec.rb +90 -1
  94. data/spec/smcat_spec.rb +164 -0
  95. data/spec/symbolator_spec.rb +200 -0
  96. data/spec/test_helper.rb +0 -18
  97. metadata +73 -11
  98. data/lib/asciidoctor-diagram/extensions.rb +0 -568
  99. data/lib/ditaa-1.3.13.jar +0 -0
  100. data/lib/ditaamini-0.11.jar +0 -0
  101. data/lib/plantuml-1.3.13.jar +0 -0
  102. data/lib/server-1.3.13.jar +0 -0
@@ -0,0 +1,306 @@
1
+ require 'asciidoctor/logging'
2
+ require_relative 'util/which'
3
+
4
+ module Asciidoctor
5
+ module Diagram
6
+ # This module describes the duck-typed interface that diagram sources must implement. Implementations
7
+ # may include this module but it is not required.
8
+ module DiagramSource
9
+ include Asciidoctor::Logging
10
+
11
+ def image_name
12
+ raise NotImplementedError.new
13
+ end
14
+
15
+ # @return [String] the String representation of the source code for the diagram
16
+ # @abstract
17
+ def code
18
+ raise NotImplementedError.new
19
+ end
20
+
21
+ # Get the value for the specified attribute. First look in the attributes on
22
+ # this document and return the value of the attribute if found. Otherwise, if
23
+ # this document is a child of the Document document, look in the attributes of the
24
+ # Document document and return the value of the attribute if found. Otherwise,
25
+ # return the default value, which defaults to nil.
26
+ #
27
+ # @param name [String, Symbol] the name of the attribute to lookup
28
+ # @param default_value [Object] the value to return if the attribute is not found
29
+ # @inherit [Boolean, String] indicates whether to check for the attribute on the AsciiDoctor::Document if not found on this document.
30
+ # When a non-nil String is given the an attribute name "#{inherit}-#{name}" is looked for on the document.
31
+ #
32
+ # @return the value of the attribute or the default value if the attribute is not found in the attributes of this node or the document node
33
+ # @abstract
34
+ def attr(name, default_value = nil, inherit = nil)
35
+ raise NotImplementedError.new
36
+ end
37
+
38
+ # @return [String] the base directory against which relative paths in this diagram should be resolved
39
+ # @abstract
40
+ def base_dir
41
+ attr('docdir', nil, true) || Dir.pwd
42
+ end
43
+
44
+ # Alias for code
45
+ def to_s
46
+ code
47
+ end
48
+
49
+ # Determines if the diagram should be regenerated or not. The default implementation of this method simply
50
+ # returns true.
51
+ #
52
+ # @param image_file [String] the path to the previously generated version of the image
53
+ # @param image_metadata [Hash] the image metadata Hash that was stored during the previous diagram generation pass
54
+ # @return [Boolean] true if the diagram should be regenerated; false otherwise
55
+ def should_process?(image_file, image_metadata)
56
+ true
57
+ end
58
+
59
+ # Creates an image metadata Hash that will be stored to disk alongside the generated image file. The contents
60
+ # of this Hash are reread during subsequent document processing and then passed to the should_process? method
61
+ # where it can be used to determine if the diagram should be regenerated or not.
62
+ # The default implementation returns an empty Hash.
63
+ # @return [Hash] a Hash containing metadata
64
+ def create_image_metadata
65
+ {}
66
+ end
67
+
68
+ def config
69
+ raise NotImplementedError.new
70
+ end
71
+
72
+ def find_command(cmd, options = {})
73
+ attr_names = options[:attrs] || options.fetch(:alt_attrs, []) + [cmd]
74
+ cmd_names = [cmd] + options.fetch(:alt_cmds, [])
75
+
76
+ cmd_var = 'cmd-' + attr_names[0]
77
+
78
+ if config.key? cmd_var
79
+ cmd_path = config[cmd_var]
80
+ else
81
+ logger.debug "Finding '#{cmd}' in attributes"
82
+ cmd_path = attr_names.map { |attr_name|
83
+ attr = attr(attr_name, nil, true)
84
+ if logger.debug? && attr
85
+ logger.debug "Found value '#{attr}' in attribute '#{attr_name}'" if attr
86
+ end
87
+ attr
88
+ }
89
+ .reject { |attr| attr.nil? }
90
+ .map { |attr|
91
+ expanded = File.expand_path(attr)
92
+ if logger.debug? && attr != expanded
93
+ logger.debug "Expanded '#{attr}' to '#{expanded}'"
94
+ end
95
+ expanded
96
+ }
97
+ .select { |path|
98
+ executable = File.executable?(path)
99
+ if logger.debug?
100
+ logger.debug "Is '#{path}' executable? #{executable}"
101
+ end
102
+ executable
103
+ }
104
+ .first
105
+
106
+ unless cmd_path
107
+ logger.debug "Finding '#{cmd}' in environment"
108
+ cmd_path = cmd_names.map { |c|
109
+ path = ::Asciidoctor::Diagram::Which.which(c, :path => options[:path])
110
+ if logger.debug? && path
111
+ logger.debug "Found '#{path}' in environment"
112
+ end
113
+ path
114
+ }
115
+ .reject { |path| path.nil? }
116
+ .first
117
+ end
118
+
119
+ config[cmd_var] = cmd_path
120
+
121
+ if cmd_path.nil? && options.fetch(:raise_on_error, true)
122
+ raise "Could not find the #{cmd_names.map { |c| "'#{c}'" }.join(', ')} executable in PATH; add it to the PATH or specify its location using the '#{attr_names[0]}' document attribute"
123
+ end
124
+ end
125
+
126
+ cmd_path
127
+ end
128
+
129
+ def resolve_path target, start = base_dir
130
+ raise NotImplementedError.new
131
+ end
132
+ end
133
+
134
+ # Base class for diagram source implementations that uses an md5 checksum of the source code of a diagram to
135
+ # determine if it has been updated or not.
136
+ class BasicSource
137
+ include DiagramSource
138
+
139
+ attr_reader :attributes
140
+
141
+ def initialize(block_processor, parent_block, attributes)
142
+ @block_processor = block_processor
143
+ @parent_block = parent_block
144
+ @attributes = attributes
145
+ end
146
+
147
+ def resolve_path target, start = base_dir
148
+ @parent_block.normalize_system_path(target, start)
149
+ end
150
+
151
+ def config
152
+ @block_processor.config
153
+ end
154
+
155
+ def image_name
156
+ attr('target', 'diag-' + checksum)
157
+ end
158
+
159
+ def attr(name, default_value = nil, inherit = nil)
160
+ name = name.to_s if ::Symbol === name
161
+
162
+ value = @attributes[name]
163
+
164
+ if value.nil? && inherit
165
+ case inherit
166
+ when String, Symbol
167
+ value = @parent_block.attr("#{inherit.to_s}-#{name}", default_value, true)
168
+ else
169
+ value = @parent_block.attr(name, default_value, inherit)
170
+ end
171
+ end
172
+
173
+ value || default_value
174
+ end
175
+
176
+ def should_process?(image_file, image_metadata)
177
+ image_metadata[:checksum] != checksum
178
+ end
179
+
180
+ def create_image_metadata
181
+ {:checksum => checksum}
182
+ end
183
+
184
+ def checksum
185
+ @checksum ||= compute_checksum(code)
186
+ end
187
+
188
+ protected
189
+
190
+ def resolve_diagram_subs
191
+ if @attributes.key? 'subs'
192
+ @parent_block.resolve_block_subs @attributes['subs'], nil, 'diagram'
193
+ else
194
+ []
195
+ end
196
+ end
197
+
198
+ private
199
+
200
+ def compute_checksum(code)
201
+ md5 = Digest::MD5.new
202
+ md5 << code
203
+ @attributes.each do |k, v|
204
+ md5 << k.to_s if k
205
+ md5 << v.to_s if v
206
+ end
207
+ md5.hexdigest
208
+ end
209
+ end
210
+
211
+ # A diagram source that retrieves the code for the diagram from the contents of a block.
212
+ class ReaderSource < BasicSource
213
+ include DiagramSource
214
+
215
+ def initialize(block_processor, parent_block, reader, attributes)
216
+ super(block_processor, parent_block, attributes)
217
+ @reader = reader
218
+ end
219
+
220
+ def code
221
+ @code ||= @parent_block.apply_subs(@reader.lines, resolve_diagram_subs).join("\n")
222
+ end
223
+ end
224
+
225
+ # A diagram source that retrieves the code for a diagram from an external source file.
226
+ class FileSource < BasicSource
227
+ def initialize(block_processor, parent_block, file_name, attributes)
228
+ super(block_processor, parent_block, attributes)
229
+ @file_name = file_name
230
+ end
231
+
232
+ def base_dir
233
+ if @file_name
234
+ File.dirname(@file_name)
235
+ else
236
+ super
237
+ end
238
+ end
239
+
240
+ def image_name
241
+ if @attributes['target']
242
+ super
243
+ elsif @file_name
244
+ File.basename(@file_name, File.extname(@file_name))
245
+ else
246
+ checksum
247
+ end
248
+ end
249
+
250
+ def should_process?(image_file, image_metadata)
251
+ (@file_name && File.mtime(@file_name) > File.mtime(image_file)) || super
252
+ end
253
+
254
+ def code
255
+ @code ||= read_code
256
+ end
257
+
258
+ def read_code
259
+ if @file_name
260
+ lines = File.readlines(@file_name)
261
+ lines = prepare_source_array(lines)
262
+ @parent_block.apply_subs(lines, resolve_diagram_subs).join("\n")
263
+ else
264
+ ''
265
+ end
266
+ end
267
+
268
+ private
269
+
270
+ # Byte arrays for UTF-* Byte Order Marks
271
+ BOM_BYTES_UTF_8 = [0xef, 0xbb, 0xbf]
272
+ BOM_BYTES_UTF_16LE = [0xff, 0xfe]
273
+ BOM_BYTES_UTF_16BE = [0xfe, 0xff]
274
+
275
+ # Prepare the source data Array for parsing.
276
+ #
277
+ # Encodes the data to UTF-8, if necessary, and removes any trailing
278
+ # whitespace from every line.
279
+ #
280
+ # If a BOM is found at the beginning of the data, a best attempt is made to
281
+ # encode it to UTF-8 from the specified source encoding.
282
+ #
283
+ # data - the source data Array to prepare (no nil entries allowed)
284
+ #
285
+ # returns a String Array of prepared lines
286
+ def prepare_source_array data
287
+ return [] if data.empty?
288
+ if (leading_2_bytes = (leading_bytes = (first = data[0]).unpack 'C3').slice 0, 2) == BOM_BYTES_UTF_16LE
289
+ data[0] = first.byteslice 2, first.bytesize
290
+ # NOTE you can't split a UTF-16LE string using .lines when encoding is UTF-8; doing so will cause this line to fail
291
+ return data.map {|line| (line.encode ::Encoding::UTF_8, ::Encoding::UTF_16LE).rstrip}
292
+ elsif leading_2_bytes == BOM_BYTES_UTF_16BE
293
+ data[0] = first.byteslice 2, first.bytesize
294
+ return data.map {|line| (line.encode ::Encoding::UTF_8, ::Encoding::UTF_16BE).rstrip}
295
+ elsif leading_bytes == BOM_BYTES_UTF_8
296
+ data[0] = first.byteslice 3, first.bytesize
297
+ end
298
+ if first.encoding == ::Encoding::UTF_8
299
+ data.map {|line| line.rstrip}
300
+ else
301
+ data.map {|line| (line.encode ::Encoding::UTF_8).rstrip}
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,86 @@
1
+ require 'set'
2
+
3
+ require_relative '../diagram_converter'
4
+ require_relative '../diagram_processor'
5
+ require_relative '../util/java'
6
+
7
+ module Asciidoctor
8
+ module Diagram
9
+ # @private
10
+ class DitaaConverter
11
+ include DiagramConverter
12
+
13
+ OPTIONS = {
14
+ :scale => lambda { |o, v| o << '--scale' << v if v },
15
+ :tabs => lambda { |o, v| o << '--tabs' << v if v },
16
+ :background => lambda { |o, v| o << '--background' << v if v },
17
+ :antialias => lambda { |o, v| o << '--no-antialias' if v == 'false' },
18
+ :separation => lambda { |o, v| o << '--no-separation' if v == 'false'},
19
+ :round_corners => lambda { |o, v| o << '--round-corners' if v == 'true'},
20
+ :shadows => lambda { |o, v| o << '--no-shadows' if v == 'false'},
21
+ :debug => lambda { |o, v| o << '--debug' if v == 'true'},
22
+ :fixed_slope => lambda { |o, v| o << '--fixed-slope' if v == 'true'},
23
+ :transparent => lambda { |o, v| o << '--transparent' if v == 'true'}
24
+ }
25
+
26
+ JARS = ['ditaa-1.3.15.jar', 'ditaamini-0.12.jar'].map do |jar|
27
+ File.expand_path File.join('../..', jar), File.dirname(__FILE__)
28
+ end
29
+ Java.classpath.concat JARS
30
+
31
+
32
+ def supported_formats
33
+ [:png, :svg]
34
+ end
35
+
36
+ def collect_options(source, name)
37
+ options = {}
38
+
39
+ OPTIONS.keys.each do |option|
40
+ attr_name = option.to_s.tr('_', '-')
41
+ options[option] = source.attr(attr_name, nil, name) || source.attr(attr_name, nil, 'ditaa-option')
42
+ end
43
+
44
+ options
45
+ end
46
+
47
+ def convert(source, format, options)
48
+ Java.load
49
+
50
+ flags = []
51
+
52
+ options.each do |option, value|
53
+ OPTIONS[option].call(flags, value)
54
+ end
55
+
56
+ options_string = flags.join(' ')
57
+
58
+ case format
59
+ when :png
60
+ mime_type = 'image/png'
61
+ when :svg
62
+ mime_type = 'image/svg+xml'
63
+ else
64
+ raise "Unsupported format: #{format}"
65
+ end
66
+
67
+ headers = {
68
+ 'Accept' => mime_type,
69
+ 'X-Options' => options_string
70
+ }
71
+
72
+ response = Java.send_request(
73
+ :url => '/ditaa',
74
+ :body => source.to_s,
75
+ :headers => headers
76
+ )
77
+
78
+ unless response[:code] == 200
79
+ raise Java.create_error("Ditaa image generation failed", response)
80
+ end
81
+
82
+ response[:body]
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,79 +1,14 @@
1
- require 'set'
2
-
3
- require_relative '../extensions'
4
- require_relative '../util/java'
1
+ require_relative 'converter'
2
+ require_relative '../diagram_processor'
5
3
 
6
4
  module Asciidoctor
7
5
  module Diagram
8
- # @private
9
- module Ditaa
10
- OPTIONS = {
11
- 'scale' => lambda { |o, v| o << '--scale' << v if v },
12
- 'tabs' => lambda { |o, v| o << '--tabs' << v if v },
13
- 'background' => lambda { |o, v| o << '--background' << v if v },
14
- 'antialias' => lambda { |o, v| o << '--no-antialias' if v == 'false' },
15
- 'separation' => lambda { |o, v| o << '--no-separation' if v == 'false'},
16
- 'round-corners' => lambda { |o, v| o << '--round-corners' if v == 'true'},
17
- 'shadows' => lambda { |o, v| o << '--no-shadows' if v == 'false'},
18
- 'debug' => lambda { |o, v| o << '--debug' if v == 'true'},
19
- 'fixed-slope' => lambda { |o, v| o << '--fixed-slope' if v == 'true'},
20
- 'transparent' => lambda { |o, v| o << '--transparent' if v == 'true'}
21
- }
22
-
23
- JARS = ['ditaa-1.3.13.jar', 'ditaamini-0.11.jar'].map do |jar|
24
- File.expand_path File.join('../..', jar), File.dirname(__FILE__)
25
- end
26
- Java.classpath.concat JARS
27
-
28
- def self.included(mod)
29
- mod.register_format(:png, :image) do |parent, source|
30
- ditaa(parent, source, 'image/png')
31
- end
32
-
33
- mod.register_format(:svg, :image) do |parent, source|
34
- ditaa(parent, source, 'image/svg+xml')
35
- end
36
- end
37
-
38
- def ditaa(parent, source, mime_type)
39
- Java.load
40
-
41
- global_attributes = parent.document.attributes
42
-
43
- options = []
44
-
45
- OPTIONS.keys.each do |key|
46
- value = source.attributes.delete(key) || global_attributes["ditaa-option-#{key}"]
47
- OPTIONS[key].call(options, value)
48
- end
49
-
50
- options_string = options.join(' ')
51
-
52
- headers = {
53
- 'Accept' => mime_type,
54
- 'X-Options' => options_string
55
- }
56
-
57
- response = Java.send_request(
58
- :url => '/ditaa',
59
- :body => source.to_s,
60
- :headers => headers
61
- )
62
-
63
- unless response[:code] == 200
64
- raise Java.create_error("Ditaa image generation failed", response)
65
- end
66
-
67
- response[:body]
68
- end
69
- end
70
-
71
- class DitaaBlockProcessor < Extensions::DiagramBlockProcessor
72
- include Ditaa
6
+ class DitaaBlockProcessor < DiagramBlockProcessor
7
+ use_converter DitaaConverter
73
8
  end
74
9
 
75
- class DitaaBlockMacroProcessor < Extensions::DiagramBlockMacroProcessor
76
- include Ditaa
10
+ class DitaaBlockMacroProcessor < DiagramBlockMacroProcessor
11
+ use_converter DitaaConverter
77
12
  end
78
13
  end
79
14
  end