diagrammatron 0.1.1

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.
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2021 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require '../lib/common.rb'
8
+ require 'optparse'
9
+ require 'yaml'
10
+ require 'erb'
11
+ require 'pathname'
12
+ require 'set'
13
+ require 'base64'
14
+
15
+
16
+ Coordinate = Struct.new(:object, :key, :direction) do
17
+ def <=>(c)
18
+ d = integer <=> c.integer
19
+ return d unless d.zero?
20
+ d = direction <=> c.direction
21
+ return d unless d.zero?
22
+ fraction <=> c.fraction
23
+ end
24
+
25
+ def integer
26
+ object[key].to_int
27
+ end
28
+
29
+ def fraction
30
+ object[key] - object[key].floor
31
+ end
32
+ end
33
+
34
+ def end_directions(end_point, neighbor)
35
+ if end_point['xo'] == neighbor['xo'] # Vertical
36
+ return [ 0, (end_point['yo'] < neighbor['yo']) ? 1 : -1 ]
37
+ end
38
+ [ (end_point['xo'] < neighbor['xo']) ? 1 : -1, 0 ]
39
+ end
40
+
41
+ def push_coords(xcoords, ycoords, ckd2count, object, xdirection, ydirection, node = false)
42
+ unless node || xdirection.zero?
43
+ k = [ object['xo'], object['yo'], 'xo', xdirection ]
44
+ ckd2count[k] = ckd2count[k] + 1
45
+ end
46
+ unless node || ydirection.zero?
47
+ k = [ object['xo'], object['yo'], 'yo', ydirection ]
48
+ ckd2count[k] = ckd2count[k] + 1
49
+ end
50
+ xcoords.push(Coordinate.new(object, 'xo', xdirection))
51
+ ycoords.push(Coordinate.new(object, 'yo', ydirection))
52
+ end
53
+
54
+ def separate_coordinates(doc)
55
+ xcoords = []
56
+ ycoords = []
57
+ ckd2count = Hash.new(0)
58
+ doc['nodes'].each do |node|
59
+ # All four sides.
60
+ push_coords(xcoords, ycoords, ckd2count, node, -1, -1, true)
61
+ push_coords(xcoords, ycoords, ckd2count, node.clone, 1, 1, true)
62
+ end
63
+ doc['edges'].each do |edge|
64
+ path = edge.fetch('path', nil)
65
+ next if path.nil?
66
+ xdirection, ydirection = end_directions(path[0], path[1])
67
+ push_coords(xcoords, ycoords, ckd2count, path[0], xdirection, ydirection)
68
+ (1...(path.size - 1)).each do |k|
69
+ push_coords(xcoords, ycoords, ckd2count, path[k], 0, 0)
70
+ end
71
+ xdirection, ydirection = end_directions(path.last, path[path.size - 2])
72
+ push_coords(xcoords, ycoords, ckd2count, path.last, xdirection, ydirection)
73
+ end
74
+ xcoords.sort!
75
+ ycoords.sort!
76
+ [ xcoords, ycoords, ckd2count ]
77
+ end
78
+
79
+ class FontInfo
80
+ attr_reader :descender, :max_ascend, :ascender, :cap_height, :line_spacing, :width, :size
81
+
82
+ def initialize(template)
83
+ font = template.fetch('defaults', {}).fetch('font', { 'font_size' => 16 })
84
+ @cap_height = font.fetch('cap_height', 0.8 * font.fetch('font_size', 16))
85
+ @ascender = font.fetch('ascender', @cap_height)
86
+ @max_ascend = [ @cap_height, @ascender ].max
87
+ @descender = font.fetch('descender', 0.25 * @max_ascend)
88
+ @size = font.fetch('font_size', @max_ascend + @descender)
89
+ @line_spacing = font.fetch('line_spacing', 0.2 * @max_ascend)
90
+ @width = font.fetch('width', 0.5 * @size)
91
+ end
92
+ end
93
+
94
+ class Defaults
95
+ attr_reader :width_key, :height_key, :width_margin, :height_margin, :edge_gap
96
+ attr_reader :font
97
+
98
+ def initialize(template)
99
+ defaults = template.fetch('defaults', {})
100
+ @width_key = defaults.fetch('width_key', 'w')
101
+ @height_key = defaults.fetch('height_key', 'h')
102
+ @width_margin = defaults.fetch('width_margin', 10)
103
+ @height_margin = defaults.fetch('height_margin', 10)
104
+ @edge_gap = defaults.fetch('edge_gap', 20)
105
+ @font = FontInfo.new(template)
106
+ end
107
+ end
108
+
109
+ class SizeEstimation
110
+ attr_accessor :node, :template, :ckd2count, :defaults
111
+
112
+ def initialize(template, ckd2count, defaults)
113
+ @node = nil
114
+ @template = template
115
+ @ckd2count = ckd2count
116
+ @defaults = defaults
117
+ end
118
+
119
+ def get_binding
120
+ binding
121
+ end
122
+
123
+ def get_default(key, default_value = nil)
124
+ @template.fetch('defaults', {}).fetch(key, default_value)
125
+ end
126
+
127
+ def max_edges(key, edge_gap)
128
+ c = [ @node['xo'], @node['yo'], key, -1 ]
129
+ count = @ckd2count[c]
130
+ c[3] = 1
131
+ count = [ count, @ckd2count[c] ].max
132
+ return 0 if count < 2
133
+ (count - 1) * (edge_gap || @defaults.edge_gap)
134
+ end
135
+
136
+ def default_size(width_scale = nil, height_scale = nil, line_spacing = nil,
137
+ width_margin = nil, height_margin = nil, edge_gap = nil)
138
+ lines = @node.fetch('label', '').split("\n")
139
+ w = 2 * (width_margin || @defaults.width_margin) +
140
+ (width_scale || @default.font.width) * (lines.map &(:size)).max
141
+ @node[@defaults.width_key] = [ w, max_edges('xo', edge_gap) ].max
142
+ h = 2 * (height_margin || @defaults.height_margin) +
143
+ (height_scale || @defaults.font.size) * lines.size +
144
+ (line_spacing || @defaults.font.line_spacing) * (lines.size - 1)
145
+ @node[@defaults.height_key] = [ h, max_edges('yo', edge_gap) ].max
146
+ end
147
+ end
148
+
149
+ def estimate_sizes(doc, template, ckd2count, defaults)
150
+ $render = SizeEstimation.new(template, ckd2count, defaults)
151
+ sizes = template.fetch('sizes', {})
152
+ defaults = template.fetch('defaults', {})
153
+ doc['nodes'].each do |node|
154
+ $render.node = node
155
+ style = node.fetch('style', 'default')
156
+ code = sizes.fetch(style, defaults.fetch('size',
157
+ %(raise NotImplementedError, "No size estimator for style: #{style}")))
158
+ if sizes.key? code
159
+ code = sizes.fetch(code)
160
+ end
161
+ code = code.join("\n") if code.is_a? Array
162
+ begin
163
+ eval(code, $render.get_binding)
164
+ rescue StandardError => e
165
+ return aargh("Size estimate style #{style} node #{node.fetch('label', 'unnamed')} error #{e}", false)
166
+ end
167
+ end
168
+ $render = nil
169
+ true
170
+ end
171
+
172
+ def maxima(doc, defaults)
173
+ xmax = Hash.new(0)
174
+ ymax = Hash.new(0)
175
+ doc.fetch('nodes', []).each do |node|
176
+ xmax[node['xo']] = [ node[defaults.width_key], xmax[node['xo']] ].max
177
+ ymax[node['yo']] = [ node[defaults.height_key], ymax[node['yo']] ].max
178
+ end
179
+ [ xmax, ymax ]
180
+ end
181
+
182
+ def apply_maxima(doc, xmax, ymax, defaults)
183
+ doc.fetch('nodes', []).each do |node|
184
+ node[defaults.width_key] = xmax[node['xo']]
185
+ node[defaults.height_key] = ymax[node['yo']]
186
+ end
187
+ end
188
+
189
+ def parallel_edge_step_minima(coords)
190
+ c2m = Hash.new(1.0)
191
+ coords.each do |coord|
192
+ f = coord.fraction
193
+ next if f.zero?
194
+ ic = coord.integer
195
+ c2m[ic] = [ c2m[ic], f ].min
196
+ end
197
+ c2m
198
+ end
199
+
200
+ def remap_coordinates(coords, cmax, c2min, defaults)
201
+ c = defaults.edge_gap
202
+ gap = 0 # How much space all edge segments need.
203
+ zero_after_decrease = false
204
+ prev_dir = -2
205
+ coords.each do |coord|
206
+ zero_after_decrease = true if prev_dir == -1 && coord.direction.zero?
207
+ case coord.direction
208
+ when -1
209
+ c += gap if -1 < prev_dir
210
+ gap = defaults.edge_gap
211
+ coord.object[coord.key] = c
212
+ when 0
213
+ gap = defaults.edge_gap / c2min[coord.integer]
214
+ if zero_after_decrease
215
+ # Edge segment is at same range as nodes.
216
+ coord.object[coord.key] = c + coord.fraction * cmax[coord.integer]
217
+ else
218
+ coord.object[coord.key] =
219
+ c + defaults.edge_gap * coord.fraction / c2min[coord.integer]
220
+ end
221
+ when 1
222
+ gap = defaults.edge_gap
223
+ c += cmax[coord.integer] unless 1 == prev_dir
224
+ coord.object[coord.key] = c
225
+ zero_after_decrease = false
226
+ end
227
+ prev_dir = coord.direction
228
+ end
229
+ end
230
+
231
+ class Render
232
+ attr_accessor :doc, :template, :defaults
233
+
234
+ def initialize(doc, template, defaults)
235
+ @doc = doc
236
+ @template = template
237
+ @defaults = defaults
238
+ end
239
+
240
+ def get_binding
241
+ binding
242
+ end
243
+
244
+ def get_default(key, default_value = nil)
245
+ @template.fetch('defaults', {}).fetch(key, default_value)
246
+ end
247
+
248
+ def dimensions
249
+ w = 0
250
+ h = 0
251
+ @doc.fetch('nodes', []).each do |node|
252
+ w = [ w, node['xo'] + node[@defaults.width_key] ].max
253
+ h = [ h, node['yo'] + node[@defaults.height_key] ].max
254
+ end
255
+ @doc.fetch('edges', []).each do |edge|
256
+ path = edge.fetch('path', nil)
257
+ next if path.nil?
258
+ path.each do |p|
259
+ w = [ w, p['xo'] ].max
260
+ h = [ h, p['yo'] ].max
261
+ end
262
+ end
263
+ [ w.to_i, h.to_i ]
264
+ end
265
+ end
266
+
267
+ def apply(doc, template, defaults)
268
+ $render = Render.new(doc, template, defaults)
269
+ out = ERB.new(template.fetch('template', '')).result($render.get_binding)
270
+ $render = nil
271
+ out
272
+ end
273
+
274
+ def main
275
+ template = nil
276
+ input = nil
277
+ output = nil
278
+ parser = OptionParser.new do |opts|
279
+ opts.summary_indent = ' '
280
+ opts.summary_width = 20
281
+ opts.banner = 'Usage: diagrammatron-render [options]'
282
+ opts.separator ''
283
+ opts.separator 'Options:'
284
+ opts.on('-t', '--template FILE', 'Template file name.') do |filename|
285
+ template = filename
286
+ end
287
+ opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
288
+ input = filename
289
+ end
290
+ opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
291
+ output = filename
292
+ end
293
+ opts.on('-h', '--help', 'Print this help and exit.') do
294
+ $stdout.puts %(#{opts}
295
+ Input YAML file is expected to be the output of diagrammatron-place.
296
+
297
+ Output is the file produced by the erb-template.
298
+ )
299
+ exit 0
300
+ end
301
+ end
302
+ parser.parse! ARGV
303
+
304
+ return aargh('Template must be given.', 2) if template.nil?
305
+ template = load_source(template)
306
+ return 2 if template.nil?
307
+ template.keys.sort.each do |key|
308
+ next unless key.start_with? 'base64'
309
+ nk = key.slice(6, key.size - 6)
310
+ begin
311
+ template[nk] = Base64.strict_decode64(template[key])
312
+ template.delete key
313
+ rescue StandardError
314
+ return aargh("Key #{key} base-64 decoding failed to key #{nk}", 2)
315
+ end
316
+ end
317
+ defaults = Defaults.new(template)
318
+
319
+ doc = load_source(input)
320
+ return 2 if doc.nil?
321
+
322
+ begin
323
+ xcoords, ycoords, ckd2count = separate_coordinates(doc)
324
+ rescue StandardError
325
+ return aargh('Error processing input.', 3)
326
+ end
327
+
328
+ return 4 unless estimate_sizes(doc, template, ckd2count, defaults)
329
+
330
+ # Make all rows the same height and all columns the same width.
331
+ xmax, ymax = maxima(doc, defaults)
332
+ apply_maxima(doc, xmax, ymax, defaults)
333
+
334
+ x2min = parallel_edge_step_minima(xcoords)
335
+ y2min = parallel_edge_step_minima(ycoords)
336
+ remap_coordinates(xcoords, xmax, x2min, defaults)
337
+ remap_coordinates(ycoords, ymax, y2min, defaults)
338
+
339
+ out = apply(doc, template, defaults)
340
+ begin
341
+ if output.nil?
342
+ $stdout.puts out
343
+ else
344
+ fp = Pathname.new output
345
+ fp.open('w') do |f|
346
+ f.puts out
347
+ end
348
+ end
349
+ rescue StandardError => e
350
+ return aargh([ e, "Failed to write output: #{output || 'stdout'}" ], 5)
351
+ end
352
+ 0
353
+ end
354
+
355
+ exit(main) if (defined? $unit_test).nil?
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2021 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require '../lib/common.rb'
8
+ require 'optparse'
9
+ require 'yaml'
10
+ require 'set'
11
+ require 'pathname'
12
+ require 'base64'
13
+
14
+
15
+ def add_field(doc, field_name, content)
16
+ if field_name.start_with?('base64')
17
+ doc[field_name] = content
18
+ else
19
+ doc["base64#{field_name}"] = Base64.strict_encode64(content)
20
+ end
21
+ end
22
+
23
+ def missing(doc)
24
+ [ 'defaults', 'sizes', 'template' ].each do |key|
25
+ next if doc.key? key
26
+ next if doc.key? "base64#{key}"
27
+ return aargh("#{key} is missing", 4)
28
+ end
29
+ nil
30
+ end
31
+
32
+ def main
33
+ input = nil
34
+ output = nil
35
+ ENV['POSIXLY_CORRECT'] = '1' # Leaves patterns as they are.
36
+ parser = OptionParser.new do |opts|
37
+ opts.summary_indent = ' '
38
+ opts.summary_width = 20
39
+ opts.banner = 'Usage: diagrammatron-template [options] field-name content-file ...'
40
+ opts.separator ''
41
+ opts.separator 'Options:'
42
+ opts.on('-r', '--root FILE', 'Starting point YAML file name.') do |filename|
43
+ input = filename
44
+ end
45
+ opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
46
+ output = filename
47
+ end
48
+ opts.on('-h', '--help', 'Print this help and exit.') do
49
+ $stdout.puts %(#{opts}
50
+ Pairs all parameter field-names with content-files contents, starting with
51
+ either given root YAML file or with an empty root.
52
+
53
+ Any field name either in root document or in parameters is trusted to be
54
+ base-64 encoded without further checking.
55
+
56
+ Outputs a YAML-file that case be used with diagrammatron-render as a template.
57
+ All fields are base64-encoded for safety. diagrammatron-render will decode
58
+ them and rename the fields by removing the base64 prefix.
59
+
60
+ Presence of "defaults", "sizes", and "template" fields is checked for.
61
+ Extra fields are not restricted in any manner.
62
+ )
63
+ exit 0
64
+ end
65
+ end
66
+ parser.parse! ARGV
67
+
68
+ if ARGV.size.odd?
69
+ return aargh('Field-names and content-files count is odd.', 1)
70
+ end
71
+
72
+ doc = input.nil? ? {} : load_source_hash(input)
73
+ return 2 if doc.nil?
74
+
75
+ (0...ARGV.size).step(2) do |k|
76
+ fn = ARGV[k]
77
+ cfn = ARGV[k + 1]
78
+ begin
79
+ c = File.read(cfn)
80
+ rescue Errno::ENOENT
81
+ return aargh("Could not read #{cfn}", 3)
82
+ rescue StandardError => e
83
+ return aargh([ e, "Failed to read #{cfn}" ], 3)
84
+ end
85
+ add_field(doc, fn, c)
86
+ end
87
+
88
+ m = missing(doc)
89
+ return m unless m.nil?
90
+
91
+ begin
92
+ d = YAML.dump(doc, line_width: 1000000)
93
+ if output.nil?
94
+ $stdout.puts d
95
+ else
96
+ fp = Pathname.new output
97
+ fp.open('w') do |f|
98
+ f.puts d
99
+ end
100
+ end
101
+ rescue StandardError => e
102
+ return aargh([ e, "Failed to write output: #{output || 'stdout'}" ], 5)
103
+ end
104
+ 0
105
+ end
106
+
107
+ exit(main) if (defined? $unit_test).nil?
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2021 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require '../lib/common.rb'
8
+ require 'optparse'
9
+ require 'yaml'
10
+ require 'json'
11
+ require 'pathname'
12
+
13
+ '''
14
+ def load_source
15
+ begin
16
+ if $INPUT.nil?
17
+ src = $stdin.read
18
+ else
19
+ src = File.read($INPUT)
20
+ end
21
+ src = JSON.parse(src)
22
+ rescue Errno::ENOENT
23
+ return aargh("Could not load #{$INPUT || 'stdin'}")
24
+ rescue StandardError => e
25
+ return aargh("#{e}\nFailed to read #{$INPUT || 'stdin'}")
26
+ end
27
+ src
28
+ end
29
+ '''
30
+
31
+ def convert(src)
32
+ idx2label = {}
33
+ labels = {} # For label collisions.
34
+ compound = {} # For compound nodes to remove if no edges.
35
+ nodes = []
36
+ items = src.fetch('objects', [])
37
+ items.each_index do |k|
38
+ item = items[k]
39
+ idx = item.fetch('_gvid', nil)
40
+ name = item.fetch('name', item.fetch('label', "node #{k}"))
41
+ return aargh("Object #{name} missing _gvid") if idx.nil?
42
+ compound[idx] = k if item.fetch('compound', false)
43
+ if labels.key? name
44
+ labels[name] += 1
45
+ name = "#{name}.#{labels[name]}" # Could collide here as well.
46
+ else
47
+ labels[name] = 0
48
+ end
49
+ idx2label[idx] = name
50
+ item['label'] = name
51
+ nodes.push(item)
52
+ end
53
+ edges = []
54
+ items = src.fetch('edges', [])
55
+ items.each_index do |k|
56
+ item = items[k]
57
+ tail = item.fetch('tail', nil)
58
+ head = item.fetch('head', nil)
59
+ if tail.nil? || head.nil?
60
+ return aargh("Edge #{k} (_gvid #{item.fetch('_gvid', 'undefined')}) head or tail missing")
61
+ end
62
+ next if head == tail
63
+ if compound.key? head
64
+ compound.delete head
65
+ elsif compound.key? tail
66
+ compound.delete tail
67
+ end
68
+ head = idx2label.fetch(head, nil)
69
+ tail = idx2label.fetch(tail, nil)
70
+ if tail.nil? || head.nil?
71
+ return aargh("Edge #{k} (_gvid #{item.fetch('_gvid', 'undefined')}) head or tail refer to unseen object index")
72
+ end
73
+ item['between'] = [ tail, head ]
74
+ edges.push(item)
75
+ end
76
+ # All compound nodes that have no edges are removed.
77
+ compound.values.sort.reverse.each do |k|
78
+ nodes.delete_at k
79
+ end
80
+ { 'edges' => edges, 'nodes' => nodes }
81
+ end
82
+
83
+ $INPUT = nil
84
+ $OUTPUT = nil
85
+
86
+ def main
87
+ parser = OptionParser.new do |opts|
88
+ opts.summary_indent = ' '
89
+ opts.summary_width = 20
90
+ opts.banner = 'Usage: dot_json2diagrammatron [options]'
91
+ opts.separator ''
92
+ opts.separator 'Options:'
93
+ opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
94
+ $INPUT = filename
95
+ end
96
+ opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
97
+ $OUTPUT = filename
98
+ end
99
+ opts.on('-h', '--help', 'Print this help and exit.') do
100
+ $stdout.puts opts
101
+ $stdout.puts %(
102
+ Output is a YAML file with "name" used as "label" and "shape" used as is
103
+ for nodes. For edges "tail" and "head" are used as the first and second element
104
+ of "between" with the "name" as the identifier. Compound objects are ignored.
105
+ )
106
+ exit 0
107
+ end
108
+ end
109
+ parser.parse! ARGV
110
+
111
+ doc = load_source
112
+ exit(2) if doc.nil?
113
+
114
+ begin
115
+ out = convert(doc)
116
+ rescue StandardError
117
+ out = aargh('Error processing input.')
118
+ end
119
+ exit(3) if out.nil?
120
+
121
+ begin
122
+ if $OUTPUT.nil?
123
+ $stdout.puts YAML.dump(out)
124
+ else
125
+ fp = Pathname.new $OUTPUT
126
+ fp.open('w') do |f|
127
+ f.puts YAML.dump(out)
128
+ end
129
+ end
130
+ rescue StandardError => e
131
+ aargh("#{e}\nFailed to write output: #{$OUTPUT}")
132
+ exit 4
133
+ end
134
+ end
135
+
136
+ main if (defined? $unit_test).nil?
data/lib/common.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2021 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ def aargh(message, return_value = nil)
7
+ message = (message.map &(:to_s)).join("\n") if message.is_a? Array
8
+ $stderr.puts message
9
+ return_value
10
+ end
11
+
12
+ def load_source(input)
13
+ YAML.safe_load(input.nil? ? $stdin : File.read(input))
14
+ rescue Errno::ENOENT
15
+ aargh "Could not load #{input || 'stdin'}"
16
+ rescue StandardError => e
17
+ aargh "#{e}\nFailed to read #{input || 'stdin'}"
18
+ end
19
+
20
+ def load_source_hash(input)
21
+ src = load_source(input)
22
+ unless src.nil?
23
+ return aargh('Input is not a mapping.') unless src.is_a? Hash
24
+ end
25
+ src
26
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: diagrammatron
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Ismo Kärkkäinen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2
14
+
15
+ Generates diagrams in SVG format from input material. Split into multiple
16
+ programs that each perform one stage.
17
+
18
+ Source: https://github.com/ismo-karkkainen/diagrammatron
19
+
20
+ Licensed under Universal Permissive License, see LICENSE.txt.
21
+ email: ismokarkkainen@icloud.com
22
+ executables:
23
+ - diagrammatron-edges
24
+ - diagrammatron-nodes
25
+ - diagrammatron-place
26
+ - diagrammatron-prune
27
+ - diagrammatron-render
28
+ - diagrammatron-template
29
+ - dot_json2diagrammatron
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - LICENSE.txt
34
+ - bin/diagrammatron-edges
35
+ - bin/diagrammatron-nodes
36
+ - bin/diagrammatron-place
37
+ - bin/diagrammatron-prune
38
+ - bin/diagrammatron-render
39
+ - bin/diagrammatron-template
40
+ - bin/dot_json2diagrammatron
41
+ - lib/common.rb
42
+ homepage: http://xn--ismo-krkkinen-gfbd.fi/diagrammatron/index.html
43
+ licenses:
44
+ - UPL-1.0
45
+ metadata: {}
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.1.2
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Generates diagrams from input graph.
65
+ test_files: []