rsyntaxtree 2.4.1 → 2.6.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/CHANGELOG.md +147 -540
- data/CITATION.cff +2 -2
- data/README.md +2 -1
- data/bin/rsyntaxtree +6 -3
- data/lib/rsyntaxtree/base_graph.rb +105 -30
- data/lib/rsyntaxtree/element.rb +1 -1
- data/lib/rsyntaxtree/elementlist.rb +95 -0
- data/lib/rsyntaxtree/json_graph.rb +27 -49
- data/lib/rsyntaxtree/notation_core.md +2 -1
- data/lib/rsyntaxtree/notation_examples.md +57 -2
- data/lib/rsyntaxtree/svg_graph.rb +219 -76
- data/lib/rsyntaxtree/tikz_generator.rb +12 -1
- data/lib/rsyntaxtree/version.rb +1 -1
- data/lib/rsyntaxtree.rb +20 -10
- data/rsyntaxtree.gemspec +2 -2
- metadata +1 -2
- data/.dockerignore +0 -5
|
@@ -57,7 +57,6 @@ module RSyntaxTree
|
|
|
57
57
|
@polygon_styles = "<polygon style='fill: none; stroke: #{@col_connector}; stroke-width:#{@global[:stroke_normal]}; stroke-linejoin:round;stroke-linecap:round;' points='X1 Y1 X2 Y2 X3 Y3' />\n"
|
|
58
58
|
@text_styles = "<text white-space='pre' alignment-baseline='text-top' style='fill: COLOR; stroke-width: 0; font-size: fontsize' x='X_VALUE' y='Y_VALUE'>CONTENT</text>\n"
|
|
59
59
|
@tree_data = String.new
|
|
60
|
-
@visited_x = {}
|
|
61
60
|
@visited_y = {}
|
|
62
61
|
@global = global
|
|
63
62
|
end
|
|
@@ -83,6 +82,15 @@ module RSyntaxTree
|
|
|
83
82
|
# the deepest leaf) keep a margin instead of touching the image edge. A
|
|
84
83
|
# margin is added around the region extents before unioning with the tree
|
|
85
84
|
# bounds. x2/y2 are the viewBox width/height, so right = x1 + x2.
|
|
85
|
+
# Bottom to top, a movement path runs above the words, past the top of
|
|
86
|
+
# the tree; the canvas grows upward to hold it.
|
|
87
|
+
if @direction == "btt" && @path_bounds
|
|
88
|
+
top = [y1, @path_bounds[:min_y] - @global[:height_connector_to_text]].min
|
|
89
|
+
y2 += y1 - top
|
|
90
|
+
@height += y1 - top
|
|
91
|
+
y1 = top
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
if @region_bounds
|
|
87
95
|
rb = @region_bounds
|
|
88
96
|
m = @global[:h_gap_between_nodes]
|
|
@@ -378,6 +386,12 @@ module RSyntaxTree
|
|
|
378
386
|
d << " L#{at.call(points.last)}"
|
|
379
387
|
end
|
|
380
388
|
|
|
389
|
+
# The dash every dashed line in the figure is drawn with.
|
|
390
|
+
def dash_array
|
|
391
|
+
dash = @fontsize / 4.0
|
|
392
|
+
"stroke-dasharray='#{dash} #{dash}'"
|
|
393
|
+
end
|
|
394
|
+
|
|
381
395
|
# One stroke rather than three lines meeting at right angles. Drawn this way
|
|
382
396
|
# the dash pattern runs round each turn instead of restarting at it, and an
|
|
383
397
|
# arrowhead is the end of the stroke rather than a marker on whichever line
|
|
@@ -393,8 +407,7 @@ module RSyntaxTree
|
|
|
393
407
|
radius = [radius, leg.call(points[-2], points[-1]) - arrow_length].min if end_arrow
|
|
394
408
|
radius = [radius, 0].max
|
|
395
409
|
|
|
396
|
-
|
|
397
|
-
dasharray = dashed ? "stroke-dasharray='#{dash} #{dash}' " : ""
|
|
410
|
+
dasharray = dashed ? "#{dash_array} " : ""
|
|
398
411
|
markers = +""
|
|
399
412
|
markers << "marker-start='url(#arrowStart)' " if start_arrow
|
|
400
413
|
markers << "marker-end='url(#arrow)' " if end_arrow
|
|
@@ -413,6 +426,59 @@ module RSyntaxTree
|
|
|
413
426
|
"#{dasharray}#{markers}/>"
|
|
414
427
|
end
|
|
415
428
|
|
|
429
|
+
# How far the labels standing between a path's two ends reach toward its
|
|
430
|
+
# rail: the rail runs past the two ends, and has to run past every label
|
|
431
|
+
# across its span too, or it is drawn through a word that stands farther
|
|
432
|
+
# out than either end. Measured to the same edge the ends are anchored at.
|
|
433
|
+
def label_reach(from, to)
|
|
434
|
+
lo, hi = [from, to].minmax
|
|
435
|
+
hctt = @global[:height_connector_to_text]
|
|
436
|
+
@element_list.get_elements.map do |e|
|
|
437
|
+
if @direction == "ltr"
|
|
438
|
+
next if e.vertical_indent > hi || e.vertical_indent + e.content_height < lo
|
|
439
|
+
|
|
440
|
+
e.horizontal_indent + e.content_width + hctt
|
|
441
|
+
else
|
|
442
|
+
next if e.horizontal_indent > hi || e.horizontal_indent + e.content_width < lo
|
|
443
|
+
|
|
444
|
+
@direction == "btt" ? e.vertical_indent - hctt : e.vertical_indent + e.content_height + hctt
|
|
445
|
+
end
|
|
446
|
+
end.compact.send(@direction == "btt" ? :min : :max) || from
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
RAIL_SLACK = 1e-6
|
|
450
|
+
|
|
451
|
+
# How far apart two rails stand, and two path ends on one label.
|
|
452
|
+
def rail_spacing
|
|
453
|
+
@global[:h_gap_between_nodes] * 1.25
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# A rail that would run along one already drawn, over part of the same
|
|
457
|
+
# span or less than one spacing short of it, is moved outward past it: two
|
|
458
|
+
# paths whose rails coincide read as one line, and nothing tells which
|
|
459
|
+
# ends it joins. Rails a spacing or more apart end to end stay on one
|
|
460
|
+
# level, as two paths meeting at a label do once their ends are spread.
|
|
461
|
+
#
|
|
462
|
+
# Each move goes outward and past the rail in the way, so no rail can be in
|
|
463
|
+
# the way twice and there are at most as many moves as rails. Distances
|
|
464
|
+
# are compared with a little slack: a rail moved exactly one spacing past
|
|
465
|
+
# another can measure a hair under it, and would be moved again forever.
|
|
466
|
+
def clear_of_rails(at, span, spacing, sign)
|
|
467
|
+
@rails ||= []
|
|
468
|
+
lo, hi = span
|
|
469
|
+
close = spacing - RAIL_SLACK
|
|
470
|
+
@rails.size.times do
|
|
471
|
+
blocking = @rails.find do |r|
|
|
472
|
+
r[:lo] < hi + close && lo < r[:hi] + close && (r[:at] - at).abs < close
|
|
473
|
+
end
|
|
474
|
+
break unless blocking
|
|
475
|
+
|
|
476
|
+
at = blocking[:at] + (sign * spacing)
|
|
477
|
+
end
|
|
478
|
+
@rails << { lo: lo, hi: hi, at: at }
|
|
479
|
+
at
|
|
480
|
+
end
|
|
481
|
+
|
|
416
482
|
# Where the movement paths have been, as a box. The canvas learns of a
|
|
417
483
|
# rail through @width and @height, which only say how far right and down
|
|
418
484
|
# anything got; the plane behind a sheared figure has to enclose the rail
|
|
@@ -427,27 +493,56 @@ module RSyntaxTree
|
|
|
427
493
|
b[:max_y] = [b[:max_y], ys.max].max
|
|
428
494
|
end
|
|
429
495
|
|
|
430
|
-
|
|
431
|
-
|
|
496
|
+
# Path ends that stand in one line across the paths' run (on one label,
|
|
497
|
+
# or on labels straight above one another) are spread apart, each to the
|
|
498
|
+
# side its path heads for: a path going left leaves from the left and one
|
|
499
|
+
# going right from the right, so neither crosses the other's rail. Among
|
|
500
|
+
# ends heading the same way, the one that goes farther takes the outer
|
|
501
|
+
# place, since its rail runs outside. Left to right, the same holds with
|
|
502
|
+
# up and down in place of left and right. Neighbouring ends stand one
|
|
503
|
+
# rail spacing apart, the same distance that keeps two rails apart, or
|
|
504
|
+
# closer where that would take them past the narrowest label they share:
|
|
505
|
+
# an end stays on its label, and so off its neighbours and on the canvas.
|
|
506
|
+
def spread_shared_ends(paths)
|
|
507
|
+
along = @direction == "ltr" ? 1 : 0
|
|
508
|
+
ends = Hash.new { |h, k| h[k] = [] }
|
|
509
|
+
paths.each do |t|
|
|
510
|
+
s = [t[:x1], t[:y1]]
|
|
511
|
+
e = [t[:x2], t[:y2]]
|
|
512
|
+
ends[s[along].round] << [t, :s_offset, s[along], e[along], t[:across1]]
|
|
513
|
+
ends[e[along].round] << [t, :t_offset, e[along], s[along], t[:across2]]
|
|
514
|
+
end
|
|
515
|
+
ends.each_value do |shared|
|
|
516
|
+
gaps = shared.size - 1
|
|
517
|
+
step = gaps.zero? ? 0 : [rail_spacing, shared.map(&:last).min / gaps.to_f].min
|
|
518
|
+
shared.each_with_index
|
|
519
|
+
.sort_by { |(_, _, at, other, _), i| [other <=> at, -other, i] }
|
|
520
|
+
.each_with_index do |((t, key, _, _, _), _), place|
|
|
521
|
+
t[key] = (place - (gaps / 2.0)) * step
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none, s_offset: 0, t_offset: 0)
|
|
527
|
+
spacing = rail_spacing
|
|
432
528
|
min_bulge = @global[:height_connector_to_text]
|
|
529
|
+
# An end that carries an arrowhead needs a run long enough for the head
|
|
530
|
+
# and a stem before the turn. The rail clears the farther end by the
|
|
531
|
+
# bulge alone, so an arrowhead on that end — every arrowhead, once the
|
|
532
|
+
# terminals stand on one row — was pressed against the corner.
|
|
533
|
+
arrow_run = @global[:h_gap_between_nodes] + (min_bulge * 2)
|
|
534
|
+
source_arrow = target_arrow == :double
|
|
535
|
+
target_arrow_drawn = target_arrow != :none
|
|
433
536
|
# Small enough that the turn reads as an eased corner rather than as an
|
|
434
537
|
# arc, and taken from a layout measure so it holds at every font size.
|
|
435
538
|
corner_radius = min_bulge / 2.0
|
|
436
539
|
|
|
437
|
-
# Centered offset for multiple lines at the same endpoint
|
|
438
|
-
s_key = "#{s_x.round}"
|
|
439
|
-
t_key = "#{t_x.round}"
|
|
440
|
-
@visited_x[s_key] = (@visited_x[s_key] || 0) + 1
|
|
441
|
-
@visited_x[t_key] = (@visited_x[t_key] || 0) + 1
|
|
442
|
-
s_offset = ((@visited_x[s_key] - 1) - (@visited_x[s_key] - 1) / 2.0) * spacing
|
|
443
|
-
t_offset = ((@visited_x[t_key] - 1) - (@visited_x[t_key] - 1) / 2.0) * spacing
|
|
444
|
-
|
|
445
540
|
dashed = true if target_arrow == :none
|
|
446
541
|
|
|
447
542
|
if @direction == "ltr"
|
|
448
543
|
# LTR: route to the RIGHT of the tree (⊃ shape / reversed C)
|
|
449
544
|
# Source → right → vertical → left → Target
|
|
450
|
-
xmax = [s_x, t_x].max
|
|
545
|
+
xmax = [s_x, t_x, label_reach(s_y, t_y)].max
|
|
451
546
|
bulge = [min_bulge, (s_y - t_y).abs * 0.3 + min_bulge].min
|
|
452
547
|
# Ensure paths don't overlap: extend beyond previous paths
|
|
453
548
|
new_x = if xmax < @width
|
|
@@ -455,6 +550,8 @@ module RSyntaxTree
|
|
|
455
550
|
else
|
|
456
551
|
xmax + bulge
|
|
457
552
|
end
|
|
553
|
+
new_x = [new_x, s_x + arrow_run].max if source_arrow
|
|
554
|
+
new_x = [new_x, t_x + arrow_run].max if target_arrow_drawn
|
|
458
555
|
|
|
459
556
|
new_s_y = s_y + s_offset
|
|
460
557
|
new_t_y = t_y + t_offset
|
|
@@ -468,16 +565,22 @@ module RSyntaxTree
|
|
|
468
565
|
end_arrow: target_arrow != :none)
|
|
469
566
|
@width = new_x if new_x > @width
|
|
470
567
|
else
|
|
471
|
-
# TTB: route BELOW the tree (U shape)
|
|
568
|
+
# TTB: route BELOW the tree (U shape), on the side the words are on;
|
|
569
|
+
# BTT turns the tree over, words at the top, and the path with it.
|
|
472
570
|
# Source → down → horizontal → up → Target
|
|
473
|
-
|
|
571
|
+
outward = @direction == "btt" ? :min : :max
|
|
572
|
+
sign = @direction == "btt" ? -1 : 1
|
|
573
|
+
edge = [s_y, t_y, label_reach(s_x, t_x)].send(outward)
|
|
474
574
|
# Proportional bulge: based on distance between endpoints,
|
|
475
575
|
# not the full tree height
|
|
476
576
|
bulge = [min_bulge, (s_x - t_x).abs * 0.3 + min_bulge].min
|
|
477
|
-
new_y =
|
|
577
|
+
new_y = edge + (sign * bulge)
|
|
578
|
+
new_y = [new_y, s_y + (sign * arrow_run)].send(outward) if source_arrow
|
|
579
|
+
new_y = [new_y, t_y + (sign * arrow_run)].send(outward) if target_arrow_drawn
|
|
478
580
|
|
|
479
|
-
new_s_x = s_x
|
|
480
|
-
new_t_x = t_x
|
|
581
|
+
new_s_x = s_x + s_offset
|
|
582
|
+
new_t_x = t_x + t_offset
|
|
583
|
+
new_y = clear_of_rails(new_y, [new_s_x, new_t_x].minmax, spacing, sign)
|
|
481
584
|
|
|
482
585
|
note_path_extent([[new_s_x, s_y], [new_s_x, new_y],
|
|
483
586
|
[new_t_x, new_y], [new_t_x, t_y]])
|
|
@@ -511,7 +614,7 @@ module RSyntaxTree
|
|
|
511
614
|
# enclosure needs, so the text starts that far inside the node's left
|
|
512
615
|
# edge and the enclosure is drawn on the edge itself.
|
|
513
616
|
enclosure_room = element.label_enclosure_room
|
|
514
|
-
text_x = txt_pos - element.content_width / 2 + enclosure_room
|
|
617
|
+
text_x = txt_pos - element.content_width / 2.0 + enclosure_room
|
|
515
618
|
text_y = top + @global[:single_line_height] - @global[:height_connector_to_text]
|
|
516
619
|
text_data = text_data.sub(/X_VALUE/, text_x.to_s)
|
|
517
620
|
text_data = text_data.sub(/Y_VALUE/, text_y.to_s)
|
|
@@ -869,9 +972,9 @@ module RSyntaxTree
|
|
|
869
972
|
lgeom = {
|
|
870
973
|
left: element.horizontal_indent,
|
|
871
974
|
right: element.horizontal_indent + element.content_width,
|
|
872
|
-
top: y_center - element.content_height / 2,
|
|
873
|
-
bottom: y_center + element.content_height / 2,
|
|
874
|
-
cx: element.horizontal_indent + element.content_width / 2,
|
|
975
|
+
top: y_center - element.content_height / 2.0,
|
|
976
|
+
bottom: y_center + element.content_height / 2.0,
|
|
977
|
+
cx: element.horizontal_indent + element.content_width / 2.0,
|
|
875
978
|
cy: y_center
|
|
876
979
|
}
|
|
877
980
|
else
|
|
@@ -881,12 +984,23 @@ module RSyntaxTree
|
|
|
881
984
|
# them into the boxes in narrow ones. Only the line pool uses
|
|
882
985
|
# x0/x2; movement paths read x1, which is unchanged.
|
|
883
986
|
x0 = element.horizontal_indent
|
|
884
|
-
x1 = element.horizontal_indent + element.content_width / 2
|
|
987
|
+
x1 = element.horizontal_indent + element.content_width / 2.0
|
|
885
988
|
x2 = element.horizontal_indent + element.content_width
|
|
886
989
|
y0 = element.vertical_indent + @global[:height_connector_to_text] / 2
|
|
887
990
|
y1 = element.vertical_indent + element.content_height + @global[:height_connector_to_text]
|
|
888
991
|
lgeom = nil
|
|
889
992
|
end
|
|
993
|
+
# A movement path leaves a label on the side the words are on: below
|
|
994
|
+
# it top to bottom, above it bottom to top.
|
|
995
|
+
path_y = @direction == "btt" ? element.vertical_indent - @global[:height_connector_to_text] : y1
|
|
996
|
+
# How much of the label there is across the paths' run, for the ends
|
|
997
|
+
# that share it to be spread over: an even amount either side of the
|
|
998
|
+
# anchor, which left to right sits below the middle of the label.
|
|
999
|
+
across = if @direction == "ltr"
|
|
1000
|
+
[element.content_height - (@global[:height_connector_to_text] * 1.5), 0].max
|
|
1001
|
+
else
|
|
1002
|
+
element.content_width
|
|
1003
|
+
end
|
|
890
1004
|
et = element.path
|
|
891
1005
|
et.each do |tr|
|
|
892
1006
|
if /\A-(>|<)?(\d+)\z/ =~ tr
|
|
@@ -901,20 +1015,20 @@ module RSyntaxTree
|
|
|
901
1015
|
elsif /\A(?:>|<)(\d+)\z/ =~ tr
|
|
902
1016
|
tr = $1
|
|
903
1017
|
if path_pool_target[tr]
|
|
904
|
-
path_pool_target[tr] << [x1,
|
|
1018
|
+
path_pool_target[tr] << [x1, path_y, across]
|
|
905
1019
|
else
|
|
906
|
-
path_pool_target[tr] = [[x1,
|
|
1020
|
+
path_pool_target[tr] = [[x1, path_y, across]]
|
|
907
1021
|
end
|
|
908
1022
|
path_flags << tr
|
|
909
1023
|
elsif path_pool_source[tr]
|
|
910
1024
|
if path_pool_other[tr]
|
|
911
|
-
path_pool_other[tr] << [x1,
|
|
1025
|
+
path_pool_other[tr] << [x1, path_y, across]
|
|
912
1026
|
else
|
|
913
|
-
path_pool_other[tr] = [[x1,
|
|
1027
|
+
path_pool_other[tr] = [[x1, path_y, across]]
|
|
914
1028
|
end
|
|
915
1029
|
path_flags << tr
|
|
916
1030
|
else
|
|
917
|
-
path_pool_source[tr] = [x1,
|
|
1031
|
+
path_pool_source[tr] = [x1, path_y, across]
|
|
918
1032
|
path_flags << tr
|
|
919
1033
|
end
|
|
920
1034
|
raise RSTError.new(+"Error: input text contains a path having more than two ends:\n > #{tr}", code: :path_multiple_ends, hint: "A path or line takes exactly two ends; check the numbers.", retryable: true) if path_flags.tally.any? { |_k, v| v > 2 } || line_flags.tally.any? { |_k, v| v > 2 }
|
|
@@ -935,11 +1049,11 @@ module RSyntaxTree
|
|
|
935
1049
|
path_flags.delete(k)
|
|
936
1050
|
if (targets = path_pool_target[k])
|
|
937
1051
|
targets.each do |t|
|
|
938
|
-
paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :single }
|
|
1052
|
+
paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], across1: v[2], across2: t[2], arrow: :single }
|
|
939
1053
|
end
|
|
940
1054
|
elsif (others = path_pool_other[k])
|
|
941
1055
|
others.each do |t|
|
|
942
|
-
paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :none }
|
|
1056
|
+
paths << { x1: v[0], y1: v[1], x2: t[0], y2: t[1], across1: v[2], across2: t[2], arrow: :none }
|
|
943
1057
|
end
|
|
944
1058
|
end
|
|
945
1059
|
end
|
|
@@ -952,12 +1066,17 @@ module RSyntaxTree
|
|
|
952
1066
|
next if fst.nil?
|
|
953
1067
|
|
|
954
1068
|
targets.each do |t|
|
|
955
|
-
paths << { x1: fst[0], y1: fst[1], x2: t[0], y2: t[1], arrow: :double }
|
|
1069
|
+
paths << { x1: fst[0], y1: fst[1], x2: t[0], y2: t[1], across1: fst[2], across2: t[2], arrow: :double }
|
|
956
1070
|
end
|
|
957
1071
|
end
|
|
958
1072
|
|
|
1073
|
+
# Narrowest first, so a path that spans another runs outside it rather
|
|
1074
|
+
# than across its ends. Left to right, every rail already clears the
|
|
1075
|
+
# ones drawn before it, in the order written.
|
|
1076
|
+
paths = paths.each_with_index.sort_by { |t, i| [(t[:x1] - t[:x2]).abs, i] }.map(&:first) unless @direction == "ltr"
|
|
1077
|
+
spread_shared_ends(paths)
|
|
959
1078
|
paths.each do |t|
|
|
960
|
-
draw_a_path(t[:x1], t[:y1], t[:x2], t[:y2], t[:arrow])
|
|
1079
|
+
draw_a_path(t[:x1], t[:y1], t[:x2], t[:y2], t[:arrow], s_offset: t[:s_offset], t_offset: t[:t_offset])
|
|
961
1080
|
end
|
|
962
1081
|
|
|
963
1082
|
line_pool.each do |_k, v|
|
|
@@ -1025,8 +1144,7 @@ module RSyntaxTree
|
|
|
1025
1144
|
else
|
|
1026
1145
|
""
|
|
1027
1146
|
end
|
|
1028
|
-
|
|
1029
|
-
dasharray = dashed ? "stroke-dasharray='#{dash} #{dash}'" : ""
|
|
1147
|
+
dasharray = dashed ? dash_array : ""
|
|
1030
1148
|
swidth = bline ? @global[:stroke_bold] : @global[:stroke_normal]
|
|
1031
1149
|
|
|
1032
1150
|
if s_arrow && t_arrow
|
|
@@ -1091,8 +1209,7 @@ module RSyntaxTree
|
|
|
1091
1209
|
else
|
|
1092
1210
|
""
|
|
1093
1211
|
end
|
|
1094
|
-
|
|
1095
|
-
dasharray = dashed ? "stroke-dasharray='#{dash} #{dash}'" : ""
|
|
1212
|
+
dasharray = dashed ? dash_array : ""
|
|
1096
1213
|
swidth = bline ? @global[:stroke_bold] : @global[:stroke_normal]
|
|
1097
1214
|
|
|
1098
1215
|
"<line x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}; stroke-linecap:round;' #{dasharray} #{string}/>"
|
|
@@ -1215,6 +1332,25 @@ module RSyntaxTree
|
|
|
1215
1332
|
element.ink_bottom
|
|
1216
1333
|
end
|
|
1217
1334
|
|
|
1335
|
+
# Where a polyline's rail sits: halfway between the parent's end and the
|
|
1336
|
+
# row its children stand in, so every child of one node turns off the same
|
|
1337
|
+
# rail. A plain child's own end is not the measure: a joint's line runs to
|
|
1338
|
+
# its middle, so as to pass straight through it, and a label's lower edge
|
|
1339
|
+
# is its own ink, which differs from letter to letter. An enclosed child is
|
|
1340
|
+
# met at the box drawn round it, as ink_over meets it, so nil sends it to
|
|
1341
|
+
# its own end.
|
|
1342
|
+
def rail_edge_y(child, parent)
|
|
1343
|
+
m = @global[:single_x_metrics].height * @global[:vmargin]
|
|
1344
|
+
row = child.enclosure == :none && row_ink[child.vertical_indent.round(1)]
|
|
1345
|
+
return nil unless m && row
|
|
1346
|
+
|
|
1347
|
+
child.vertical_indent <= parent.vertical_indent ? row[:bottom] + m : row[:top] - m
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
def rail_edge_x(child)
|
|
1351
|
+
child.horizontal_indent - @global[:height_connector_to_text]
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1218
1354
|
def connector_edges(child, parent)
|
|
1219
1355
|
hctt = @global[:height_connector_to_text]
|
|
1220
1356
|
child_above = child.vertical_indent <= parent.vertical_indent
|
|
@@ -1232,7 +1368,7 @@ module RSyntaxTree
|
|
|
1232
1368
|
m = @global[:single_x_metrics].height * @global[:vmargin]
|
|
1233
1369
|
|
|
1234
1370
|
child_y = if child.empty_label?
|
|
1235
|
-
child.vertical_indent + child.content_height / 2
|
|
1371
|
+
child.vertical_indent + child.content_height / 2.0
|
|
1236
1372
|
elsif child_above
|
|
1237
1373
|
if m && child.ink_bottom
|
|
1238
1374
|
ink_under(child) + m
|
|
@@ -1246,7 +1382,7 @@ module RSyntaxTree
|
|
|
1246
1382
|
end
|
|
1247
1383
|
|
|
1248
1384
|
parent_y = if parent.empty_label?
|
|
1249
|
-
parent.vertical_indent + parent.content_height / 2
|
|
1385
|
+
parent.vertical_indent + parent.content_height / 2.0
|
|
1250
1386
|
elsif child_above
|
|
1251
1387
|
if m && parent.ink_top
|
|
1252
1388
|
ink_over(parent) - m
|
|
@@ -1276,8 +1412,12 @@ module RSyntaxTree
|
|
|
1276
1412
|
[child_y, parent_y]
|
|
1277
1413
|
end
|
|
1278
1414
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1415
|
+
# Dashed from +dash_from+ on, when given: a line through the joints leaf
|
|
1416
|
+
# alignment inserted is drawn one piece at a time, and each piece picks up
|
|
1417
|
+
# the dash where the one above it left off, so the run reads as one line.
|
|
1418
|
+
# Returns how long the line drawn is.
|
|
1419
|
+
def line_to_parent(parent, child, dash_from: nil)
|
|
1420
|
+
return 0 if child.horizontal_indent.zero?
|
|
1281
1421
|
|
|
1282
1422
|
if @direction == "ltr"
|
|
1283
1423
|
# LTR: parent's right side → child's left side
|
|
@@ -1287,64 +1427,67 @@ module RSyntaxTree
|
|
|
1287
1427
|
hctt = @global[:height_connector_to_text]
|
|
1288
1428
|
y1 = child.vertical_indent + (child.content_height + hctt * 1.5) / 2
|
|
1289
1429
|
y2 = parent.vertical_indent + (parent.content_height + hctt * 1.5) / 2
|
|
1290
|
-
x1 = child.empty_label? ? child.horizontal_indent + child.content_width / 2 : child.horizontal_indent - hctt
|
|
1291
|
-
x2 = parent.empty_label? ? parent.horizontal_indent + parent.content_width / 2 : parent.horizontal_indent + parent.content_width + hctt
|
|
1430
|
+
x1 = child.empty_label? ? child.horizontal_indent + child.content_width / 2.0 : child.horizontal_indent - hctt
|
|
1431
|
+
x2 = parent.empty_label? ? parent.horizontal_indent + parent.content_width / 2.0 : parent.horizontal_indent + parent.content_width + hctt
|
|
1292
1432
|
|
|
1293
1433
|
if @polyline
|
|
1294
|
-
mid_x1 = x2 + (
|
|
1434
|
+
mid_x1 = x2 + (rail_edge_x(child) - x2) / 2
|
|
1295
1435
|
mid_y1 = y1
|
|
1296
1436
|
mid_x2 = mid_x1
|
|
1297
1437
|
mid_y2 = y2
|
|
1298
|
-
|
|
1299
|
-
.sub(/CHIY/, y1.to_s)
|
|
1300
|
-
.sub(/MIDX1/, mid_x1.to_s)
|
|
1301
|
-
.sub(/MIDY1/, mid_y1.to_s)
|
|
1302
|
-
.sub(/MIDX2/, mid_x2.to_s)
|
|
1303
|
-
.sub(/MIDY2/, mid_y2.to_s)
|
|
1304
|
-
.sub(/PARX/, x2.to_s)
|
|
1305
|
-
.sub(/PARY/, y2.to_s)
|
|
1438
|
+
points = [[x1, y1], [mid_x1, mid_y1], [mid_x2, mid_y2], [x2, y2]]
|
|
1306
1439
|
else
|
|
1307
|
-
|
|
1308
|
-
line_data = line_data.sub(/Y1/, y1.to_s)
|
|
1309
|
-
line_data = line_data.sub(/X2/, x2.to_s)
|
|
1310
|
-
@tree_data += line_data.sub(/Y2/, y2.to_s)
|
|
1440
|
+
points = [[x1, y1], [x2, y2]]
|
|
1311
1441
|
end
|
|
1312
1442
|
else
|
|
1313
1443
|
# TTB: parent's bottom → child's top
|
|
1314
1444
|
if @polyline
|
|
1315
|
-
chi_x = child.horizontal_indent + child.content_width / 2
|
|
1316
|
-
par_x = parent.horizontal_indent + parent.content_width / 2
|
|
1445
|
+
chi_x = child.horizontal_indent + child.content_width / 2.0
|
|
1446
|
+
par_x = parent.horizontal_indent + parent.content_width / 2.0
|
|
1317
1447
|
chi_y, par_y = connector_edges(child, parent)
|
|
1318
1448
|
|
|
1319
1449
|
mid_x1 = chi_x
|
|
1320
|
-
mid_y1 = par_y + (chi_y - par_y) / 2
|
|
1450
|
+
mid_y1 = par_y + ((rail_edge_y(child, parent) || chi_y) - par_y) / 2
|
|
1321
1451
|
|
|
1322
1452
|
mid_x2 = par_x
|
|
1323
1453
|
mid_y2 = mid_y1
|
|
1324
1454
|
|
|
1325
|
-
|
|
1326
|
-
.sub(/CHIY/, chi_y.to_s)
|
|
1327
|
-
.sub(/MIDX1/, mid_x1.to_s)
|
|
1328
|
-
.sub(/MIDY1/, mid_y1.to_s)
|
|
1329
|
-
.sub(/MIDX2/, mid_x2.to_s)
|
|
1330
|
-
.sub(/MIDY2/, mid_y2.to_s)
|
|
1331
|
-
.sub(/PARX/, par_x.to_s)
|
|
1332
|
-
.sub(/PARY/, par_y.to_s)
|
|
1455
|
+
points = [[chi_x, chi_y], [mid_x1, mid_y1], [mid_x2, mid_y2], [par_x, par_y]]
|
|
1333
1456
|
else
|
|
1334
|
-
x1 = child.horizontal_indent + child.content_width / 2
|
|
1335
|
-
x2 = parent.horizontal_indent + parent.content_width / 2
|
|
1457
|
+
x1 = child.horizontal_indent + child.content_width / 2.0
|
|
1458
|
+
x2 = parent.horizontal_indent + parent.content_width / 2.0
|
|
1336
1459
|
y1, y2 = connector_edges(child, parent)
|
|
1337
|
-
|
|
1338
|
-
line_data = @line_styles.sub(/X1/, x1.to_s)
|
|
1339
|
-
line_data = line_data.sub(/Y1/, y1.to_s)
|
|
1340
|
-
line_data = line_data.sub(/X2/, x2.to_s)
|
|
1341
|
-
@tree_data += line_data.sub(/Y2/, y2.to_s)
|
|
1460
|
+
points = [[x1, y1], [x2, y2]]
|
|
1342
1461
|
end
|
|
1343
1462
|
end
|
|
1463
|
+
emit_connector(points, dash_from)
|
|
1464
|
+
end
|
|
1465
|
+
|
|
1466
|
+
# +points+ run from the child to the parent. A dashed line is laid the
|
|
1467
|
+
# other way, from the parent down, so that its dash carries on into the
|
|
1468
|
+
# piece below it.
|
|
1469
|
+
def emit_connector(points, dash_from)
|
|
1470
|
+
dash = ""
|
|
1471
|
+
if dash_from
|
|
1472
|
+
points = points.reverse
|
|
1473
|
+
dash = " #{dash_array} stroke-dashoffset='#{dash_from.round(3)}'"
|
|
1474
|
+
end
|
|
1475
|
+
if points.size == 2
|
|
1476
|
+
(x1, y1), (x2, y2) = points
|
|
1477
|
+
@tree_data += @line_styles.sub(/X1/, x1.to_s).sub(/Y1/, y1.to_s)
|
|
1478
|
+
.sub(/X2/, x2.to_s).sub(/Y2/, y2.to_s).sub(" />", "#{dash} />")
|
|
1479
|
+
else
|
|
1480
|
+
(chix, chiy), (mx1, my1), (mx2, my2), (parx, pary) = points
|
|
1481
|
+
@tree_data += @polyline_styles.sub(/CHIX/, chix.to_s).sub(/CHIY/, chiy.to_s)
|
|
1482
|
+
.sub(/MIDX1/, mx1.to_s).sub(/MIDY1/, my1.to_s)
|
|
1483
|
+
.sub(/MIDX2/, mx2.to_s).sub(/MIDY2/, my2.to_s)
|
|
1484
|
+
.sub(/PARX/, parx.to_s).sub(/PARY/, pary.to_s).sub(" />", "#{dash} />")
|
|
1485
|
+
end
|
|
1486
|
+
points.each_cons(2).sum { |(ax, ay), (bx, by)| Math.hypot(bx - ax, by - ay) }
|
|
1344
1487
|
end
|
|
1345
1488
|
|
|
1346
1489
|
# Draw a triangle between child/parent elements
|
|
1347
|
-
def triangle_to_parent(parent, child)
|
|
1490
|
+
def triangle_to_parent(parent, child, _joints = [])
|
|
1348
1491
|
return if child.horizontal_indent.zero?
|
|
1349
1492
|
|
|
1350
1493
|
if @direction == "ltr"
|
|
@@ -1373,7 +1516,7 @@ module RSyntaxTree
|
|
|
1373
1516
|
# bottom-to-top examples are derivations, which draw rules instead.
|
|
1374
1517
|
x1 = child.horizontal_indent
|
|
1375
1518
|
x2 = child.horizontal_indent + child.content_width
|
|
1376
|
-
x3 = parent.horizontal_indent + parent.content_width / 2
|
|
1519
|
+
x3 = parent.horizontal_indent + parent.content_width / 2.0
|
|
1377
1520
|
child_above = child.vertical_indent <= parent.vertical_indent
|
|
1378
1521
|
m = child.ink_top && parent.ink_bottom ? @global[:single_x_metrics].height * @global[:vmargin] : nil
|
|
1379
1522
|
if child_above
|
|
@@ -189,7 +189,7 @@ module RSyntaxTree
|
|
|
189
189
|
|
|
190
190
|
label = extract_label(element)
|
|
191
191
|
escaped_label = escape_latex(label)
|
|
192
|
-
head = "#{escaped_label}#{region_option(element)}"
|
|
192
|
+
head = "#{escaped_label}#{leaf_edge_option(element)}#{region_option(element)}"
|
|
193
193
|
|
|
194
194
|
children = element.children
|
|
195
195
|
indent_str = " " * indent
|
|
@@ -202,6 +202,17 @@ module RSyntaxTree
|
|
|
202
202
|
end
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
+
# The edge down to a word is dashed when leafstyle asks for it, every
|
|
206
|
+
# piece of it: leaf alignment runs it through joints, and forest gives
|
|
207
|
+
# each joint the edge from the node above. Added to the edge style the
|
|
208
|
+
# tree sets, not in place of it, so the width holds.
|
|
209
|
+
def leaf_edge_option(element)
|
|
210
|
+
return "" unless @params[:leafstyle] == "dashed"
|
|
211
|
+
|
|
212
|
+
element = @element_list.get_id(element.children.first) while element.inserted_joint
|
|
213
|
+
element.type == ETYPE_LEAF ? ", edge+=dashed" : ""
|
|
214
|
+
end
|
|
215
|
+
|
|
205
216
|
# forest node option that paints a semi-transparent plane behind the whole
|
|
206
217
|
# subtree governed by +element+ (region shade). Uses forest's "fit to=tree"
|
|
207
218
|
# on the background layer so the plane sits below the tree. Empty string
|
data/lib/rsyntaxtree/version.rb
CHANGED
data/lib/rsyntaxtree.rb
CHANGED
|
@@ -55,13 +55,14 @@ OPTION_VALUES = {
|
|
|
55
55
|
# an alias until 3.0. It collided with a code-intelligence format of the
|
|
56
56
|
# same initials, and claimed an interchange nobody else had implemented.
|
|
57
57
|
format: FORMATS + ["lsif"],
|
|
58
|
-
leafstyle: %w[auto triangle bar nothing],
|
|
58
|
+
leafstyle: %w[auto triangle bar dashed nothing],
|
|
59
59
|
fontstyle: ["sans", "serif", "cjk", "mono", "noto-sans", "noto-serif", "noto-sans-mono", "cjk zenhei"],
|
|
60
60
|
color: %w[modern traditional gray grey off none on true false],
|
|
61
61
|
tidy: %w[off symmetric low medium high on compact true false],
|
|
62
62
|
direction: %w[ttb ltr btt],
|
|
63
63
|
hyphen: %w[markup literal],
|
|
64
|
-
derivation: %w[on off true false yes no 0 1 none]
|
|
64
|
+
derivation: %w[on off true false yes no 0 1 none],
|
|
65
|
+
leaf_align: %w[off words subtrees]
|
|
65
66
|
}.freeze
|
|
66
67
|
|
|
67
68
|
NUMERIC_RANGES = {
|
|
@@ -96,6 +97,7 @@ DEFAULT_OPTS = {
|
|
|
96
97
|
transparent: "off",
|
|
97
98
|
polyline: "off",
|
|
98
99
|
hide_default_connectors: "off",
|
|
100
|
+
leaf_align: "off",
|
|
99
101
|
mirror: "off",
|
|
100
102
|
tidy: "off",
|
|
101
103
|
hspacing: 1.0,
|
|
@@ -306,6 +308,8 @@ module RSyntaxTree
|
|
|
306
308
|
end
|
|
307
309
|
when :transparent, :polyline, :hide_default_connectors, :mirror, :derivation
|
|
308
310
|
new_params[key] = switched_on?(value)
|
|
311
|
+
when :leaf_align
|
|
312
|
+
new_params[key] = value.to_s
|
|
309
313
|
when :color
|
|
310
314
|
new_params[key] = case value.to_s
|
|
311
315
|
when "modern", "on", "true"
|
|
@@ -693,16 +697,12 @@ module RSyntaxTree
|
|
|
693
697
|
end
|
|
694
698
|
|
|
695
699
|
def draw_svg
|
|
696
|
-
|
|
697
|
-
sp.parse
|
|
698
|
-
graph = SVGGraph.new(sp.get_elementlist, @params, @global)
|
|
700
|
+
graph = SVGGraph.new(parsed_element_list, @params, @global)
|
|
699
701
|
graph.svg_data
|
|
700
702
|
end
|
|
701
703
|
|
|
702
704
|
def draw_json
|
|
703
|
-
|
|
704
|
-
sp.parse
|
|
705
|
-
graph = JSONGraph.new(sp.get_elementlist, @params, @global)
|
|
705
|
+
graph = JSONGraph.new(parsed_element_list, @params, @global)
|
|
706
706
|
graph.json_data
|
|
707
707
|
end
|
|
708
708
|
|
|
@@ -712,10 +712,20 @@ module RSyntaxTree
|
|
|
712
712
|
end
|
|
713
713
|
|
|
714
714
|
def draw_tikz(standalone: false, font: nil)
|
|
715
|
+
generator = TikZGenerator.new(parsed_element_list, @params)
|
|
716
|
+
generator.generate(standalone: standalone, font: font)
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
private
|
|
720
|
+
|
|
721
|
+
def parsed_element_list
|
|
715
722
|
sp = StringParser.new(@params[:data].gsub('&', '&'), @params[:fontset], @params[:fontsize], @global)
|
|
716
723
|
sp.parse
|
|
717
|
-
|
|
718
|
-
|
|
724
|
+
list = sp.get_elementlist
|
|
725
|
+
if @params[:leaf_align] != "off" && @params[:derivation] != true
|
|
726
|
+
list.align_leaves(@params[:leaf_align], @params[:fontset], @params[:fontsize], @global)
|
|
727
|
+
end
|
|
728
|
+
list
|
|
719
729
|
end
|
|
720
730
|
end
|
|
721
731
|
end
|
data/rsyntaxtree.gemspec
CHANGED
|
@@ -19,8 +19,8 @@ Gem::Specification.new do |s|
|
|
|
19
19
|
# the scripts that build them, the tests, the CI and container setup, the
|
|
20
20
|
# bundle and the rake tasks — and whoever wants those clones the repository.
|
|
21
21
|
development = ["docs/", "dev/", "img/", "test/", ".github/"]
|
|
22
|
-
development_files = [".
|
|
23
|
-
"Dockerfile", "Gemfile", "Rakefile"]
|
|
22
|
+
development_files = [".dockerignore", ".gitattributes", ".gitignore",
|
|
23
|
+
".ruby-version", "Dockerfile", "Gemfile", "Rakefile"]
|
|
24
24
|
s.files = `git ls-files`.split("\n").reject do |path|
|
|
25
25
|
development.any? { |dir| path.start_with?(dir) } ||
|
|
26
26
|
development_files.include?(path)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rsyntaxtree
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yoichiro Hasebe
|
|
@@ -115,7 +115,6 @@ executables:
|
|
|
115
115
|
extensions: []
|
|
116
116
|
extra_rdoc_files: []
|
|
117
117
|
files:
|
|
118
|
-
- ".dockerignore"
|
|
119
118
|
- CHANGELOG.md
|
|
120
119
|
- CITATION.cff
|
|
121
120
|
- CODE_OF_CONDUCT.md
|