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,446 @@
|
|
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
|
+
|
13
|
+
def info(msg, return_value = nil, loud = false)
|
14
|
+
$stderr.puts(msg) unless $QUIET && !loud
|
15
|
+
return_value
|
16
|
+
end
|
17
|
+
|
18
|
+
BoundingBox = Struct.new(:xmin, :ymin, :xmax, :ymax) do
|
19
|
+
def merge(bb)
|
20
|
+
return self if bb.nil?
|
21
|
+
BoundingBox.new(
|
22
|
+
(xmin < bb.xmin) ? xmin : bb.xmin,
|
23
|
+
(ymin < bb.ymin) ? ymin : bb.ymin,
|
24
|
+
(xmax < bb.xmax) ? bb.xmax : xmax,
|
25
|
+
(ymax < bb.ymax) ? bb.ymax : ymax
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def width
|
30
|
+
xmax - xmin
|
31
|
+
end
|
32
|
+
|
33
|
+
def height
|
34
|
+
ymax - ymin
|
35
|
+
end
|
36
|
+
|
37
|
+
def area
|
38
|
+
(xmax - xmin) * (ymax - ymin)
|
39
|
+
end
|
40
|
+
|
41
|
+
def short_side
|
42
|
+
[(xmax - xmin) * $W2HRATIO, (ymax - ymin)].min
|
43
|
+
end
|
44
|
+
|
45
|
+
def long_side
|
46
|
+
[(xmax - xmin) * $W2HRATIO, (ymax - ymin)].max
|
47
|
+
end
|
48
|
+
|
49
|
+
def overlap?(bb)
|
50
|
+
return false if bb.nil?
|
51
|
+
return false if bb.xmax <= xmin
|
52
|
+
return false if xmax <= bb.xmin
|
53
|
+
return false if bb.ymax <= ymin
|
54
|
+
return false if ymax <= bb.ymin
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def shift(dx, dy)
|
59
|
+
self.xmin += dx
|
60
|
+
self.ymin += dy
|
61
|
+
self.xmax += dx
|
62
|
+
self.ymax += dy
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Edge = Struct.new(:index, :sid, :path) do
|
67
|
+
def bounding_box
|
68
|
+
xs = path.map { |xy| xy['xo'] }
|
69
|
+
ys = path.map { |xy| xy['yo'] }
|
70
|
+
BoundingBox.new(xs.min.floor, ys.min.floor, xs.max.floor, ys.max.floor)
|
71
|
+
end
|
72
|
+
|
73
|
+
def shift(dx, dy)
|
74
|
+
path.each do |c|
|
75
|
+
c['xo'] += dx
|
76
|
+
c['yo'] += dy
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
Node = Struct.new(:index, :sid, :xo, :yo) do
|
82
|
+
def bounding_box
|
83
|
+
BoundingBox.new(xo.floor, yo.floor, xo.floor, yo.floor)
|
84
|
+
end
|
85
|
+
|
86
|
+
def shift(dx, dy)
|
87
|
+
self.xo += dx
|
88
|
+
self.yo += dy
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def work_copy(src)
|
93
|
+
work = { edges: {}, nodes: {} }
|
94
|
+
# Expected nodes, edges. Other pass-through.
|
95
|
+
nodes = src.fetch('nodes', [])
|
96
|
+
nodes.each_index do |k|
|
97
|
+
node = nodes[k]
|
98
|
+
sid = node.fetch('sid', nil)
|
99
|
+
xo = node.fetch('xo', nil)
|
100
|
+
yo = node.fetch('yo', nil)
|
101
|
+
next if sid.nil? || xo.nil? || yo.nil?
|
102
|
+
work[:nodes][sid] = work[:nodes].fetch(sid, []).push(Node.new(k, sid, xo, yo))
|
103
|
+
end
|
104
|
+
edges = src.fetch('edges', [])
|
105
|
+
edges.each_index do |k|
|
106
|
+
edge = edges[k]
|
107
|
+
path = edge.fetch('path', nil)
|
108
|
+
sid = edge.fetch('sid', nil)
|
109
|
+
next if path.nil? || sid.nil?
|
110
|
+
work[:edges][sid] = work[:edges].fetch(sid, []).push(Edge.new(k, sid, path))
|
111
|
+
end
|
112
|
+
work[:subsets] = work[:nodes].keys.to_set.merge(work[:edges].keys.to_set).to_a
|
113
|
+
work
|
114
|
+
end
|
115
|
+
|
116
|
+
def bounding_box(work, sid)
|
117
|
+
bb = nil
|
118
|
+
%i[edges nodes].each do |kind|
|
119
|
+
work[kind].fetch(sid, []).each do |item|
|
120
|
+
bb = item.bounding_box.merge(bb)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
bb.xmax += 1
|
124
|
+
bb.ymax += 1
|
125
|
+
bb
|
126
|
+
end
|
127
|
+
|
128
|
+
def area_compare(b, a)
|
129
|
+
d = a.area <=> b.area
|
130
|
+
return d unless d.zero?
|
131
|
+
d = (a.xmax - a.xmin) <=> (b.xmax - b.xmin)
|
132
|
+
return d unless d.zero?
|
133
|
+
(a.ymax - a.ymin) <=> (b.ymax - b.ymin)
|
134
|
+
end
|
135
|
+
|
136
|
+
def area_order(bbs)
|
137
|
+
order = []
|
138
|
+
bbs.each_pair do |sid, bbox|
|
139
|
+
order.push([sid, bbox])
|
140
|
+
end
|
141
|
+
order.sort! do |a, b|
|
142
|
+
d = area_compare(a[1], b[1])
|
143
|
+
(d != 0) ? d : (a[0] <=> b[0])
|
144
|
+
end
|
145
|
+
order
|
146
|
+
end
|
147
|
+
|
148
|
+
def overlaps(state, cand)
|
149
|
+
state.each_value do |s|
|
150
|
+
return true if cand.overlap?(s)
|
151
|
+
end
|
152
|
+
false
|
153
|
+
end
|
154
|
+
|
155
|
+
State = Struct.new(:state, :bbox, :used, :remaining_area) do
|
156
|
+
def key?(sid_bbox)
|
157
|
+
state.key? sid_bbox.first
|
158
|
+
end
|
159
|
+
|
160
|
+
def can_improve?(small)
|
161
|
+
return true if small.nil?
|
162
|
+
must_add = [ remaining_area - (bbox.area - used), 0 ].max
|
163
|
+
bbox.area + must_add < small.bbox.area
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def depth_first_search(order, state, small, reverse = false)
|
168
|
+
if state.state.size < order.size
|
169
|
+
return small unless state.can_improve? small
|
170
|
+
# Loop over all sids not in state and the loop over false, true.
|
171
|
+
prev = nil
|
172
|
+
order.each do |sb|
|
173
|
+
next if state.key? sb
|
174
|
+
next if sb[1] == prev
|
175
|
+
prev = sb[1]
|
176
|
+
state.state.each_value do |n|
|
177
|
+
[false, true].each do |right|
|
178
|
+
right = !right if reverse
|
179
|
+
cand = sb[1].clone
|
180
|
+
cand.shift(right ? n.xmax : n.xmin, right ? n.ymin : n.ymax)
|
181
|
+
next if overlaps(state.state, cand) # Invalid state.
|
182
|
+
# Did we create a worse solution?
|
183
|
+
bc = state.bbox.merge(cand)
|
184
|
+
unless small.nil?
|
185
|
+
d = small.bbox.long_side <=> bc.long_side
|
186
|
+
next if d.negative?
|
187
|
+
next if d.zero? && small.bbox.short_side <= bc.short_side
|
188
|
+
end
|
189
|
+
s = state.state.clone
|
190
|
+
s[sb[0]] = cand
|
191
|
+
a = sb[1].area
|
192
|
+
small = depth_first_search(order,
|
193
|
+
State.new(s, bc, state.used + a, state.remaining_area - a),
|
194
|
+
small, !reverse)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
else # We compared with area before call so no need to do it here.
|
199
|
+
small = State.new(state.state.clone, state.bbox.clone, state.used, 0)
|
200
|
+
info("Found (#{state.bbox.width}, #{state.bbox.height})")
|
201
|
+
end
|
202
|
+
small
|
203
|
+
end
|
204
|
+
|
205
|
+
def diagram_info(order, remains)
|
206
|
+
return if $QUIET
|
207
|
+
sizes = [ order[0][1] ]
|
208
|
+
(1...order.size).each do |k|
|
209
|
+
next if order[k - 1][1] == order[k][1]
|
210
|
+
sizes.push(order[k][1])
|
211
|
+
end
|
212
|
+
sizes = sizes.map { |b| "(#{b.width}, #{b.height})" }
|
213
|
+
info("#{order.size} sub-diagrams total area #{remains} in sizes #{sizes.join(' ')}")
|
214
|
+
end
|
215
|
+
|
216
|
+
def dumb_depth_first_search(bbs)
|
217
|
+
order = area_order(bbs)
|
218
|
+
remains = (order.map { |sb| sb[1].area }).sum
|
219
|
+
diagram_info(order, remains)
|
220
|
+
# At top level try only widest and tallest as in other places they can swap
|
221
|
+
# with a block of others in other locations.
|
222
|
+
widest = 0
|
223
|
+
tallest = 0
|
224
|
+
(1...order.size).each do |k|
|
225
|
+
widest = k if order[widest][1].width < order[k][1].width
|
226
|
+
tallest = k if order[tallest][1].height < order[k][1].height
|
227
|
+
end
|
228
|
+
small = nil
|
229
|
+
((widest == tallest) ? [ widest ] : [ widest, tallest ]).each do |k|
|
230
|
+
info("Top-level candidate #{k + 1}")
|
231
|
+
sb = order[k]
|
232
|
+
a = sb[1].area
|
233
|
+
small = depth_first_search(order,
|
234
|
+
State.new({ sb[0] => sb[1].clone }, sb[1], a, remains - a), small)
|
235
|
+
end
|
236
|
+
small.state # State is sid to final bbox mapping.
|
237
|
+
end
|
238
|
+
|
239
|
+
def tallest_and_widest(order, state)
|
240
|
+
widest = nil
|
241
|
+
tallest = nil
|
242
|
+
order.each do |sb|
|
243
|
+
next if state.key? sb
|
244
|
+
unless widest.nil?
|
245
|
+
widest = sb if widest[1].width < sb[1].width
|
246
|
+
tallest = sb if tallest[1].height < sb[1].height
|
247
|
+
else
|
248
|
+
widest = sb
|
249
|
+
tallest = sb
|
250
|
+
end
|
251
|
+
end
|
252
|
+
return [ tallest ] if widest[1] == tallest[1]
|
253
|
+
[ tallest, widest ]
|
254
|
+
end
|
255
|
+
|
256
|
+
def tall_wide_dfs(order, state, small)
|
257
|
+
if state.state.size < order.size
|
258
|
+
return small unless state.can_improve? small
|
259
|
+
tallest_and_widest(order, state).each do |sb|
|
260
|
+
state.state.each_value do |n|
|
261
|
+
[false, true].each do |right|
|
262
|
+
cand = sb[1].clone
|
263
|
+
cand.shift(right ? n.xmax : n.xmin, right ? n.ymin : n.ymax)
|
264
|
+
next if overlaps(state.state, cand) # Invalid state.
|
265
|
+
# Did we create a worse solution?
|
266
|
+
bc = state.bbox.merge(cand)
|
267
|
+
unless small.nil?
|
268
|
+
d = small.bbox.long_side <=> bc.long_side
|
269
|
+
next if d.negative?
|
270
|
+
next if d.zero? && small.bbox.short_side <= bc.short_side
|
271
|
+
end
|
272
|
+
s = state.state.clone
|
273
|
+
s[sb[0]] = cand
|
274
|
+
a = sb[1].area
|
275
|
+
small = tall_wide_dfs(order,
|
276
|
+
State.new(s, bc, state.used + a, state.remaining_area - a), small)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
else # We compared with area before call so no need to do it here.
|
281
|
+
small = State.new(state.state.clone, state.bbox.clone, state.used, 0)
|
282
|
+
info("Found (#{state.bbox.width}, #{state.bbox.height})")
|
283
|
+
end
|
284
|
+
small
|
285
|
+
end
|
286
|
+
|
287
|
+
def tallest_widest_dfs(bbs)
|
288
|
+
order = area_order(bbs)
|
289
|
+
remains = (order.map { |sb| sb[1].area }).sum
|
290
|
+
diagram_info(order, remains)
|
291
|
+
small = nil
|
292
|
+
tallest_and_widest(order, State.new({})).each do |sb|
|
293
|
+
a = sb[1].area
|
294
|
+
small = tall_wide_dfs(order,
|
295
|
+
State.new({ sb[0] => sb[1].clone }, sb[1], a, remains - a), small)
|
296
|
+
end
|
297
|
+
small.state # State is sid to final bbox mapping.
|
298
|
+
end
|
299
|
+
|
300
|
+
def pre_search_bounding_boxes(work)
|
301
|
+
bbs = {}
|
302
|
+
original = {}
|
303
|
+
work[:subsets].each do |sid|
|
304
|
+
bb = bounding_box(work, sid)
|
305
|
+
original[sid] = bb.clone
|
306
|
+
bb.shift(-bb.xmin, -bb.ymin) # Ensure the corner is at (0, 0)
|
307
|
+
bbs[sid] = bb
|
308
|
+
end
|
309
|
+
[ original, bbs ]
|
310
|
+
end
|
311
|
+
|
312
|
+
def shift(work, sid, dx, dy)
|
313
|
+
[ :edges, :nodes ].each do |kind|
|
314
|
+
work[kind].fetch(sid, []).each do |item|
|
315
|
+
item.shift(dx, dy)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def shift_subdiagrams(work, original, bbs)
|
321
|
+
# Shift everything in each subset according to shift compared to original.
|
322
|
+
original.each_pair do |sid, bbox|
|
323
|
+
shifted = bbs[sid]
|
324
|
+
shift(work, sid, shifted.xmin - bbox.xmin, shifted.ymin - bbox.ymin)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def place_subsets(work)
|
329
|
+
original, bbs = pre_search_bounding_boxes(work)
|
330
|
+
bbs = dumb_depth_first_search(bbs)
|
331
|
+
shift_subdiagrams(work, original, bbs)
|
332
|
+
end
|
333
|
+
|
334
|
+
def tallest_widest(work)
|
335
|
+
original, bbs = pre_search_bounding_boxes(work)
|
336
|
+
bbs = tallest_widest_dfs(bbs)
|
337
|
+
shift_subdiagrams(work, original, bbs)
|
338
|
+
end
|
339
|
+
|
340
|
+
def prepare_output(doc, work)
|
341
|
+
work[:nodes].each_value do |nodes|
|
342
|
+
nodes.each do |node|
|
343
|
+
doc['nodes'][node.index]['xo'] = node.xo
|
344
|
+
doc['nodes'][node.index]['yo'] = node.yo
|
345
|
+
end
|
346
|
+
end
|
347
|
+
work[:edges].each_value do |edges|
|
348
|
+
edges.each do |edge|
|
349
|
+
doc['edges'][edge.index]['path'] = edge.path
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
$algorithms = {
|
355
|
+
'depthfirst' => method(:place_subsets),
|
356
|
+
'tallwide' => method(:tallest_widest)
|
357
|
+
}
|
358
|
+
|
359
|
+
$W2HRATIO = 1.0
|
360
|
+
$QUIET = false
|
361
|
+
|
362
|
+
def main
|
363
|
+
input = nil
|
364
|
+
output = nil
|
365
|
+
algo = 'tallwide'
|
366
|
+
parser = OptionParser.new do |opts|
|
367
|
+
opts.summary_indent = ' '
|
368
|
+
opts.summary_width = 20
|
369
|
+
opts.banner = 'Usage: diagrammatron-place [options]'
|
370
|
+
opts.separator ''
|
371
|
+
opts.separator 'Options:'
|
372
|
+
opts.on('-a', '--algorithm NAME', "Subdiagram placement algoritm. Default #{algo}") do |a|
|
373
|
+
algo = a
|
374
|
+
end
|
375
|
+
opts.on('-r', '--ratio VALUE', 'Node width to height ratio. Default 1.0.') do |value|
|
376
|
+
$W2HRATIO = value
|
377
|
+
end
|
378
|
+
opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
|
379
|
+
input = filename
|
380
|
+
end
|
381
|
+
opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
|
382
|
+
output = filename
|
383
|
+
end
|
384
|
+
opts.on('-q', '--quiet', 'No output except errors.') do
|
385
|
+
$QUIET = true
|
386
|
+
end
|
387
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
388
|
+
$stdout.puts opts
|
389
|
+
$stdout.puts %(
|
390
|
+
Algorithm names are: #{$algorithms.keys.sort.join(' ')}
|
391
|
+
|
392
|
+
Input YAML file is expected to be the output of diagrammatron-edges.
|
393
|
+
|
394
|
+
Output is the input file with 'xo' and 'yo' modified so that the sub-diagrams
|
395
|
+
do not overlap.
|
396
|
+
)
|
397
|
+
exit 0
|
398
|
+
end
|
399
|
+
end
|
400
|
+
parser.parse! ARGV
|
401
|
+
unless $W2HRATIO.is_a? Float
|
402
|
+
begin
|
403
|
+
whratio = Float($W2HRATIO)
|
404
|
+
if whratio <= 0
|
405
|
+
return info("Ratio must be greater than zero: #{$W2HRATIO}", 1, true)
|
406
|
+
end
|
407
|
+
$W2HRATIO = whratio
|
408
|
+
rescue StandardError
|
409
|
+
return info("Ratio parameter not a number: #{$W2HRATIO}", 1, true)
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
unless $algorithms.key? algo
|
414
|
+
return info("Unrecognized algorithm: #{algo}", 2, true)
|
415
|
+
end
|
416
|
+
algo = $algorithms[algo]
|
417
|
+
|
418
|
+
doc = load_source(input)
|
419
|
+
return 2 if doc.nil?
|
420
|
+
|
421
|
+
begin
|
422
|
+
work = work_copy(doc)
|
423
|
+
return 3 if work.nil?
|
424
|
+
rescue StandardError
|
425
|
+
return info('Error processing input.', 3, true)
|
426
|
+
end
|
427
|
+
|
428
|
+
algo.call(work)
|
429
|
+
prepare_output(doc, work)
|
430
|
+
begin
|
431
|
+
d = YAML.dump(doc, line_width: 1000000)
|
432
|
+
if output.nil?
|
433
|
+
$stdout.puts d
|
434
|
+
else
|
435
|
+
fp = Pathname.new output
|
436
|
+
fp.open('w') do |f|
|
437
|
+
f.puts d
|
438
|
+
end
|
439
|
+
end
|
440
|
+
rescue StandardError => e
|
441
|
+
return info("#{e}\nFailed to write output: #{output || 'stdout'}", 4, true)
|
442
|
+
end
|
443
|
+
0
|
444
|
+
end
|
445
|
+
|
446
|
+
exit(main) if (defined? $unit_test).nil?
|
@@ -0,0 +1,117 @@
|
|
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
|
+
|
13
|
+
|
14
|
+
def prune_array(original, matching, keep)
|
15
|
+
kept = []
|
16
|
+
original.each_index do |k|
|
17
|
+
if (keep && matching[k]) || (!keep && !matching[k])
|
18
|
+
kept.push original[k]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
kept
|
22
|
+
end
|
23
|
+
|
24
|
+
def prune(doc, patterns, keep)
|
25
|
+
labels = Set.new
|
26
|
+
nodes = doc.fetch('nodes', [])
|
27
|
+
matching = Array.new(nodes.size, false)
|
28
|
+
nodes.each_index do |k|
|
29
|
+
label = nodes[k].fetch('label', '')
|
30
|
+
patterns.each do |p|
|
31
|
+
next unless p.match(label)
|
32
|
+
matching[k] = true
|
33
|
+
labels.add label
|
34
|
+
break
|
35
|
+
end
|
36
|
+
end
|
37
|
+
doc['nodes'] = prune_array(nodes, matching, keep)
|
38
|
+
edges = doc.fetch('edges', [])
|
39
|
+
matching = Array.new(edges.size, false)
|
40
|
+
edges.each_index do |k|
|
41
|
+
between = edges[k].fetch('between', [])
|
42
|
+
if between.size != 2
|
43
|
+
matching[k] = keep # Removing these suppresses later warnings.
|
44
|
+
elsif keep
|
45
|
+
matching[k] = (labels.member?(between[0]) && labels.member?(between[1]))
|
46
|
+
else
|
47
|
+
matching[k] = (labels.member?(between[0]) || labels.member?(between[1]))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
doc['edges'] = prune_array(edges, matching, keep)
|
51
|
+
end
|
52
|
+
|
53
|
+
def main
|
54
|
+
input = nil
|
55
|
+
output = nil
|
56
|
+
keep = false
|
57
|
+
ENV['POSIXLY_CORRECT'] = '1' # Leaves patterns as they are.
|
58
|
+
parser = OptionParser.new do |opts|
|
59
|
+
opts.summary_indent = ' '
|
60
|
+
opts.summary_width = 20
|
61
|
+
opts.banner = 'Usage: diagrammatron-prune [options] patterns...'
|
62
|
+
opts.separator ''
|
63
|
+
opts.separator 'Options:'
|
64
|
+
opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
|
65
|
+
input = filename
|
66
|
+
end
|
67
|
+
opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
|
68
|
+
output = filename
|
69
|
+
end
|
70
|
+
opts.on('-r', '--reverse', 'Keep nodes that match patterns.') do
|
71
|
+
keep = true
|
72
|
+
end
|
73
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
74
|
+
$stdout.puts %(#{opts}
|
75
|
+
Patterns are strings used to create Ruby Regexps.
|
76
|
+
|
77
|
+
Input YAML file is expected to be dot_json2diagrammatron output.
|
78
|
+
|
79
|
+
Output is the input file with nodes that have labels that match patterns
|
80
|
+
removed or kept depending on options. Edges to removed nodes are removed.
|
81
|
+
)
|
82
|
+
exit 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
parser.parse! ARGV
|
86
|
+
|
87
|
+
begin
|
88
|
+
patterns = ARGV.map { |p| Regexp.new(p) }
|
89
|
+
rescue StandardError => e
|
90
|
+
return aargh("Error creating Regexp: #{e}", 1)
|
91
|
+
end
|
92
|
+
doc = load_source(input)
|
93
|
+
return 2 if doc.nil?
|
94
|
+
|
95
|
+
begin
|
96
|
+
prune(doc, patterns, keep)
|
97
|
+
rescue StandardError
|
98
|
+
return aargh('Error processing input.', 3)
|
99
|
+
end
|
100
|
+
|
101
|
+
begin
|
102
|
+
d = YAML.dump(doc, line_width: 1000000)
|
103
|
+
if output.nil?
|
104
|
+
$stdout.puts d
|
105
|
+
else
|
106
|
+
fp = Pathname.new output
|
107
|
+
fp.open('w') do |f|
|
108
|
+
f.puts d
|
109
|
+
end
|
110
|
+
end
|
111
|
+
rescue StandardError => e
|
112
|
+
return aargh("#{e}\nFailed to write output: #{output || 'stdout'}", 4)
|
113
|
+
end
|
114
|
+
0
|
115
|
+
end
|
116
|
+
|
117
|
+
exit(main) if (defined? $unit_test).nil?
|