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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e75d5bdc504a1706dcfe0d3c120f4c49a4024076ebd42b5bcd5b151d127bd7f9
4
- data.tar.gz: 9872615f7842209b32b9402cb77a0071194ea5fa4478d5a26521d2f5c20787da
3
+ metadata.gz: 32bd6ca5ad8db808b0b6c921ee0efe5b8e1a6befe4e535636f16ff98a22de139
4
+ data.tar.gz: c00027483fc54a9fab024c1e42a4e10e27b797e52e3a9217319a50542926a894
5
5
  SHA512:
6
- metadata.gz: d820b1d922dbd336ed795eec477c34ec43508edd731142be9c5144dd9d00efdad1745d47fc586d2af6c1a65cd743024de6da440f413868bcd680e5384e839cac
7
- data.tar.gz: 709c2a401cee6f107faba0f86cc18025016b0b9c99d35f8f48959bc63721bf7b67d7fe45374df3a971eff73fdfd9266cda3c20e3b7ed672a44e7f590c3a87e1e
6
+ metadata.gz: 7723bc76dfd7463a864319421074d8c17357f6c88a309efd81919c165ad836f876d48e062bf618b45e2b7fe779b1c564afaedfa6b105ae1fee9f84e5f52003d5
7
+ data.tar.gz: ed23e22fd6476e33afb4360cce23cefb0373dbd2c763611d9f61d0a20a283fe2dcd7a4bac6f137d628020db86819aacdea6387c889e5821a3ed4b958a5a2a736
@@ -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.rb'
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.key?(label)
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
- def direction(s) # To decreasing or increasing coordinates.
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 ? [:xo, :yo] : [:yo, :xo]
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) do
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 # From top left, down, bottom right: -|_
428
- d = a[2].range.min <=> b[2].range.min # Guesstimate for DFS.
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.min <=> b[2].range.min # Guesstimate for DFS.
433
+ d = a[2].range.max <=> b[2].range.max # Descend on maximum.
432
434
  return -d unless d.zero?
433
- when 3 # Descending on length, range maximum. |=
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, :count) do
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
- placed = segments[n]
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
- count + crossings
453
+ cross_count + crossings
458
454
  end
459
455
  end
460
456
 
461
- def depth_first_search(segments, state, best)
462
- if state.order.size < segments.size
463
- segments.each_index do |k|
464
- next if state.order.include? k
465
- c = state.fitness(segments, k)
466
- next if (best.nil? ? c + 1 : best.count) <= c
467
- state.order.push(k)
468
- best = depth_first_search(segments, GapState.new(state.order, c), best)
469
- state.order.pop
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
- else
472
- best = GapState.new(state.order.clone, state.count)
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(segments)
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
- best = nil
503
+ lut = {}
482
504
  segments.each_index do |k|
483
- best = depth_first_search(segments, GapState.new([ k ], 0), best)
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, size = here
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 = Hash.new
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, before, after, sb, sa = p.segment_directions(k)
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 = (before.negative? ? 0 : 1) + (after.negative? ? 0 : 2)
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 || a[1] == 2 })
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
- begin
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?
@@ -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.rb'
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
- begin
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?
@@ -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.rb'
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.key?(label)
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
- begin
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?
@@ -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.rb'
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, loud = false)
14
- $stderr.puts(msg) unless $QUIET && !loud
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
- [ :edges, :nodes ].each do |kind|
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 info("Ratio must be greater than zero: #{$W2HRATIO}", 1, true)
405
+ return aargh("Ratio must be greater than zero: #{$W2HRATIO}", 1)
406
406
  end
407
407
  $W2HRATIO = whratio
408
408
  rescue StandardError
409
- return info("Ratio parameter not a number: #{$W2HRATIO}", 1, true)
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 info("Unrecognized algorithm: #{algo}", 2, true)
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 info('Error processing input.', 3, true)
425
+ return aargh('Error processing input.', 3)
426
426
  end
427
427
 
428
428
  algo.call(work)
429
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
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?
@@ -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.rb'
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
- 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
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?
@@ -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.rb'
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 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)
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
- 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)
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, :template, :ckd2count, :defaults
124
+ attr_accessor :node, :ckd2count, :doc
111
125
 
112
- def initialize(template, ckd2count, defaults)
126
+ def initialize(ckd2count, doc)
113
127
  @node = nil
114
- @template = template
115
128
  @ckd2count = ckd2count
116
- @defaults = defaults
129
+ @doc = doc
117
130
  end
118
131
 
119
- def get_binding
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) * (edge_gap || @defaults.edge_gap)
142
+ (count - 1) * edge_gap
134
143
  end
135
144
 
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
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, template, ckd2count, defaults)
150
- $render = SizeEstimation.new(template, ckd2count, defaults)
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 = 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
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.get_binding)
170
+ eval(code, $render.exposed_binding)
164
171
  rescue StandardError => e
165
- return aargh("Size estimate style #{style} node #{node.fetch('label', 'unnamed')} error #{e}", false)
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, defaults)
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[defaults.width_key], xmax[node['xo']] ].max
177
- ymax[node['yo']] = [ node[defaults.height_key], ymax[node['yo']] ].max
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, defaults)
189
+ def apply_maxima(doc, xmax, ymax)
183
190
  doc.fetch('nodes', []).each do |node|
184
- node[defaults.width_key] = xmax[node['xo']]
185
- node[defaults.height_key] = ymax[node['yo']]
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, defaults)
201
- c = defaults.edge_gap
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 = defaults.edge_gap
217
+ gap = edge_gap
211
218
  coord.object[coord.key] = c
212
219
  when 0
213
- gap = defaults.edge_gap / c2min[coord.integer]
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 + defaults.edge_gap * coord.fraction / c2min[coord.integer]
226
+ c + (edge_gap * coord.fraction) / c2min[coord.integer]
220
227
  end
221
228
  when 1
222
- gap = defaults.edge_gap
223
- c += cmax[coord.integer] unless 1 == prev_dir
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, :defaults
239
+ attr_accessor :doc, :template
233
240
 
234
- def initialize(doc, template, defaults)
241
+ def initialize(doc, template)
235
242
  @doc = doc
236
243
  @template = template
237
- @defaults = defaults
238
244
  end
239
245
 
240
- def get_binding
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[@defaults.width_key] ].max
253
- h = [ h, node['yo'] + node[@defaults.height_key] ].max
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, defaults)
268
- $render = Render.new(doc, template, defaults)
269
- out = ERB.new(template.fetch('template', '')).result($render.get_binding)
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, template, ckd2count, defaults)
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, defaults)
332
- apply_maxima(doc, xmax, ymax, defaults)
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, defaults)
337
- remap_coordinates(ycoords, ymax, y2min, defaults)
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
- 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
350
+ dump_result(output, apply(doc, template), 5)
353
351
  end
354
352
 
355
353
  exit(main) if (defined? $unit_test).nil?
@@ -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.rb'
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
- [ 'defaults', 'sizes', 'template' ].each do |key|
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
- 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
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?
@@ -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.rb'
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
@@ -1,21 +1,32 @@
1
1
  ---
2
- defaults:
3
- edge_gap: 20
4
- width_margin: 10
5
- height_margin: 10
6
- font:
7
- font_size: 16
8
- stroke_width: 2
9
- width_key: w
10
- height_key: h
11
- size: default
12
- sizes:
13
- default: |
14
- $render.default_size(
15
- $render.node.fetch('width_scale', $render.defaults.font.width),
16
- $render.node.fetch('height_scale', $render.defaults.font.size),
17
- $render.node.fetch('line_spacing', $render.defaults.font.line_spacing),
18
- $render.node.fetch('width_margin', $render.defaults.width_margin),
19
- $render.node.fetch('height_margin', $render.defaults.height_margin),
20
- $render.node.fetch('edge_gap', $render.defaults.edge_gap))
21
- base64template: PD94bWwgdmVyc2lvbj0iMS4wIj8+CjwlPQp3LCBoaCA9ICRyZW5kZXIuZGltZW5zaW9ucwpobSA9ICRyZW5kZXIuZGVmYXVsdHMuaGVpZ2h0X21hcmdpbgpoaCArPSBobQptYSA9ICRyZW5kZXIuZGVmYXVsdHMuZm9udC5tYXhfYXNjZW5kCmZzID0gJHJlbmRlci5kZWZhdWx0cy5mb250LnNpemUKbGggPSBmcyArICRyZW5kZXIuZGVmYXVsdHMuZm9udC5saW5lX3NwYWNpbmcKd20gPSAkcmVuZGVyLmRlZmF1bHRzLndpZHRoX21hcmdpbgpzdyA9ICRyZW5kZXIuZ2V0X2RlZmF1bHQoJ3N0cm9rZV93aWR0aCcsIDUpCmxpbmVzdHlsZSA9ICUoZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IiN7c3d9IikKdGV4dHN0eWxlID0gJShmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2VyaWYiIGZvbnQtc2l6ZT0iI3tmc30iIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIwIiB4bWw6c3BhY2U9InByZXNlcnZlIikKbGlua3N0eWxlID0gJShmaWxsPSIjMjAyMGZmIiBmb250LWZhbWlseT0ic2VyaWYiIGZvbnQtc2l6ZT0iI3tmc30iIHN0cm9rZT0iIzIwMjBmZiIgc3Ryb2tlLXdpZHRoPSIwIiB4bWw6c3BhY2U9InByZXNlcnZlIikKCm91dCA9IFsKICAlKDxzdmcgd2lkdGg9IiN7dyArICRyZW5kZXIuZGVmYXVsdHMud2lkdGhfbWFyZ2lufSIgaGVpZ2h0PSIje2hofSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIj4pCl0KJHJlbmRlci5kb2MuZmV0Y2goJ25vZGVzJywgW10pLmVhY2ggZG8gfG5vZGV8CiAgdyA9IG5vZGVbJHJlbmRlci5kZWZhdWx0cy53aWR0aF9rZXldLnRvX2kKICBoID0gbm9kZVskcmVuZGVyLmRlZmF1bHRzLmhlaWdodF9rZXldLnRvX2kKICB4ID0gbm9kZVsneG8nXS50b19pCiAgeSA9IGhoIC0gbm9kZVsneW8nXS50b19pIC0gaAogIG5vZGVzdHlsZSA9ICUoZmlsbD0iI3tub2RlLmZldGNoKCdmaWxsJywgJyNmZmZmZmYnKX0iIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIje3N3fSIpCiAgb3V0LnB1c2goJSg8cmVjdCAje25vZGVzdHlsZX0gaGVpZ2h0PSIje2h9IiB3aWR0aD0iI3t3fSIgeD0iI3t4fSIgeT0iI3t5fSIvPikpCiAgeCArPSB3bQogIHkgKz0gaG0gKyBtYSAjIEJhc2VsaW5lIGZvciBmaXJzdCBsaW5lLgogIHVybCA9IG5vZGUuZmV0Y2goJ3VybCcsIG5pbCkKICB1cmwuZW5jb2RlISg6eG1sID0+IDphdHRyKSB1bmxlc3MgdXJsLm5pbD8KICBsaW5lcyA9IG5vZGUuZmV0Y2goJ2xhYmVsJywgJycpLnNwbGl0KCJcbiIpCiAgeTAgPSB5CiAgbGluZXMuZWFjaCBkbyB8bGFiZWx8CiAgICBsYWJlbC5lbmNvZGUhKDp4bWwgPT4gOnRleHQpCiAgICBpZiB1cmwubmlsPwogICAgICBvdXQucHVzaCglKDx0ZXh0ICN7dGV4dHN0eWxlfSB4PSIje3h9IiB5PSIje3kwfSI+I3tsYWJlbH08L3RleHQ+KSkKICAgIGVsc2UKICAgICAgb3V0LnB1c2goJSg8YSB4bGluazpocmVmPSN7dXJsfSB0YXJnZXQ9Il9wYXJlbnQiPjx0ZXh0ICN7bGlua3N0eWxlfSB4PSIje3h9IiB5PSIje3kwfSI+I3tsYWJlbH08L3RleHQ+PC9hPikpCiAgICBlbmQKICAgIHkwICs9IGxoICMgU2hpZnQgYmFzZWxpbmUgYnkgZnVsbCBsaW5lICsgc3BhY2luZyBoZWlnaHQuCiAgZW5kCmVuZAokcmVuZGVyLmRvYy5mZXRjaCgnZWRnZXMnLCBbXSkuZWFjaCBkbyB8ZWRnZXwKICBwYXRoID0gZWRnZS5mZXRjaCgncGF0aCcsIG5pbCkKICBuZXh0IGlmIHBhdGgubmlsPwogIHBhdGguZWFjaCBkbyB8cHwKICAgIHBbJ3hvJ10gPSBwWyd4byddLnRvX2kudG9fcwogICAgcFsneW8nXSA9IChoaCAtIHBbJ3lvJ10pLnRvX2kudG9fcwogIGVuZAogIGlmIHBhdGguc2l6ZSA9PSAyCiAgICBvdXQucHVzaCglKDxsaW5lICN7bGluZXN0eWxlfSB4MT0iI3twYXRoWzBdWyd4byddfSIgeDI9IiN7cGF0aFsxXVsneG8nXX0iIHkxPSIje3BhdGhbMF1bJ3lvJ119IiB5Mj0iI3twYXRoWzFdWyd5byddfSIvPikpCiAgZWxzZQogICAgcHRzID0gcGF0aC5tYXAgeyB8cHwgIiN7cFsneG8nXX0sI3twWyd5byddfSIgfQogICAgb3V0LnB1c2goJSg8cG9seWxpbmUgI3tsaW5lc3R5bGV9IHBvaW50cz0iI3twdHMuam9pbignICcpfSIvPikpCiAgZW5kCmVuZApvdXQuam9pbigiXG4iKQolPgo8L3N2Zz4K
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
- defaults:
2
- edge_gap: 20
3
- width_margin: 10
4
- height_margin: 10
5
- font:
6
- font_size: 16
7
- stroke_width: 2
8
- width_key: w
9
- height_key: h
10
- size: default
11
- sizes:
12
- default: |
13
- $render.default_size(
14
- $render.node.fetch('width_scale', $render.defaults.font.width),
15
- $render.node.fetch('height_scale', $render.defaults.font.size),
16
- $render.node.fetch('line_spacing', $render.defaults.font.line_spacing),
17
- $render.node.fetch('width_margin', $render.defaults.width_margin),
18
- $render.node.fetch('height_margin', $render.defaults.height_margin),
19
- $render.node.fetch('edge_gap', $render.defaults.edge_gap))
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
- hm = $render.defaults.height_margin
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.defaults.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">)
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[$render.defaults.width_key].to_i
20
- h = node[$render.defaults.height_key].to_i
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.fetch('fill', '#ffffff')}" stroke="#000000" stroke-width="#{sw}")
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 += wm
26
- y += hm + ma # Baseline for first line.
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
- lines.each do |label|
32
- label.encode!(:xml => :text)
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}">#{label}</text>))
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}">#{label}</text></a>))
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.2.2
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: 2021-10-15 00:00:00.000000000 Z
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: '0'
55
+ version: 2.7.0
56
56
  required_rubygems_version: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="