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.
- checksums.yaml +7 -0
- data/LICENSE.txt +17 -0
- data/bin/diagrammatron-edges +845 -0
- data/bin/diagrammatron-nodes +439 -0
- data/bin/diagrammatron-place +446 -0
- data/bin/diagrammatron-prune +117 -0
- data/bin/diagrammatron-render +355 -0
- data/bin/diagrammatron-template +107 -0
- data/bin/dot_json2diagrammatron +136 -0
- data/lib/common.rb +26 -0
- metadata +65 -0
@@ -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: []
|