diagrammatron 0.2.2 → 0.4.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 +96 -59
- data/bin/diagrammatron-get +4 -16
- data/bin/diagrammatron-nodes +4 -17
- data/bin/diagrammatron-place +10 -23
- data/bin/diagrammatron-prune +2 -15
- data/bin/diagrammatron-render +102 -104
- data/bin/diagrammatron-template +4 -17
- data/bin/dot_json2diagrammatron +2 -19
- data/lib/common.rb +15 -1
- data/template/internal.yaml +31 -20
- data/template/root.yaml +30 -19
- data/template/svg_1.1.erb +16 -21
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 32bd6ca5ad8db808b0b6c921ee0efe5b8e1a6befe4e535636f16ff98a22de139
|
4
|
+
data.tar.gz: c00027483fc54a9fab024c1e42a4e10e27b797e52e3a9217319a50542926a894
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7723bc76dfd7463a864319421074d8c17357f6c88a309efd81919c165ad836f876d48e062bf618b45e2b7fe779b1c564afaedfa6b105ae1fee9f84e5f52003d5
|
7
|
+
data.tar.gz: ed23e22fd6476e33afb4360cce23cefb0373dbd2c763611d9f61d0a20a283fe2dcd7a4bac6f137d628020db86819aacdea6387c889e5821a3ed4b958a5a2a736
|
data/bin/diagrammatron-edges
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2021, 2022 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common
|
7
|
+
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
9
|
require 'yaml'
|
10
10
|
require 'set'
|
@@ -68,7 +68,7 @@ def work_copy(src, quiet)
|
|
68
68
|
next
|
69
69
|
end
|
70
70
|
label = node['label']
|
71
|
-
if label2idx.key?(label) && edge_nodes.
|
71
|
+
if label2idx.key?(label) && edge_nodes.member?(label)
|
72
72
|
aargh "Edge-referred label used twice: #{label}"
|
73
73
|
errors = true
|
74
74
|
end
|
@@ -109,7 +109,8 @@ def edge_subsets(work)
|
|
109
109
|
end
|
110
110
|
|
111
111
|
Segment = Struct.new(:vertical, :cc, :range, :edge_index, :at_node, :segment_index, :offset) do
|
112
|
-
|
112
|
+
# To decreasing or increasing coordinates.
|
113
|
+
def direction(s)
|
113
114
|
return 0 if s.nil?
|
114
115
|
if segment_index < s.segment_index
|
115
116
|
(cc < s.range[1]) ? 1 : -1
|
@@ -119,7 +120,7 @@ Segment = Struct.new(:vertical, :cc, :range, :edge_index, :at_node, :segment_ind
|
|
119
120
|
end
|
120
121
|
|
121
122
|
def over_other_node(work, node_subset)
|
122
|
-
ck, rk = vertical ? [
|
123
|
+
ck, rk = vertical ? %i[xo yo] : %i[yo xo]
|
123
124
|
i0, i1 = (range[0] < range[1]) ? [0, 1] : [1, 0]
|
124
125
|
node_subset.each do |n|
|
125
126
|
node = work[:nodes][n]
|
@@ -172,8 +173,7 @@ def segment(x0, y0, x1, y1)
|
|
172
173
|
Segment.new(vert, cc, range, 0, [false, false], 0)
|
173
174
|
end
|
174
175
|
|
175
|
-
Connection = Struct.new(:node_index, :side_index)
|
176
|
-
end
|
176
|
+
Connection = Struct.new(:node_index, :side_index)
|
177
177
|
|
178
178
|
$paths = {}
|
179
179
|
Path = Struct.new(:edge_index, :ends, :segments, :id, :crosses, :steps) do
|
@@ -424,63 +424,111 @@ def segment_order(a, b)
|
|
424
424
|
return d unless d.zero?
|
425
425
|
d = a[2].range.min <=> b[2].range.min
|
426
426
|
return d unless d.zero?
|
427
|
-
when 1 #
|
428
|
-
d = a[2].range.min <=> b[2].range.min #
|
427
|
+
when 1 # Top left, vertical, bottom right: -|_
|
428
|
+
d = a[2].range.min <=> b[2].range.min # Ascend on minimum.
|
429
|
+
return d unless d.zero?
|
430
|
+
d = a[2].length <=> b[2].length # Ascend on length.
|
429
431
|
return d unless d.zero?
|
430
432
|
when 2 # From top right, down, bottom left: _|-
|
431
|
-
d = a[2].range.
|
433
|
+
d = a[2].range.max <=> b[2].range.max # Descend on maximum.
|
432
434
|
return -d unless d.zero?
|
433
|
-
|
434
|
-
d = b[2].length <=> a[2].length
|
435
|
-
return d unless d.zero?
|
436
|
-
d = b[2].range.max <=> a[2].range.max
|
435
|
+
d = a[2].length <=> b[2].length # Ascend on length.
|
437
436
|
return d unless d.zero?
|
437
|
+
when 3 # Descending on length, range maximum. |=
|
438
|
+
d = a[2].length <=> b[2].length
|
439
|
+
return -d unless d.zero?
|
440
|
+
d = a[2].range.max <=> b[2].range.max
|
441
|
+
return -d unless d.zero?
|
438
442
|
end
|
439
443
|
a[0].edge_index <=> b[0].edge_index
|
440
444
|
end
|
441
445
|
|
442
|
-
GapState = Struct.new(:order, :
|
446
|
+
GapState = Struct.new(:order, :cross_count, :lut) do
|
443
447
|
def fitness(segments, k)
|
448
|
+
table = lut[k]
|
444
449
|
crossings = 0
|
445
|
-
s = segments[k]
|
446
|
-
prev = (s[1] == 1) ? s[2].range.max : s[2].range.min
|
447
450
|
order.each do |n|
|
448
|
-
|
449
|
-
if placed[2].range.min <= prev && prev <= placed[2].range.max
|
450
|
-
crossings += 1
|
451
|
-
end
|
452
|
-
v = (placed[1] == 1) ? placed[2].range.min : placed[2].range.max
|
453
|
-
if s[2].range.min <= v && v <= s[2].range.max
|
454
|
-
crossings += 1
|
455
|
-
end
|
451
|
+
crossings += table[n]
|
456
452
|
end
|
457
|
-
|
453
|
+
cross_count + crossings
|
458
454
|
end
|
459
455
|
end
|
460
456
|
|
461
|
-
def
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
457
|
+
def candidate_compare(a, b)
|
458
|
+
d = a[1] <=> b[1]
|
459
|
+
return d unless d == 0
|
460
|
+
a[0] <=> b[0]
|
461
|
+
end
|
462
|
+
|
463
|
+
def candidate_order(lut, used)
|
464
|
+
# Compute crossing sums for all remaining segments among themselves.
|
465
|
+
cands = []
|
466
|
+
lut.keys.each do |k|
|
467
|
+
next if used.include? k
|
468
|
+
total = 0
|
469
|
+
lut.each_pair do |idx, v|
|
470
|
+
total += v[k] unless used.include? idx
|
470
471
|
end
|
471
|
-
|
472
|
-
|
472
|
+
cands.push([k, total])
|
473
|
+
end
|
474
|
+
cands.sort! { |a, b| candidate_compare(a, b) }
|
475
|
+
cands.map { |a| a[0] }
|
476
|
+
end
|
477
|
+
|
478
|
+
def depth_first_search(segments, state, best)
|
479
|
+
if state.order.size == segments.size
|
480
|
+
$stderr.puts state.order.join(', '), state.cross_count
|
481
|
+
return GapState.new(state.order.clone, state.cross_count, state.lut)
|
482
|
+
end
|
483
|
+
cands = candidate_order(state.lut, state.order)
|
484
|
+
cands.each do |k|
|
485
|
+
c = state.fitness(segments, k)
|
486
|
+
next if (best.nil? ? c + 1 : best.cross_count) <= c
|
487
|
+
state.order.push(k)
|
488
|
+
best = depth_first_search(segments, GapState.new(state.order, c, state.lut), best)
|
489
|
+
state.order.pop
|
473
490
|
end
|
474
491
|
best
|
475
492
|
end
|
476
493
|
|
477
|
-
def zigzag_order(
|
494
|
+
def zigzag_order(from_right_up_to_left, from_left_up_to_right)
|
495
|
+
# Interleave the two sets.
|
496
|
+
out = []
|
497
|
+
from_right_up_to_left.each { |x| out.push x }
|
498
|
+
from_left_up_to_right.each { |x| out.push x }
|
499
|
+
return out
|
478
500
|
return segments if segments.size < 2
|
479
501
|
# DFS. Fitness is how many end segments cross the placed segments.
|
480
502
|
# Sort so that those that cross least when at start are first and vice versa.
|
481
|
-
|
503
|
+
lut = {}
|
482
504
|
segments.each_index do |k|
|
483
|
-
|
505
|
+
crossings = Hash.new(0)
|
506
|
+
lut[k] = crossings
|
507
|
+
s = segments[k]
|
508
|
+
prev = (s[1] == 1) ? s[2].range.max : s[2].range.min
|
509
|
+
segments.each_index do |n|
|
510
|
+
next if k == n
|
511
|
+
other = segments[n]
|
512
|
+
if other[2].range.min <= prev && prev <= other[2].range.max
|
513
|
+
crossings[n] = crossings[n] + 1
|
514
|
+
end
|
515
|
+
v = (other[1] == 1) ? other[2].range.min : other[2].range.max
|
516
|
+
if s[2].range.min <= v && v <= s[2].range.max
|
517
|
+
crossings[n] = crossings[n] + 1
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
$stderr.puts segments.size, lut
|
522
|
+
# Could take all crossings for k, sort, sum up to get minimum fitness for
|
523
|
+
# used set size N ignoring what set has. Since real fitness can not be
|
524
|
+
# lower, once minimal sum + cross_count exceeds best, we can not use that
|
525
|
+
# segment in any later position and can cut off search.
|
526
|
+
# Add to GapState after lut.
|
527
|
+
# Also, coult set time budget for single search and terminate search.
|
528
|
+
best = nil
|
529
|
+
cands = candidate_order(lut, [])
|
530
|
+
cands.each do |k|
|
531
|
+
best = depth_first_search(segments, GapState.new([ k ], 0, lut), best)
|
484
532
|
end
|
485
533
|
out = []
|
486
534
|
best.order.each do |k|
|
@@ -526,6 +574,7 @@ def group_stacked_offsets(group)
|
|
526
574
|
group[k][0].offset = k + 1
|
527
575
|
end
|
528
576
|
return group.size
|
577
|
+
%q(
|
529
578
|
# This produces narrower gaps but they may be less clear.
|
530
579
|
prev = group[0]
|
531
580
|
prev[0].offset = 1
|
@@ -535,6 +584,7 @@ def group_stacked_offsets(group)
|
|
535
584
|
prev = g
|
536
585
|
end
|
537
586
|
prev[0].offset
|
587
|
+
)
|
538
588
|
end
|
539
589
|
|
540
590
|
def direct_range(paths)
|
@@ -549,7 +599,7 @@ def offsets(conn, paths, direct_ranges)
|
|
549
599
|
dlow = [ here[0], opposite[0] ].max
|
550
600
|
dhigh = [ here[2] - here[1], opposite[2] - opposite[1] ].max - 1
|
551
601
|
d = dlow + here[1] - here[0] + dhigh + 1
|
552
|
-
low, high,
|
602
|
+
low, high, _size = here
|
553
603
|
offsets = []
|
554
604
|
(0...low).each do |k|
|
555
605
|
offsets.push(Rational((k + 1) * dlow, (d + 1) * low))
|
@@ -589,7 +639,7 @@ end
|
|
589
639
|
def place_edges(work)
|
590
640
|
subsets = edge_subsets(work)
|
591
641
|
subsets.each_pair do |sid, subset|
|
592
|
-
full =
|
642
|
+
full = {}
|
593
643
|
subset.each do |edge_index|
|
594
644
|
link = work[:edges][edge_index][:between]
|
595
645
|
full[edge_index] = candidates(
|
@@ -651,7 +701,7 @@ def place_edges(work)
|
|
651
701
|
chosen.each_value do |p|
|
652
702
|
(1...(p.segments.size - 1)).each do |k|
|
653
703
|
# Middle segments always have surrounding segments.
|
654
|
-
s,
|
704
|
+
s, below, above, sb, sa = p.segment_directions(k)
|
655
705
|
so = s.clone # More accurate info on actual range with end offsets.
|
656
706
|
if so.range[0] < so.range[1]
|
657
707
|
so.range[0] += sb.offset unless sb.offset.nil?
|
@@ -660,7 +710,7 @@ def place_edges(work)
|
|
660
710
|
so.range[0] += sb.offset.nil? ? 0.9999 : sb.offset
|
661
711
|
so.range[1] += sa.offset unless sa.offset.nil?
|
662
712
|
end
|
663
|
-
group = (
|
713
|
+
group = (below.negative? ? 0 : 1) + (above.negative? ? 0 : 2)
|
664
714
|
d = gaps[s.vertical]
|
665
715
|
d[s.cc] = d.fetch(s.cc, []).push([ s, group, so ])
|
666
716
|
end
|
@@ -670,7 +720,7 @@ def place_edges(work)
|
|
670
720
|
gap.sort! { |a, b| segment_order(a, b) }
|
671
721
|
gleft = gap.select { |a| a[1].zero? }
|
672
722
|
gright = gap.select { |a| a[1] == 3 }
|
673
|
-
gmiddle = zigzag_order(gap.select { |a| a[1] == 1
|
723
|
+
gmiddle = zigzag_order(gap.select() { |a| a[1] == 1 }, gap.select() { |a| a[1] == 2 })
|
674
724
|
gmiddle.each do |s|
|
675
725
|
s[1] = 1
|
676
726
|
end
|
@@ -826,20 +876,7 @@ Input YAML file is expected to be the output of diagrammatron-nodes.
|
|
826
876
|
|
827
877
|
place_edges(work)
|
828
878
|
prepare_output(doc, work)
|
829
|
-
|
830
|
-
d = YAML.dump(doc, line_width: 1000000)
|
831
|
-
if output.nil?
|
832
|
-
$stdout.puts d
|
833
|
-
else
|
834
|
-
fp = Pathname.new output
|
835
|
-
fp.open('w') do |f|
|
836
|
-
f.puts d
|
837
|
-
end
|
838
|
-
end
|
839
|
-
rescue StandardError => e
|
840
|
-
return aargh("#{e}\nFailed to write output: #{output || 'stdout'}", 4)
|
841
|
-
end
|
842
|
-
0
|
879
|
+
dump_result(output, YAML.dump(doc, line_width: 1_000_000), 4)
|
843
880
|
end
|
844
881
|
|
845
882
|
exit(main) if (defined? $unit_test).nil?
|
data/bin/diagrammatron-get
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2021, 2022 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common
|
7
|
+
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
9
|
require 'pathname'
|
10
10
|
|
@@ -16,7 +16,6 @@ def template(name = nil)
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def main
|
19
|
-
input = nil
|
20
19
|
output = nil
|
21
20
|
parser = OptionParser.new do |opts|
|
22
21
|
opts.summary_indent = ' '
|
@@ -48,6 +47,7 @@ Given a name of a included file, saves it to --output.
|
|
48
47
|
next if name.start_with? '.'
|
49
48
|
$stdout.puts name
|
50
49
|
end
|
50
|
+
return 0
|
51
51
|
elsif ARGV.size > 1
|
52
52
|
return aargh('You can give only one content-file name.', 1)
|
53
53
|
end
|
@@ -59,19 +59,7 @@ Given a name of a included file, saves it to --output.
|
|
59
59
|
rescue StandardError => e
|
60
60
|
return aargh("#{e}\nFailed to read #{ARGV.first}", 3)
|
61
61
|
end
|
62
|
-
|
63
|
-
if output.nil?
|
64
|
-
$stdout.puts src
|
65
|
-
else
|
66
|
-
fp = Pathname.new output
|
67
|
-
fp.open('w') do |f|
|
68
|
-
f.puts src
|
69
|
-
end
|
70
|
-
end
|
71
|
-
rescue StandardError => e
|
72
|
-
return aargh([ e, "Failed to write output: #{output || 'stdout'}" ], 4)
|
73
|
-
end
|
74
|
-
0
|
62
|
+
dump_result(output, src, 4)
|
75
63
|
end
|
76
64
|
|
77
65
|
exit(main) if (defined? $unit_test).nil?
|
data/bin/diagrammatron-nodes
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2021, 2022 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common
|
7
|
+
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
9
|
require 'yaml'
|
10
10
|
require 'set'
|
@@ -312,7 +312,7 @@ def work_copy(src, quiet)
|
|
312
312
|
next
|
313
313
|
end
|
314
314
|
label = node['label']
|
315
|
-
if label2idx.key?(label) && edge_nodes.
|
315
|
+
if label2idx.key?(label) && edge_nodes.member?(label)
|
316
316
|
aargh "Edge-referred label used twice: #{label}"
|
317
317
|
errors = true
|
318
318
|
end
|
@@ -420,20 +420,7 @@ The 'sid' indicates the sub-diagram consisting of connected nodes.
|
|
420
420
|
|
421
421
|
algo.call(work)
|
422
422
|
prepare_output(doc, work)
|
423
|
-
|
424
|
-
d = YAML.dump(doc, line_width: 1000000)
|
425
|
-
if output.nil?
|
426
|
-
$stdout.puts d
|
427
|
-
else
|
428
|
-
fp = Pathname.new output
|
429
|
-
fp.open('w') do |f|
|
430
|
-
f.puts d
|
431
|
-
end
|
432
|
-
end
|
433
|
-
rescue StandardError => e
|
434
|
-
return aargh("#{e}\nFailed to write output: #{output || 'stdout'}", 4)
|
435
|
-
end
|
436
|
-
0
|
423
|
+
dump_result(output, YAML.dump(doc, line_width: 1_000_000), 4)
|
437
424
|
end
|
438
425
|
|
439
426
|
exit(main) if (defined? $unit_test).nil?
|
data/bin/diagrammatron-place
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2021, 2022 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common
|
7
|
+
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
9
|
require 'yaml'
|
10
10
|
require 'set'
|
11
11
|
require 'pathname'
|
12
12
|
|
13
|
-
def info(msg, return_value = nil
|
14
|
-
$stderr.puts(msg) unless $QUIET
|
13
|
+
def info(msg, return_value = nil)
|
14
|
+
$stderr.puts(msg) unless $QUIET
|
15
15
|
return_value
|
16
16
|
end
|
17
17
|
|
@@ -310,7 +310,7 @@ def pre_search_bounding_boxes(work)
|
|
310
310
|
end
|
311
311
|
|
312
312
|
def shift(work, sid, dx, dy)
|
313
|
-
[
|
313
|
+
%i[edges nodes].each do |kind|
|
314
314
|
work[kind].fetch(sid, []).each do |item|
|
315
315
|
item.shift(dx, dy)
|
316
316
|
end
|
@@ -402,16 +402,16 @@ do not overlap.
|
|
402
402
|
begin
|
403
403
|
whratio = Float($W2HRATIO)
|
404
404
|
if whratio <= 0
|
405
|
-
return
|
405
|
+
return aargh("Ratio must be greater than zero: #{$W2HRATIO}", 1)
|
406
406
|
end
|
407
407
|
$W2HRATIO = whratio
|
408
408
|
rescue StandardError
|
409
|
-
return
|
409
|
+
return aargh("Ratio parameter not a number: #{$W2HRATIO}", 1)
|
410
410
|
end
|
411
411
|
end
|
412
412
|
|
413
413
|
unless $algorithms.key? algo
|
414
|
-
return
|
414
|
+
return aargh("Unrecognized algorithm: #{algo}", 2)
|
415
415
|
end
|
416
416
|
algo = $algorithms[algo]
|
417
417
|
|
@@ -422,25 +422,12 @@ do not overlap.
|
|
422
422
|
work = work_copy(doc)
|
423
423
|
return 3 if work.nil?
|
424
424
|
rescue StandardError
|
425
|
-
return
|
425
|
+
return aargh('Error processing input.', 3)
|
426
426
|
end
|
427
427
|
|
428
428
|
algo.call(work)
|
429
429
|
prepare_output(doc, work)
|
430
|
-
|
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
|
430
|
+
dump_result(output, YAML.dump(doc, line_width: 1_000_000), 4)
|
444
431
|
end
|
445
432
|
|
446
433
|
exit(main) if (defined? $unit_test).nil?
|
data/bin/diagrammatron-prune
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# Copyright © 2021 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common
|
7
|
+
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
9
|
require 'yaml'
|
10
10
|
require 'set'
|
@@ -98,20 +98,7 @@ removed or kept depending on options. Edges to removed nodes are removed.
|
|
98
98
|
return aargh('Error processing input.', 3)
|
99
99
|
end
|
100
100
|
|
101
|
-
|
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
|
101
|
+
dump_result(output, YAML.dump(doc, line_width: 1_000_000), 4)
|
115
102
|
end
|
116
103
|
|
117
104
|
exit(main) if (defined? $unit_test).nil?
|
data/bin/diagrammatron-render
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2021, 2022 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common
|
7
|
+
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
9
|
require 'yaml'
|
10
10
|
require 'erb'
|
@@ -76,113 +76,120 @@ def separate_coordinates(doc)
|
|
76
76
|
[ xcoords, ycoords, ckd2count ]
|
77
77
|
end
|
78
78
|
|
79
|
-
class
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
79
|
+
class Styles
|
80
|
+
def base_styles(m, styles, group)
|
81
|
+
d = styles.dig(group, 'default') || {}
|
82
|
+
m['default'] = m.fetch('default', {}).merge(d)
|
83
|
+
styles.fetch(group, {}).each_pair do |name, values|
|
84
|
+
s = d.clone
|
85
|
+
s.merge!(values) unless name == 'default'
|
86
|
+
m[name] = m.fetch(name, {}).merge(s)
|
87
|
+
end
|
88
|
+
m
|
89
|
+
end
|
90
|
+
|
91
|
+
def initialize(template_styles, diagram_styles)
|
92
|
+
@n = base_styles(base_styles({}, template_styles, 'node'), diagram_styles, 'node')
|
93
|
+
@e = base_styles(base_styles({}, template_styles, 'edge'), diagram_styles, 'edge')
|
94
|
+
@d = base_styles(base_styles({}, template_styles, 'diagram'), diagram_styles, 'diagram')
|
95
|
+
end
|
96
|
+
|
97
|
+
def fill(mapping, type_name, item)
|
98
|
+
styles = item.fetch('style', [ 'default' ])
|
99
|
+
styles = [ styles ] unless styles.is_a?(Array)
|
100
|
+
s = {}
|
101
|
+
styles.each do |name|
|
102
|
+
ns = mapping.fetch(name, nil)
|
103
|
+
raise "No such #{type_name} style: #{name} in #{mapping.keys.join(', ')}" if ns.nil?
|
104
|
+
s.merge! ns
|
105
|
+
end
|
106
|
+
# Keep values specified explicitly.
|
107
|
+
item.merge!(s) {|key, existing, from_template| existing }
|
91
108
|
end
|
92
|
-
end
|
93
109
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
@
|
104
|
-
@edge_gap = defaults.fetch('edge_gap', 20)
|
105
|
-
@font = FontInfo.new(template)
|
110
|
+
def apply_node_styles(node)
|
111
|
+
fill(@n, 'node', node)
|
112
|
+
end
|
113
|
+
|
114
|
+
def apply_edge_styles(edge)
|
115
|
+
fill(@e, 'edge', edge)
|
116
|
+
end
|
117
|
+
|
118
|
+
def apply_diagram_styles(diagram)
|
119
|
+
fill(@d, 'diagram', diagram)
|
106
120
|
end
|
107
121
|
end
|
108
122
|
|
109
123
|
class SizeEstimation
|
110
|
-
attr_accessor :node, :
|
124
|
+
attr_accessor :node, :ckd2count, :doc
|
111
125
|
|
112
|
-
def initialize(
|
126
|
+
def initialize(ckd2count, doc)
|
113
127
|
@node = nil
|
114
|
-
@template = template
|
115
128
|
@ckd2count = ckd2count
|
116
|
-
@
|
129
|
+
@doc = doc
|
117
130
|
end
|
118
131
|
|
119
|
-
def
|
132
|
+
def exposed_binding
|
120
133
|
binding
|
121
134
|
end
|
122
135
|
|
123
|
-
def get_default(key, default_value = nil)
|
124
|
-
@template.fetch('defaults', {}).fetch(key, default_value)
|
125
|
-
end
|
126
|
-
|
127
136
|
def max_edges(key, edge_gap)
|
128
137
|
c = [ @node['xo'], @node['yo'], key, -1 ]
|
129
138
|
count = @ckd2count[c]
|
130
139
|
c[3] = 1
|
131
140
|
count = [ count, @ckd2count[c] ].max
|
132
141
|
return 0 if count < 2
|
133
|
-
(count - 1) *
|
142
|
+
(count - 1) * edge_gap
|
134
143
|
end
|
135
144
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
145
|
+
# font_size is the actual size.
|
146
|
+
# font_width, font_height, font_line_spacing are [0, 1] size scaling factors.
|
147
|
+
# width_margin, height_margin are in same units as fonti, space inside node.
|
148
|
+
# edge_gap is minimum space between edges at any node side.
|
149
|
+
def default_size(font_size, font_width, font_height, font_line_spacing,
|
150
|
+
width_margin, height_margin, edge_gap)
|
151
|
+
lines = @node['text']
|
152
|
+
w = 2 * width_margin + font_width * font_size * (lines.map &(:size)).max
|
153
|
+
@node['w'] = [ w, max_edges('xo', edge_gap) ].max
|
154
|
+
h = 2 * height_margin + font_height * font_size * lines.size +
|
155
|
+
font_line_spacing * font_size * (lines.size - 1)
|
156
|
+
@node['h'] = [ h, max_edges('yo', edge_gap) ].max
|
146
157
|
end
|
147
158
|
end
|
148
159
|
|
149
|
-
def estimate_sizes(doc,
|
150
|
-
$render = SizeEstimation.new(
|
151
|
-
sizes = template.fetch('sizes', {})
|
152
|
-
defaults = template.fetch('defaults', {})
|
160
|
+
def estimate_sizes(doc, ckd2count)
|
161
|
+
$render = SizeEstimation.new(ckd2count, doc)
|
153
162
|
doc['nodes'].each do |node|
|
154
163
|
$render.node = node
|
164
|
+
label = node.fetch('label', 'unnamed')
|
155
165
|
style = node.fetch('style', 'default')
|
156
|
-
code =
|
157
|
-
%(raise NotImplementedError, "No size estimator for style: #{style}"))
|
158
|
-
if sizes.key? code
|
159
|
-
code = sizes.fetch(code)
|
160
|
-
end
|
166
|
+
code = node.fetch('size_estimator',
|
167
|
+
%(raise NotImplementedError, "No size estimator for style: #{style}"))
|
161
168
|
code = code.join("\n") if code.is_a? Array
|
162
169
|
begin
|
163
|
-
eval(code, $render.
|
170
|
+
eval(code, $render.exposed_binding)
|
164
171
|
rescue StandardError => e
|
165
|
-
return aargh("Size estimate style #{style} node #{
|
172
|
+
return aargh("Size estimate style #{style} node #{label} error #{e}", false)
|
166
173
|
end
|
167
174
|
end
|
168
175
|
$render = nil
|
169
176
|
true
|
170
177
|
end
|
171
178
|
|
172
|
-
def maxima(doc
|
179
|
+
def maxima(doc)
|
173
180
|
xmax = Hash.new(0)
|
174
181
|
ymax = Hash.new(0)
|
175
182
|
doc.fetch('nodes', []).each do |node|
|
176
|
-
xmax[node['xo']] = [ node[
|
177
|
-
ymax[node['yo']] = [ node[
|
183
|
+
xmax[node['xo']] = [ node['w'], xmax[node['xo']] ].max
|
184
|
+
ymax[node['yo']] = [ node['h'], ymax[node['yo']] ].max
|
178
185
|
end
|
179
186
|
[ xmax, ymax ]
|
180
187
|
end
|
181
188
|
|
182
|
-
def apply_maxima(doc, xmax, ymax
|
189
|
+
def apply_maxima(doc, xmax, ymax)
|
183
190
|
doc.fetch('nodes', []).each do |node|
|
184
|
-
node[
|
185
|
-
node[
|
191
|
+
node['w'] = xmax[node['xo']]
|
192
|
+
node['h'] = ymax[node['yo']]
|
186
193
|
end
|
187
194
|
end
|
188
195
|
|
@@ -197,8 +204,8 @@ def parallel_edge_step_minima(coords)
|
|
197
204
|
c2m
|
198
205
|
end
|
199
206
|
|
200
|
-
def remap_coordinates(coords, cmax, c2min,
|
201
|
-
c =
|
207
|
+
def remap_coordinates(coords, cmax, c2min, edge_gap)
|
208
|
+
c = edge_gap
|
202
209
|
gap = 0 # How much space all edge segments need.
|
203
210
|
zero_after_decrease = false
|
204
211
|
prev_dir = -2
|
@@ -207,20 +214,20 @@ def remap_coordinates(coords, cmax, c2min, defaults)
|
|
207
214
|
case coord.direction
|
208
215
|
when -1
|
209
216
|
c += gap if -1 < prev_dir
|
210
|
-
gap =
|
217
|
+
gap = edge_gap
|
211
218
|
coord.object[coord.key] = c
|
212
219
|
when 0
|
213
|
-
gap =
|
220
|
+
gap = edge_gap / c2min[coord.integer]
|
214
221
|
if zero_after_decrease
|
215
222
|
# Edge segment is at same range as nodes.
|
216
223
|
coord.object[coord.key] = c + coord.fraction * cmax[coord.integer]
|
217
224
|
else
|
218
225
|
coord.object[coord.key] =
|
219
|
-
c +
|
226
|
+
c + (edge_gap * coord.fraction) / c2min[coord.integer]
|
220
227
|
end
|
221
228
|
when 1
|
222
|
-
gap =
|
223
|
-
c += cmax[coord.integer] unless
|
229
|
+
gap = edge_gap
|
230
|
+
c += cmax[coord.integer] unless prev_dir == 1
|
224
231
|
coord.object[coord.key] = c
|
225
232
|
zero_after_decrease = false
|
226
233
|
end
|
@@ -229,28 +236,23 @@ def remap_coordinates(coords, cmax, c2min, defaults)
|
|
229
236
|
end
|
230
237
|
|
231
238
|
class Render
|
232
|
-
attr_accessor :doc, :template
|
239
|
+
attr_accessor :doc, :template
|
233
240
|
|
234
|
-
def initialize(doc, template
|
241
|
+
def initialize(doc, template)
|
235
242
|
@doc = doc
|
236
243
|
@template = template
|
237
|
-
@defaults = defaults
|
238
244
|
end
|
239
245
|
|
240
|
-
def
|
246
|
+
def exposed_binding
|
241
247
|
binding
|
242
248
|
end
|
243
249
|
|
244
|
-
def get_default(key, default_value = nil)
|
245
|
-
@template.fetch('defaults', {}).fetch(key, default_value)
|
246
|
-
end
|
247
|
-
|
248
250
|
def dimensions
|
249
251
|
w = 0
|
250
252
|
h = 0
|
251
253
|
@doc.fetch('nodes', []).each do |node|
|
252
|
-
w = [ w, node['xo'] + node[
|
253
|
-
h = [ h, node['yo'] + node[
|
254
|
+
w = [ w, node['xo'] + node['w'] ].max
|
255
|
+
h = [ h, node['yo'] + node['h'] ].max
|
254
256
|
end
|
255
257
|
@doc.fetch('edges', []).each do |edge|
|
256
258
|
path = edge.fetch('path', nil)
|
@@ -264,9 +266,9 @@ class Render
|
|
264
266
|
end
|
265
267
|
end
|
266
268
|
|
267
|
-
def apply(doc, template
|
268
|
-
$render = Render.new(doc, template
|
269
|
-
out = ERB.new(template.fetch('template', '')).result($render.
|
269
|
+
def apply(doc, template)
|
270
|
+
$render = Render.new(doc, template)
|
271
|
+
out = ERB.new(template.fetch('template', '')).result($render.exposed_binding)
|
270
272
|
$render = nil
|
271
273
|
out
|
272
274
|
end
|
@@ -275,6 +277,7 @@ def main
|
|
275
277
|
template = nil
|
276
278
|
input = nil
|
277
279
|
output = nil
|
280
|
+
styles = nil
|
278
281
|
parser = OptionParser.new do |opts|
|
279
282
|
opts.summary_indent = ' '
|
280
283
|
opts.summary_width = 20
|
@@ -314,42 +317,37 @@ Output is the file produced by the erb-template.
|
|
314
317
|
return aargh("Key #{key} base-64 decoding failed to key #{nk}", 2)
|
315
318
|
end
|
316
319
|
end
|
317
|
-
defaults = Defaults.new(template)
|
318
320
|
|
319
321
|
doc = load_source(input)
|
320
322
|
return 2 if doc.nil?
|
321
323
|
|
324
|
+
styles = Styles.new(template.fetch('styles', {}), doc.fetch('styles', {}))
|
325
|
+
doc.fetch('nodes', []).each do |node|
|
326
|
+
styles.apply_node_styles(node)
|
327
|
+
node['text'] = node.fetch('text', node.fetch('label', '')).split("\n")
|
328
|
+
end
|
329
|
+
doc.fetch('edges', []).each { |edge| styles.apply_edge_styles(edge) }
|
330
|
+
doc['diagram'] = {} unless doc.key? 'diagram'
|
331
|
+
styles.apply_diagram_styles(doc['diagram'])
|
332
|
+
|
322
333
|
begin
|
323
334
|
xcoords, ycoords, ckd2count = separate_coordinates(doc)
|
324
335
|
rescue StandardError
|
325
336
|
return aargh('Error processing input.', 3)
|
326
337
|
end
|
327
338
|
|
328
|
-
return 4 unless estimate_sizes(doc,
|
339
|
+
return 4 unless estimate_sizes(doc, ckd2count)
|
329
340
|
|
330
341
|
# Make all rows the same height and all columns the same width.
|
331
|
-
xmax, ymax = maxima(doc
|
332
|
-
apply_maxima(doc, xmax, ymax
|
342
|
+
xmax, ymax = maxima(doc)
|
343
|
+
apply_maxima(doc, xmax, ymax)
|
333
344
|
|
334
345
|
x2min = parallel_edge_step_minima(xcoords)
|
335
346
|
y2min = parallel_edge_step_minima(ycoords)
|
336
|
-
remap_coordinates(xcoords, xmax, x2min,
|
337
|
-
remap_coordinates(ycoords, ymax, y2min,
|
347
|
+
remap_coordinates(xcoords, xmax, x2min, doc.dig('diagram', 'edge_gap'))
|
348
|
+
remap_coordinates(ycoords, ymax, y2min, doc.dig('diagram', 'edge_gap'))
|
338
349
|
|
339
|
-
|
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
|
350
|
+
dump_result(output, apply(doc, template), 5)
|
353
351
|
end
|
354
352
|
|
355
353
|
exit(main) if (defined? $unit_test).nil?
|
data/bin/diagrammatron-template
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2021, 2022 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common
|
7
|
+
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
9
|
require 'yaml'
|
10
10
|
require 'set'
|
@@ -21,7 +21,7 @@ def add_field(doc, field_name, content)
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def missing(doc)
|
24
|
-
[
|
24
|
+
%w[template].each do |key|
|
25
25
|
next if doc.key? key
|
26
26
|
next if doc.key? "base64#{key}"
|
27
27
|
return aargh("#{key} is missing", 4)
|
@@ -88,20 +88,7 @@ Extra fields are not restricted in any manner.
|
|
88
88
|
m = missing(doc)
|
89
89
|
return m unless m.nil?
|
90
90
|
|
91
|
-
|
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
|
91
|
+
dump_result(output, YAML.dump(doc, line_width: 1_000_000), 5)
|
105
92
|
end
|
106
93
|
|
107
94
|
exit(main) if (defined? $unit_test).nil?
|
data/bin/dot_json2diagrammatron
CHANGED
@@ -1,32 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2021, 2022 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common
|
7
|
+
require_relative '../lib/common'
|
8
8
|
require 'optparse'
|
9
9
|
require 'yaml'
|
10
10
|
require 'json'
|
11
11
|
require 'pathname'
|
12
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
13
|
|
31
14
|
def convert(src)
|
32
15
|
idx2label = {}
|
data/lib/common.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright © 2021 Ismo Kärkkäinen
|
3
|
+
# Copyright © 2021, 2022 Ismo Kärkkäinen
|
4
4
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
5
|
|
6
6
|
def aargh(message, return_value = nil)
|
@@ -24,3 +24,17 @@ def load_source_hash(input)
|
|
24
24
|
end
|
25
25
|
src
|
26
26
|
end
|
27
|
+
|
28
|
+
def dump_result(output, doc, error_return)
|
29
|
+
if output.nil?
|
30
|
+
$stdout.puts doc
|
31
|
+
else
|
32
|
+
fp = Pathname.new output
|
33
|
+
fp.open('w') do |f|
|
34
|
+
f.puts doc
|
35
|
+
end
|
36
|
+
end
|
37
|
+
0
|
38
|
+
rescue StandardError => e
|
39
|
+
aargh([ e, "Failed to write output: #{output || 'stdout'}" ], error_return)
|
40
|
+
end
|
data/template/internal.yaml
CHANGED
@@ -1,21 +1,32 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
2
|
+
styles:
|
3
|
+
diagram:
|
4
|
+
default:
|
5
|
+
edge_gap: 20
|
6
|
+
width_margin: 10
|
7
|
+
height_margin: 10
|
8
|
+
node:
|
9
|
+
default:
|
10
|
+
width_margin: 10
|
11
|
+
height_margin: 10
|
12
|
+
font_size: 16
|
13
|
+
font_ascend: 0.8
|
14
|
+
font_line_spacing: 0.2
|
15
|
+
font_height: 1
|
16
|
+
font_width: 0.5
|
17
|
+
font_fill: "#000000"
|
18
|
+
url_fill: "#000000"
|
19
|
+
fill: "#ffffff"
|
20
|
+
stroke: "#000000"
|
21
|
+
stroke_width: 2
|
22
|
+
size_estimator: |
|
23
|
+
$render.default_size($render.node['font_size'],
|
24
|
+
$render.node['font_width'], $render.node['font_height'],
|
25
|
+
$render.node['font_line_spacing'],
|
26
|
+
$render.node['width_margin'], $render.node['height_margin'],
|
27
|
+
$render.doc['diagram']['edge_gap'])
|
28
|
+
edge:
|
29
|
+
default:
|
30
|
+
stroke_width: 2
|
31
|
+
stroke: "#000000"
|
32
|
+
base64template: PD94bWwgdmVyc2lvbj0iMS4wIj8+CjwlPQp3LCBoaCA9ICRyZW5kZXIuZGltZW5zaW9ucwpoaCArPSAkcmVuZGVyLmRvYy5kaWcoJ2RpYWdyYW0nLCAnaGVpZ2h0X21hcmdpbicpCgpvdXQgPSBbCiAgJSg8c3ZnIHdpZHRoPSIje3cgKyAkcmVuZGVyLmRvYy5kaWcoJ2RpYWdyYW0nLCAnd2lkdGhfbWFyZ2luJyl9IiBoZWlnaHQ9IiN7aGh9IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiPikKXQokcmVuZGVyLmRvYy5mZXRjaCgnbm9kZXMnLCBbXSkuZWFjaCBkbyB8bm9kZXwKICB3ID0gbm9kZVsndyddLnRvX2kKICBoID0gbm9kZVsnaCddLnRvX2kKICB4ID0gbm9kZVsneG8nXS50b19pCiAgeSA9IGhoIC0gbm9kZVsneW8nXS50b19pIC0gaAogIG5vZGVzdHlsZSA9ICUoZmlsbD0iI3tub2RlWydmaWxsJ119IiBzdHJva2U9IiN7bm9kZVsnc3Ryb2tlJ119IiBzdHJva2Utd2lkdGg9IiN7bm9kZVsnc3Ryb2tlX3dpZHRoJ119IikKICBvdXQucHVzaCglKDxyZWN0ICN7bm9kZXN0eWxlfSBoZWlnaHQ9IiN7aH0iIHdpZHRoPSIje3d9IiB4PSIje3h9IiB5PSIje3l9Ii8+KSkKICB4ICs9IG5vZGVbJ3dpZHRoX21hcmdpbiddCiAgZnMgPSBub2RlWydmb250X3NpemUnXQogIGxoID0gZnMgKiAoMSArIG5vZGVbJ2ZvbnRfbGluZV9zcGFjaW5nJ10pCiAgeSArPSBub2RlWydoZWlnaHRfbWFyZ2luJ10gKyBmcyAqIG5vZGVbJ2ZvbnRfYXNjZW5kJ10gIyBCYXNlbGluZSBmb3IgZmlyc3QgbGluZS4KICB1cmwgPSBub2RlLmZldGNoKCd1cmwnLCBuaWwpCiAgdXJsLmVuY29kZSEoOnhtbCA9PiA6YXR0cikgdW5sZXNzIHVybC5uaWw/CiAgeTAgPSB5CiAgdGV4dHN0eWxlID0gJShmaWxsPSIje25vZGVbJ2ZvbnRfZmlsbCddfSIgZm9udC1mYW1pbHk9InNlcmlmIiBmb250LXNpemU9IiN7ZnN9IiBzdHJva2U9IiN7bm9kZVsnZm9udF9maWxsJ119IiBzdHJva2Utd2lkdGg9IjAiIHhtbDpzcGFjZT0icHJlc2VydmUiKQogIGxpbmtzdHlsZSA9ICUoZmlsbD0iI3tub2RlWyd1cmxfZmlsbCddfSIgZm9udC1mYW1pbHk9InNlcmlmIiBmb250LXNpemU9IiN7ZnN9IiBzdHJva2U9IiN7bm9kZVsndXJsX2ZpbGwnXX0iIHN0cm9rZS13aWR0aD0iMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIpCiAgbm9kZVsndGV4dCddLmVhY2ggZG8gfGxpbmV8CiAgICBsaW5lLmVuY29kZSEoOnhtbCA9PiA6dGV4dCkKICAgIGlmIHVybC5uaWw/CiAgICAgIG91dC5wdXNoKCUoPHRleHQgI3t0ZXh0c3R5bGV9IHg9IiN7eH0iIHk9IiN7eTB9Ij4je2xpbmV9PC90ZXh0PikpCiAgICBlbHNlCiAgICAgIG91dC5wdXNoKCUoPGEgeGxpbms6aHJlZj0je3VybH0gdGFyZ2V0PSJfcGFyZW50Ij48dGV4dCAje2xpbmtzdHlsZX0geD0iI3t4fSIgeT0iI3t5MH0iPiN7bGluZX08L3RleHQ+PC9hPikpCiAgICBlbmQKICAgIHkwICs9IGxoICMgU2hpZnQgYmFzZWxpbmUgYnkgZnVsbCBsaW5lICsgc3BhY2luZyBoZWlnaHQuCiAgZW5kCmVuZAokcmVuZGVyLmRvYy5mZXRjaCgnZWRnZXMnLCBbXSkuZWFjaCBkbyB8ZWRnZXwKICBsaW5lc3R5bGUgPSAlKGZpbGw9Im5vbmUiIHN0cm9rZT0iI3tlZGdlWydzdHJva2UnXX0iIHN0cm9rZS13aWR0aD0iI3tlZGdlWydzdHJva2Vfd2lkdGgnXX0iKQogIHBhdGggPSBlZGdlLmZldGNoKCdwYXRoJywgbmlsKQogIG5leHQgaWYgcGF0aC5uaWw/CiAgcGF0aC5lYWNoIGRvIHxwfAogICAgcFsneG8nXSA9IHBbJ3hvJ10udG9faS50b19zCiAgICBwWyd5byddID0gKGhoIC0gcFsneW8nXSkudG9faS50b19zCiAgZW5kCiAgaWYgcGF0aC5zaXplID09IDIKICAgIG91dC5wdXNoKCUoPGxpbmUgI3tsaW5lc3R5bGV9IHgxPSIje3BhdGhbMF1bJ3hvJ119IiB4Mj0iI3twYXRoWzFdWyd4byddfSIgeTE9IiN7cGF0aFswXVsneW8nXX0iIHkyPSIje3BhdGhbMV1bJ3lvJ119Ii8+KSkKICBlbHNlCiAgICBwdHMgPSBwYXRoLm1hcCB7IHxwfCAiI3twWyd4byddfSwje3BbJ3lvJ119IiB9CiAgICBvdXQucHVzaCglKDxwb2x5bGluZSAje2xpbmVzdHlsZX0gcG9pbnRzPSIje3B0cy5qb2luKCcgJyl9Ii8+KSkKICBlbmQKZW5kCm91dC5qb2luKCJcbiIpCiU+Cjwvc3ZnPgo=
|
data/template/root.yaml
CHANGED
@@ -1,19 +1,30 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
1
|
+
styles:
|
2
|
+
diagram:
|
3
|
+
default:
|
4
|
+
edge_gap: 20
|
5
|
+
width_margin: 10
|
6
|
+
height_margin: 10
|
7
|
+
node:
|
8
|
+
default:
|
9
|
+
width_margin: 10
|
10
|
+
height_margin: 10
|
11
|
+
font_size: 16
|
12
|
+
font_ascend: 0.8
|
13
|
+
font_line_spacing: 0.2
|
14
|
+
font_height: 1
|
15
|
+
font_width: 0.5
|
16
|
+
font_fill: "#000000"
|
17
|
+
url_fill: "#000000"
|
18
|
+
fill: "#ffffff"
|
19
|
+
stroke: "#000000"
|
20
|
+
stroke_width: 2
|
21
|
+
size_estimator: |
|
22
|
+
$render.default_size($render.node['font_size'],
|
23
|
+
$render.node['font_width'], $render.node['font_height'],
|
24
|
+
$render.node['font_line_spacing'],
|
25
|
+
$render.node['width_margin'], $render.node['height_margin'],
|
26
|
+
$render.doc['diagram']['edge_gap'])
|
27
|
+
edge:
|
28
|
+
default:
|
29
|
+
stroke_width: 2
|
30
|
+
stroke: "#000000"
|
data/template/svg_1.1.erb
CHANGED
@@ -1,44 +1,39 @@
|
|
1
1
|
<?xml version="1.0"?>
|
2
2
|
<%=
|
3
3
|
w, hh = $render.dimensions
|
4
|
-
|
5
|
-
hh += hm
|
6
|
-
ma = $render.defaults.font.max_ascend
|
7
|
-
fs = $render.defaults.font.size
|
8
|
-
lh = fs + $render.defaults.font.line_spacing
|
9
|
-
wm = $render.defaults.width_margin
|
10
|
-
sw = $render.get_default('stroke_width', 5)
|
11
|
-
linestyle = %(fill="none" stroke="#000000" stroke-width="#{sw}")
|
12
|
-
textstyle = %(fill="#000000" font-family="serif" font-size="#{fs}" stroke="#000000" stroke-width="0" xml:space="preserve")
|
13
|
-
linkstyle = %(fill="#2020ff" font-family="serif" font-size="#{fs}" stroke="#2020ff" stroke-width="0" xml:space="preserve")
|
4
|
+
hh += $render.doc.dig('diagram', 'height_margin')
|
14
5
|
|
15
6
|
out = [
|
16
|
-
%(<svg width="#{w + $render.
|
7
|
+
%(<svg width="#{w + $render.doc.dig('diagram', 'width_margin')}" height="#{hh}" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">)
|
17
8
|
]
|
18
9
|
$render.doc.fetch('nodes', []).each do |node|
|
19
|
-
w = node[
|
20
|
-
h = node[
|
10
|
+
w = node['w'].to_i
|
11
|
+
h = node['h'].to_i
|
21
12
|
x = node['xo'].to_i
|
22
13
|
y = hh - node['yo'].to_i - h
|
23
|
-
nodestyle = %(fill="#{node
|
14
|
+
nodestyle = %(fill="#{node['fill']}" stroke="#{node['stroke']}" stroke-width="#{node['stroke_width']}")
|
24
15
|
out.push(%(<rect #{nodestyle} height="#{h}" width="#{w}" x="#{x}" y="#{y}"/>))
|
25
|
-
x +=
|
26
|
-
|
16
|
+
x += node['width_margin']
|
17
|
+
fs = node['font_size']
|
18
|
+
lh = fs * (1 + node['font_line_spacing'])
|
19
|
+
y += node['height_margin'] + fs * node['font_ascend'] # Baseline for first line.
|
27
20
|
url = node.fetch('url', nil)
|
28
21
|
url.encode!(:xml => :attr) unless url.nil?
|
29
|
-
lines = node.fetch('label', '').split("\n")
|
30
22
|
y0 = y
|
31
|
-
|
32
|
-
|
23
|
+
textstyle = %(fill="#{node['font_fill']}" font-family="serif" font-size="#{fs}" stroke="#{node['font_fill']}" stroke-width="0" xml:space="preserve")
|
24
|
+
linkstyle = %(fill="#{node['url_fill']}" font-family="serif" font-size="#{fs}" stroke="#{node['url_fill']}" stroke-width="0" xml:space="preserve")
|
25
|
+
node['text'].each do |line|
|
26
|
+
line.encode!(:xml => :text)
|
33
27
|
if url.nil?
|
34
|
-
out.push(%(<text #{textstyle} x="#{x}" y="#{y0}">#{
|
28
|
+
out.push(%(<text #{textstyle} x="#{x}" y="#{y0}">#{line}</text>))
|
35
29
|
else
|
36
|
-
out.push(%(<a xlink:href=#{url} target="_parent"><text #{linkstyle} x="#{x}" y="#{y0}">#{
|
30
|
+
out.push(%(<a xlink:href=#{url} target="_parent"><text #{linkstyle} x="#{x}" y="#{y0}">#{line}</text></a>))
|
37
31
|
end
|
38
32
|
y0 += lh # Shift baseline by full line + spacing height.
|
39
33
|
end
|
40
34
|
end
|
41
35
|
$render.doc.fetch('edges', []).each do |edge|
|
36
|
+
linestyle = %(fill="none" stroke="#{edge['stroke']}" stroke-width="#{edge['stroke_width']}")
|
42
37
|
path = edge.fetch('path', nil)
|
43
38
|
next if path.nil?
|
44
39
|
path.each do |p|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: diagrammatron
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismo Kärkkäinen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-31 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
|
@@ -52,7 +52,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
52
|
requirements:
|
53
53
|
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version:
|
55
|
+
version: 2.7.0
|
56
56
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - ">="
|