diagrammatron 0.4.2 → 0.5.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.
- checksums.yaml +4 -4
- data/bin/diagrammatron-edges +100 -136
- data/bin/diagrammatron-get +4 -5
- data/bin/diagrammatron-nodes +18 -40
- data/bin/diagrammatron-place +20 -21
- data/bin/diagrammatron-prune +15 -7
- data/bin/diagrammatron-render +65 -17
- data/bin/diagrammatron-schema +107 -0
- data/bin/diagrammatron-subset +188 -0
- data/bin/dot_json2diagrammatron +14 -25
- data/lib/common.rb +50 -2
- data/lib/edges.yaml +45 -0
- data/lib/nodes.yaml +36 -0
- data/lib/place.yaml +65 -0
- data/lib/render.yaml +111 -0
- data/lib/subset.yaml +55 -0
- data/template/internal.yaml +3 -1
- data/template/root.yaml +2 -0
- data/template/svg_1.1.erb +39 -36
- metadata +36 -6
data/bin/diagrammatron-prune
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2021-2023 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
7
|
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
|
-
require 'yaml'
|
10
9
|
require 'set'
|
11
|
-
require 'pathname'
|
12
10
|
|
13
11
|
|
14
12
|
def prune_array(original, matching, keep)
|
@@ -26,7 +24,12 @@ def prune(doc, patterns, keep)
|
|
26
24
|
nodes = doc.fetch('nodes', [])
|
27
25
|
matching = Array.new(nodes.size, false)
|
28
26
|
nodes.each_index do |k|
|
29
|
-
label = nodes[k].fetch('label',
|
27
|
+
label = nodes[k].fetch('label', nil)
|
28
|
+
if label.nil?
|
29
|
+
# Schemas require 'label' to be present.
|
30
|
+
matching[k] = !keep
|
31
|
+
next
|
32
|
+
end
|
30
33
|
patterns.each do |p|
|
31
34
|
next unless p.match(label)
|
32
35
|
matching[k] = true
|
@@ -40,10 +43,12 @@ def prune(doc, patterns, keep)
|
|
40
43
|
edges.each_index do |k|
|
41
44
|
between = edges[k].fetch('between', [])
|
42
45
|
if between.size != 2
|
43
|
-
matching[k] = keep #
|
46
|
+
matching[k] = !keep # Schemas require between size to be 2.
|
44
47
|
elsif keep
|
48
|
+
# Both ends matched reversed remove rule, keep the edge.
|
45
49
|
matching[k] = (labels.member?(between[0]) && labels.member?(between[1]))
|
46
50
|
else
|
51
|
+
# If either end matched remove rule, remove the edge.
|
47
52
|
matching[k] = (labels.member?(between[0]) || labels.member?(between[1]))
|
48
53
|
end
|
49
54
|
end
|
@@ -53,6 +58,7 @@ end
|
|
53
58
|
def main
|
54
59
|
input = nil
|
55
60
|
output = nil
|
61
|
+
input_output_schema = 'nodes'
|
56
62
|
keep = false
|
57
63
|
ENV['POSIXLY_CORRECT'] = '1' # Leaves patterns as they are.
|
58
64
|
parser = OptionParser.new do |opts|
|
@@ -75,6 +81,8 @@ def main
|
|
75
81
|
Patterns are strings used to create Ruby Regexps.
|
76
82
|
|
77
83
|
Input YAML file is expected to be dot_json2diagrammatron output.
|
84
|
+
Input and output YAML file schema is returned by:
|
85
|
+
diagrammatron-schema #{input_output_schema}
|
78
86
|
|
79
87
|
Output is the input file with nodes that have labels that match patterns
|
80
88
|
removed or kept depending on options. Edges to removed nodes are removed.
|
@@ -89,7 +97,7 @@ removed or kept depending on options. Edges to removed nodes are removed.
|
|
89
97
|
rescue StandardError => e
|
90
98
|
return aargh("Error creating Regexp: #{e}", 1)
|
91
99
|
end
|
92
|
-
doc =
|
100
|
+
doc = load_verified(input, input_output_schema)
|
93
101
|
return 2 if doc.nil?
|
94
102
|
|
95
103
|
begin
|
@@ -98,7 +106,7 @@ removed or kept depending on options. Edges to removed nodes are removed.
|
|
98
106
|
return aargh('Error processing input.', 3)
|
99
107
|
end
|
100
108
|
|
101
|
-
|
109
|
+
save_verified(output, doc, 4, input_output_schema)
|
102
110
|
end
|
103
111
|
|
104
112
|
exit(main) if (defined? $unit_test).nil?
|
data/bin/diagrammatron-render
CHANGED
@@ -61,8 +61,7 @@ def separate_coordinates(doc)
|
|
61
61
|
push_coords(xcoords, ycoords, ckd2count, node.clone, 1, 1, true)
|
62
62
|
end
|
63
63
|
doc['edges'].each do |edge|
|
64
|
-
path = edge
|
65
|
-
next if path.nil?
|
64
|
+
path = edge['path']
|
66
65
|
xdirection, ydirection = end_directions(path[0], path[1])
|
67
66
|
push_coords(xcoords, ycoords, ckd2count, path[0], xdirection, ydirection)
|
68
67
|
(1...(path.size - 1)).each do |k|
|
@@ -80,9 +79,10 @@ class Styles
|
|
80
79
|
def base_styles(m, styles, group)
|
81
80
|
d = styles.dig(group, 'default')
|
82
81
|
if d.nil?
|
83
|
-
d = m.fetch('default', {})
|
82
|
+
d = m.fetch('default', {}) # No default in styles.
|
84
83
|
else
|
85
84
|
m['default'] = m.fetch('default', {}).merge(d)
|
85
|
+
d = m['default']
|
86
86
|
end
|
87
87
|
styles.fetch(group, {}).each_pair do |name, values|
|
88
88
|
s = d.clone
|
@@ -98,7 +98,7 @@ class Styles
|
|
98
98
|
@d = base_styles(base_styles({}, template_styles, 'diagram'), diagram_styles, 'diagram')
|
99
99
|
end
|
100
100
|
|
101
|
-
def fill(mapping,
|
101
|
+
def fill(mapping, _type_name, item)
|
102
102
|
styles = item.fetch('style', [ 'default' ])
|
103
103
|
styles = [ styles ] unless styles.is_a?(Array)
|
104
104
|
s = {}
|
@@ -111,7 +111,7 @@ class Styles
|
|
111
111
|
end
|
112
112
|
s.merge!(mapping['default']) unless found # Merge default at least.
|
113
113
|
# Keep values specified explicitly.
|
114
|
-
item.merge!(s) { |
|
114
|
+
item.merge!(s) { |_key, existing, from_template| existing || from_template }
|
115
115
|
end
|
116
116
|
|
117
117
|
def apply_node_styles(node)
|
@@ -168,7 +168,7 @@ def estimate_sizes(doc, ckd2count)
|
|
168
168
|
$render = SizeEstimation.new(ckd2count, doc)
|
169
169
|
doc['nodes'].each do |node|
|
170
170
|
$render.node = node
|
171
|
-
label = node
|
171
|
+
label = node['label']
|
172
172
|
style = node.fetch('style', 'default')
|
173
173
|
code = node.fetch('size_estimator',
|
174
174
|
%(raise NotImplementedError, "No size estimator for style: #{style}"))
|
@@ -186,7 +186,7 @@ end
|
|
186
186
|
def maxima(doc)
|
187
187
|
xmax = Hash.new(0)
|
188
188
|
ymax = Hash.new(0)
|
189
|
-
doc
|
189
|
+
doc['nodes'].each do |node|
|
190
190
|
xmax[node['xo']] = [ node['w'], xmax[node['xo']] ].max
|
191
191
|
ymax[node['yo']] = [ node['h'], ymax[node['yo']] ].max
|
192
192
|
end
|
@@ -194,7 +194,7 @@ def maxima(doc)
|
|
194
194
|
end
|
195
195
|
|
196
196
|
def apply_maxima(doc, xmax, ymax)
|
197
|
-
doc
|
197
|
+
doc['nodes'].each do |node|
|
198
198
|
node['w'] = xmax[node['xo']]
|
199
199
|
node['h'] = ymax[node['yo']]
|
200
200
|
end
|
@@ -257,13 +257,12 @@ class Render
|
|
257
257
|
def dimensions
|
258
258
|
w = 0
|
259
259
|
h = 0
|
260
|
-
@doc
|
260
|
+
@doc['nodes'].each do |node|
|
261
261
|
w = [ w, node['xo'] + node['w'] ].max
|
262
262
|
h = [ h, node['yo'] + node['h'] ].max
|
263
263
|
end
|
264
|
-
@doc
|
265
|
-
path = edge
|
266
|
-
next if path.nil?
|
264
|
+
@doc['edges'].each do |edge|
|
265
|
+
path = edge['path']
|
267
266
|
path.each do |p|
|
268
267
|
w = [ w, p['xo'] ].max
|
269
268
|
h = [ h, p['yo'] ].max
|
@@ -280,9 +279,31 @@ def apply(doc, template)
|
|
280
279
|
out
|
281
280
|
end
|
282
281
|
|
282
|
+
def reverse_depth_order(a, b)
|
283
|
+
d = b['depth'] <=> a['depth']
|
284
|
+
return d unless d.zero?
|
285
|
+
d = a['index'] <=> b['index']
|
286
|
+
return d unless d.zero?
|
287
|
+
a['kind'] <=> b['kind']
|
288
|
+
end
|
289
|
+
|
290
|
+
def reverse_depth_sort(items)
|
291
|
+
arr = []
|
292
|
+
items.each_index do |k|
|
293
|
+
arr.push({
|
294
|
+
'depth' => items[k].fetch('depth', 0),
|
295
|
+
'index' => k,
|
296
|
+
'kind' => items[k].fetch('kind', 'unknown')
|
297
|
+
})
|
298
|
+
end
|
299
|
+
arr.sort! { |a, b| reverse_depth_order(a, b) }
|
300
|
+
arr.map { |x| items[x['index']] }
|
301
|
+
end
|
302
|
+
|
283
303
|
def main
|
284
304
|
template = nil
|
285
305
|
input = nil
|
306
|
+
input_schema = 'render'
|
286
307
|
output = nil
|
287
308
|
styles = nil
|
288
309
|
parser = OptionParser.new do |opts|
|
@@ -302,7 +323,10 @@ def main
|
|
302
323
|
end
|
303
324
|
opts.on('-h', '--help', 'Print this help and exit.') do
|
304
325
|
$stdout.puts %(#{opts}
|
305
|
-
Input YAML file is
|
326
|
+
Input YAML file schema is returned by:
|
327
|
+
diagrammatron-schema #{input_schema}
|
328
|
+
|
329
|
+
There can be other fields present as needed by the template.
|
306
330
|
|
307
331
|
Output is the file produced by the erb-template.
|
308
332
|
)
|
@@ -325,15 +349,15 @@ Output is the file produced by the erb-template.
|
|
325
349
|
end
|
326
350
|
end
|
327
351
|
|
328
|
-
doc =
|
352
|
+
doc = load_verified(input, input_schema)
|
329
353
|
return 2 if doc.nil?
|
330
354
|
|
331
355
|
styles = Styles.new(template.fetch('styles', {}), doc.fetch('styles', {}))
|
332
|
-
doc
|
356
|
+
doc['nodes'].each do |node|
|
333
357
|
styles.apply_node_styles(node)
|
334
|
-
node['text'] = node.fetch('text', node
|
358
|
+
node['text'] = node.fetch('text', node['label']).split("\n")
|
335
359
|
end
|
336
|
-
doc
|
360
|
+
doc['edges'].each { |edge| styles.apply_edge_styles(edge) }
|
337
361
|
doc['diagram'] = {} unless doc.key? 'diagram'
|
338
362
|
styles.apply_diagram_styles(doc['diagram'])
|
339
363
|
|
@@ -354,6 +378,30 @@ Output is the file produced by the erb-template.
|
|
354
378
|
remap_coordinates(xcoords, xmax, x2min, doc.dig('diagram', 'edge_gap'))
|
355
379
|
remap_coordinates(ycoords, ymax, y2min, doc.dig('diagram', 'edge_gap'))
|
356
380
|
|
381
|
+
doc['nodes'] = reverse_depth_sort(doc['nodes'])
|
382
|
+
doc['edges'] = reverse_depth_sort(doc['edges'])
|
383
|
+
all = doc['nodes'].map do |a|
|
384
|
+
{
|
385
|
+
'kind' => 'node',
|
386
|
+
'depth' => a.fetch('depth', 0),
|
387
|
+
'item' => a
|
388
|
+
}
|
389
|
+
end
|
390
|
+
all.concat(doc['edges'].map do |a|
|
391
|
+
{
|
392
|
+
'kind' => 'edge',
|
393
|
+
'depth' => a.fetch('depth', 0),
|
394
|
+
'item' => a
|
395
|
+
}
|
396
|
+
end)
|
397
|
+
all = reverse_depth_sort(all)
|
398
|
+
doc['all'] = all.map do |x|
|
399
|
+
{
|
400
|
+
'kind' => x['kind'],
|
401
|
+
'item' => x['item']
|
402
|
+
}
|
403
|
+
end
|
404
|
+
|
357
405
|
dump_result(output, apply(doc, template), 5)
|
358
406
|
end
|
359
407
|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Copyright © 2023 Ismo Kärkkäinen
|
5
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
|
+
|
7
|
+
require_relative '../lib/common'
|
8
|
+
require 'optparse'
|
9
|
+
require 'yaml'
|
10
|
+
require 'json'
|
11
|
+
|
12
|
+
def convert(result, format)
|
13
|
+
case format
|
14
|
+
when :json then JSON.generate(result)
|
15
|
+
when :yaml then YAML.dump(result)
|
16
|
+
when :prettyjson then JSON.pretty_generate(result)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def main
|
21
|
+
output = nil
|
22
|
+
format = :json
|
23
|
+
package = :bare
|
24
|
+
ENV['POSIXLY_CORRECT'] = '1' # Leaves patterns as they are.
|
25
|
+
parser = OptionParser.new do |opts|
|
26
|
+
opts.summary_indent = ' '
|
27
|
+
opts.summary_width = 20
|
28
|
+
opts.banner = 'Usage: diagrammatron-schema [options] schema-name ...'
|
29
|
+
opts.separator ''
|
30
|
+
opts.separator 'Options:'
|
31
|
+
opts.on('-f', '--format FMT', 'Output schema format (json, yaml, pretty).') do |fmt|
|
32
|
+
case fmt.upcase
|
33
|
+
when 'JSON' then format = :json
|
34
|
+
when 'YAML' then format = :yaml
|
35
|
+
when 'PRETTY' then format = :prettyjson
|
36
|
+
else
|
37
|
+
format = fmt
|
38
|
+
end
|
39
|
+
end
|
40
|
+
opts.on('-p', '--package PKG', 'Wrap schema(s) in given way.') do |pkg|
|
41
|
+
case pkg.upcase
|
42
|
+
when 'OBJECT' then package = :object
|
43
|
+
when 'ARRAY' then package = :array
|
44
|
+
when 'BARE' then package = :bare
|
45
|
+
else
|
46
|
+
package = pkg
|
47
|
+
end
|
48
|
+
end
|
49
|
+
opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
|
50
|
+
output = filename
|
51
|
+
end
|
52
|
+
opts.on('-l', '--list', 'List available schemas and exit.') do
|
53
|
+
$stdout.puts list_schemas.join("\n")
|
54
|
+
exit 0
|
55
|
+
end
|
56
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
57
|
+
$stdout.puts %(#{opts}
|
58
|
+
Outputs the given schema files.
|
59
|
+
Formats are: JSON, YAML, pretty for prettified JSON.
|
60
|
+
Packages are:
|
61
|
+
object: Output schemas as an object with name to schema mapping.
|
62
|
+
array: Output schemas as an array in the order given on the command-line
|
63
|
+
bare: Output schemas in the specified order as they are. The default.
|
64
|
+
)
|
65
|
+
exit 0
|
66
|
+
end
|
67
|
+
end
|
68
|
+
parser.parse! ARGV
|
69
|
+
|
70
|
+
aargh("Unknown format: #{format}", 1) unless format.is_a? Symbol
|
71
|
+
case package
|
72
|
+
when :bare, :array then out = []
|
73
|
+
when :object then out = {}
|
74
|
+
else
|
75
|
+
return aargh("Unknown package: #{package}", 1) unless package.is_a? Symbol
|
76
|
+
end
|
77
|
+
|
78
|
+
ARGV.each do |name|
|
79
|
+
s = load_schema(name)
|
80
|
+
if out.is_a? Array
|
81
|
+
out.push(s)
|
82
|
+
else
|
83
|
+
out[name] = s
|
84
|
+
end
|
85
|
+
rescue Errno::ENOENT
|
86
|
+
aargh "No such schema: #{name}", 2
|
87
|
+
rescue StandardError => e
|
88
|
+
aargh "#{e}\nInternal error: #{name}", 3
|
89
|
+
end
|
90
|
+
|
91
|
+
if output.nil?
|
92
|
+
fp = $stdout
|
93
|
+
else
|
94
|
+
fp = File.new(output, 'w')
|
95
|
+
end
|
96
|
+
if package == :bare
|
97
|
+
out.each do |item|
|
98
|
+
fp.puts(convert(item, format))
|
99
|
+
end
|
100
|
+
else
|
101
|
+
fp.puts(convert(out, format))
|
102
|
+
end
|
103
|
+
fp.close
|
104
|
+
0
|
105
|
+
end
|
106
|
+
|
107
|
+
exit(main) if (defined? $unit_test).nil?
|
@@ -0,0 +1,188 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Copyright © 2023 Ismo Kärkkäinen
|
5
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
|
+
|
7
|
+
require_relative '../lib/common'
|
8
|
+
require_relative '../lib/subsets'
|
9
|
+
require 'optparse'
|
10
|
+
require 'set'
|
11
|
+
|
12
|
+
|
13
|
+
class Item
|
14
|
+
@@counter = 0
|
15
|
+
attr_reader :item, :ids
|
16
|
+
|
17
|
+
def initialize(item, label2id = {})
|
18
|
+
@item = item
|
19
|
+
@ids = [ @@counter ]
|
20
|
+
@@counter += 1
|
21
|
+
# This silently removes edges that have no valid end-point.
|
22
|
+
item.fetch('between', []).each { |x| @ids.push(label2id.fetch(x, -1)) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.reset
|
26
|
+
@@counter = 0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def work_copy(src)
|
31
|
+
nodes = src.fetch('nodes', []).map { |n| Item.new(n) }
|
32
|
+
label2id = {}
|
33
|
+
nodes.each { |x| label2id[x.item['label']] = x.ids.first }
|
34
|
+
edges = src.fetch('edges', []).map { |e| Item.new(e, label2id) }
|
35
|
+
{
|
36
|
+
edges: edges,
|
37
|
+
nodes: nodes
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def prepare_output(doc, work, chosen)
|
42
|
+
%i[edges nodes].each do |s|
|
43
|
+
work[s].select! do |x|
|
44
|
+
(x.ids.index { |id| !chosen.member?(id) }).nil?
|
45
|
+
end
|
46
|
+
doc[s.to_s] = work[s].map(&:item)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def gather_set(work, rules, current)
|
51
|
+
r = rules['sets'].fetch(current, nil)
|
52
|
+
return aargh("Undefined set: #{current}", 9) if r.nil?
|
53
|
+
result = Set.new
|
54
|
+
%i[nodes edges].each do |category|
|
55
|
+
work[category].each do |item|
|
56
|
+
if match_item(item.item, r.fetch(category, {}))
|
57
|
+
result.add(item.ids.first)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
def gather(work, rules, current, above, set_cache)
|
65
|
+
e = rules['sets'].fetch(current, nil)
|
66
|
+
unless e.nil?
|
67
|
+
set = set_cache.fetch(current, nil)
|
68
|
+
return set unless set.nil?
|
69
|
+
# Evaluate needed set, store, and return it.
|
70
|
+
set = gather_set(work, rules, current)
|
71
|
+
set_cache[current] = set # Error may end up in cache, not a problem.
|
72
|
+
return set
|
73
|
+
end
|
74
|
+
return aargh("Loop in expressions for #{current}: #{above.reverse.join("\n")}", 8) if above.include?(current)
|
75
|
+
e = rules['expressions'].fetch(current, nil)
|
76
|
+
return aargh("Undefined expression: #{current}", 7) if e.nil?
|
77
|
+
above.push(current)
|
78
|
+
result = gather(work, rules, e.first, above, set_cache)
|
79
|
+
return result unless result.is_a?(Set)
|
80
|
+
(1...e.size).step(2) do |k|
|
81
|
+
op = e[k]
|
82
|
+
id = e[k + 1]
|
83
|
+
s = gather(work, rules, id, above, set_cache)
|
84
|
+
return s unless s.is_a?(Set)
|
85
|
+
case op
|
86
|
+
when :plus then result += s
|
87
|
+
when :minus then result -= s
|
88
|
+
else
|
89
|
+
return aargh("Unsupported operator: #{op}", 100)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
above.pop
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
def main
|
97
|
+
input_output_schema = 'nodes'
|
98
|
+
input = nil
|
99
|
+
output = nil
|
100
|
+
selected = nil
|
101
|
+
ENV['POSIXLY_CORRECT'] = '1'
|
102
|
+
parser = OptionParser.new do |opts|
|
103
|
+
opts.summary_indent = ' '
|
104
|
+
opts.summary_width = 20
|
105
|
+
opts.banner = 'Usage: diagrammatron-subset [options] rule-file-names...'
|
106
|
+
opts.separator ''
|
107
|
+
opts.separator 'Options:'
|
108
|
+
opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
|
109
|
+
input = filename
|
110
|
+
end
|
111
|
+
opts.on('-o', '--output FILE', 'Output file name. Written to stdout if not given.') do |filename|
|
112
|
+
output = filename
|
113
|
+
end
|
114
|
+
opts.on('-s', '--select NAME', 'Select expression or subset NAME result.') do |name|
|
115
|
+
selected = name
|
116
|
+
end
|
117
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
118
|
+
$stdout.puts %(#{opts}
|
119
|
+
Rule files can specify named subset selection rules and named expressions on
|
120
|
+
how to combine the subsets when determining which nodes and edges will form the
|
121
|
+
diagram.
|
122
|
+
|
123
|
+
Subset selection rules can apply to nodes only, edges only, or both. The rules
|
124
|
+
give for a field name an array of strings or patterns. If a node or edge
|
125
|
+
matches any of the rules, it belongs to the subset.
|
126
|
+
|
127
|
+
Expressions can refer to the subset or expression names and use operations to
|
128
|
+
form more complex subsets. In the simplest form, they just are a subset name.
|
129
|
+
Operations are a + b to add elements of b and a - b to drop elements of b. If
|
130
|
+
expression and subset have the same name, identifier refers to subset.
|
131
|
+
|
132
|
+
The select option gives the name of the expression or subset to use.
|
133
|
+
|
134
|
+
Edges to removed nodes are removed. Nodes with all edges removed are retained.
|
135
|
+
Output is the input file with remaining nodes and edges.
|
136
|
+
|
137
|
+
Input and output YAML file schema is returned by:
|
138
|
+
diagrammatron-schema #{input_output_schema}
|
139
|
+
|
140
|
+
Rule YAML file schema is returned by:
|
141
|
+
diagrammatron-schema subset
|
142
|
+
|
143
|
+
Note that input schema is minimal and does not restrict running this program
|
144
|
+
at a later stage in the pipeline after other tools have added their fields.
|
145
|
+
You can refer to fields outside the schema.
|
146
|
+
)
|
147
|
+
exit 0
|
148
|
+
end
|
149
|
+
end
|
150
|
+
parser.parse! ARGV
|
151
|
+
|
152
|
+
if selected.nil?
|
153
|
+
return aargh('Must select an expression or subset to use for output.', 2)
|
154
|
+
end
|
155
|
+
|
156
|
+
rules = {}
|
157
|
+
ARGV.each do |filename|
|
158
|
+
r = load_verified(filename, 'subset')
|
159
|
+
return 5 if r.nil?
|
160
|
+
return 6 unless check_rules(r, filename)
|
161
|
+
merge_any(r) # The 'any' field exists to avoid duplicating rules by hand.
|
162
|
+
merge_rules(rules, r)
|
163
|
+
end
|
164
|
+
|
165
|
+
if rules.fetch('sets', {}).key? selected
|
166
|
+
# Refers directly to subset so other expressions do not matter.
|
167
|
+
rules['expressions'] = { selected => selected }
|
168
|
+
elsif !rules.fetch('expressions', {}).key? selected
|
169
|
+
return aargh("Selected #{selected} is not expression or subset name.", 2)
|
170
|
+
end
|
171
|
+
|
172
|
+
doc = load_verified(input, input_output_schema)
|
173
|
+
return 2 if doc.nil?
|
174
|
+
|
175
|
+
begin
|
176
|
+
work = work_copy(doc)
|
177
|
+
return 3 if work.nil?
|
178
|
+
rescue StandardError
|
179
|
+
return aargh('Error processing input.', 3)
|
180
|
+
end
|
181
|
+
|
182
|
+
chosen = gather(work, rules, selected, [], {})
|
183
|
+
return chosen unless chosen.is_a?(Set)
|
184
|
+
prepare_output(doc, work, chosen)
|
185
|
+
save_verified(output, doc, 4, input_output_schema)
|
186
|
+
end
|
187
|
+
|
188
|
+
exit(main) if (defined? $unit_test).nil?
|
data/bin/dot_json2diagrammatron
CHANGED
@@ -1,14 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021
|
4
|
+
# Copyright © 2021-2023 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
7
|
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
|
-
require 'yaml'
|
10
|
-
require 'json'
|
11
|
-
require 'pathname'
|
12
9
|
|
13
10
|
|
14
11
|
def convert(src)
|
@@ -63,10 +60,10 @@ def convert(src)
|
|
63
60
|
{ 'edges' => edges, 'nodes' => nodes }
|
64
61
|
end
|
65
62
|
|
66
|
-
$INPUT = nil
|
67
|
-
$OUTPUT = nil
|
68
|
-
|
69
63
|
def main
|
64
|
+
input = nil
|
65
|
+
output = nil
|
66
|
+
output_schema = 'nodes'
|
70
67
|
parser = OptionParser.new do |opts|
|
71
68
|
opts.summary_indent = ' '
|
72
69
|
opts.summary_width = 20
|
@@ -74,24 +71,28 @@ def main
|
|
74
71
|
opts.separator ''
|
75
72
|
opts.separator 'Options:'
|
76
73
|
opts.on('-i', '--input FILE', 'Input file name. Read from stdin if not given.') do |filename|
|
77
|
-
|
74
|
+
input = filename
|
78
75
|
end
|
79
76
|
opts.on('-o', '--output FILE', 'Output file name. Write to stdout if not given.') do |filename|
|
80
|
-
|
77
|
+
output = filename
|
81
78
|
end
|
82
79
|
opts.on('-h', '--help', 'Print this help and exit.') do
|
83
80
|
$stdout.puts opts
|
84
81
|
$stdout.puts %(
|
85
82
|
Output is a YAML file with "name" used as "label" and "shape" used as is
|
86
|
-
for nodes. For edges "tail" and "head" are used as the first and second
|
87
|
-
of "between" with the "name" as the identifier. Compound objects
|
83
|
+
for nodes. For edges "tail" and "head" are used as the first and second
|
84
|
+
element of "between" with the "name" as the identifier. Compound objects
|
85
|
+
are ignored.
|
86
|
+
|
87
|
+
Output YAML file schema is returned by:
|
88
|
+
diagrammatron-schema #{output_schema}
|
88
89
|
)
|
89
90
|
exit 0
|
90
91
|
end
|
91
92
|
end
|
92
93
|
parser.parse! ARGV
|
93
94
|
|
94
|
-
doc = load_source
|
95
|
+
doc = load_source(input)
|
95
96
|
exit(2) if doc.nil?
|
96
97
|
|
97
98
|
begin
|
@@ -101,19 +102,7 @@ of "between" with the "name" as the identifier. Compound objects are ignored.
|
|
101
102
|
end
|
102
103
|
exit(3) if out.nil?
|
103
104
|
|
104
|
-
|
105
|
-
if $OUTPUT.nil?
|
106
|
-
$stdout.puts YAML.dump(out)
|
107
|
-
else
|
108
|
-
fp = Pathname.new $OUTPUT
|
109
|
-
fp.open('w') do |f|
|
110
|
-
f.puts YAML.dump(out)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
rescue StandardError => e
|
114
|
-
aargh("#{e}\nFailed to write output: #{$OUTPUT}")
|
115
|
-
exit 4
|
116
|
-
end
|
105
|
+
save_verified(output, pout, 4, output_schema)
|
117
106
|
end
|
118
107
|
|
119
108
|
main if (defined? $unit_test).nil?
|
data/lib/common.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright © 2021
|
3
|
+
# Copyright © 2021-2023 Ismo Kärkkäinen
|
4
4
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
5
|
|
6
|
+
require 'json_schemer'
|
7
|
+
require 'yaml'
|
8
|
+
require 'set'
|
9
|
+
|
10
|
+
|
6
11
|
def aargh(message, return_value = nil)
|
7
12
|
message = (message.map &(:to_s)).join("\n") if message.is_a? Array
|
8
13
|
$stderr.puts message
|
@@ -20,9 +25,41 @@ end
|
|
20
25
|
def load_source_hash(input)
|
21
26
|
src = load_source(input)
|
22
27
|
unless src.nil?
|
23
|
-
return aargh(
|
28
|
+
return aargh("#{input} is not a simple mapping.") unless src.is_a? Hash
|
29
|
+
end
|
30
|
+
src
|
31
|
+
end
|
32
|
+
|
33
|
+
def list_schemas
|
34
|
+
(Dir.entries(File.dirname(__FILE__)).select { |name| name.upcase.end_with?('.YAML') }).map { |name| name[0, name.size - 5] }
|
35
|
+
end
|
36
|
+
|
37
|
+
def load_schema(schema_name)
|
38
|
+
YAML.safe_load_file(File.join(File.dirname(__FILE__), "#{schema_name}.yaml"))
|
39
|
+
end
|
40
|
+
|
41
|
+
def make_schemer(schema, reading)
|
42
|
+
JSONSchemer.schema(JSON.generate(schema),
|
43
|
+
meta_schema: 'https://json-schema.org/draft/2020-12/schema',
|
44
|
+
insert_property_defaults: reading)
|
45
|
+
end
|
46
|
+
|
47
|
+
def load_verified(input, schema_name)
|
48
|
+
src = load_source(input)
|
49
|
+
unless src.nil?
|
50
|
+
s = load_schema(schema_name)
|
51
|
+
schemer = make_schemer(s, true)
|
52
|
+
errs = schemer.validate(src).to_a
|
53
|
+
unless errs.empty?
|
54
|
+
aargh (errs.map { |e| e['error'] }).join("\n")
|
55
|
+
src = nil
|
56
|
+
end
|
24
57
|
end
|
25
58
|
src
|
59
|
+
rescue Errno::ENOENT
|
60
|
+
aargh "Could not load #{schema_name}"
|
61
|
+
rescue StandardError => e
|
62
|
+
aargh "Internal error: #{e}\n#{e.backtrace.join("\n")}\nFailed to read #{schema_name}"
|
26
63
|
end
|
27
64
|
|
28
65
|
def dump_result(output, doc, error_return)
|
@@ -38,3 +75,14 @@ def dump_result(output, doc, error_return)
|
|
38
75
|
rescue StandardError => e
|
39
76
|
aargh([ e, "Failed to write output: #{output || 'stdout'}" ], error_return)
|
40
77
|
end
|
78
|
+
|
79
|
+
def save_verified(output, doc, error_return, schema_name)
|
80
|
+
s = load_schema(schema_name)
|
81
|
+
schemer = make_schemer(s, false)
|
82
|
+
errs = schemer.validate(doc).to_a
|
83
|
+
unless errs.empty?
|
84
|
+
aargh (errs.map { |e| e['error'] }).join("\n")
|
85
|
+
return error_return
|
86
|
+
end
|
87
|
+
dump_result(output, YAML.dump(doc, line_width: 1_000_000), error_return)
|
88
|
+
end
|