rsyntaxtree 1.13.2 → 2.0.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 +211 -23
- data/README.md +57 -4
- data/bin/rsyntaxtree +10 -11
- data/lib/rsyntaxtree/base_graph.rb +17 -8
- data/lib/rsyntaxtree/element.rb +35 -3
- data/lib/rsyntaxtree/lsif_graph.rb +9 -3
- data/lib/rsyntaxtree/markup_parser.rb +16 -4
- data/lib/rsyntaxtree/notation_core.md +108 -42
- data/lib/rsyntaxtree/notation_examples.md +97 -5
- data/lib/rsyntaxtree/string_parser.rb +50 -1
- data/lib/rsyntaxtree/svg_graph.rb +271 -15
- data/lib/rsyntaxtree/tikz_generator.rb +9 -0
- data/lib/rsyntaxtree/version.rb +1 -1
- data/lib/rsyntaxtree.rb +93 -68
- data/rsyntaxtree.gemspec +4 -3
- metadata +1 -29
|
@@ -24,6 +24,10 @@ module RSyntaxTree
|
|
|
24
24
|
# a darker shade of the same color — keeping the region clearly bounded on
|
|
25
25
|
# a white page without a per-color "darker shade" lookup.
|
|
26
26
|
REGION_STROKE_OPACITY = 0.55
|
|
27
|
+
# The plane a sheared figure lies on. Lighter than a region shade: a
|
|
28
|
+
# region marks part of a figure out from the rest, and the plane is
|
|
29
|
+
# behind all of it — at the region's own opacity the whole page darkens.
|
|
30
|
+
SHEAR_PLANE_FILL_OPACITY = 0.12
|
|
27
31
|
|
|
28
32
|
attr_accessor :width, :height
|
|
29
33
|
|
|
@@ -41,11 +45,17 @@ module RSyntaxTree
|
|
|
41
45
|
@fontstyle = params[:fontstyle]
|
|
42
46
|
@polyline = params[:polyline] == true
|
|
43
47
|
@direction = params[:direction] || "ttb"
|
|
48
|
+
# The angle arrives in degrees, positive leaning the top to the right.
|
|
49
|
+
# What the transform wants is the x-shift per unit of y, and y grows
|
|
50
|
+
# downward here, so the tangent changes sign on the way in.
|
|
51
|
+
@shear_deg = (params[:shear] || 0).to_f
|
|
52
|
+
@shear_k = (-Math.tan(@shear_deg * Math::PI / 180)).round(3)
|
|
53
|
+
@shear_plane = params[:shear_plane] || "on"
|
|
44
54
|
@line_styles = "<line style='fill: none; stroke:#{@col_line}; stroke-width:#{@global[:stroke_normal]}; stroke-linejoin:round; stroke-linecap:round;' x1='X1' y1='Y1' x2='X2' y2='Y2' />\n"
|
|
45
55
|
@polyline_styles = "<polyline style='stroke:#{@col_line}; stroke-width:#{@global[:stroke_normal]}; fill:none; stroke-linejoin:round; stroke-linecap:round;'
|
|
46
56
|
points='CHIX CHIY MIDX1 MIDY1 MIDX2 MIDY2 PARX PARY' />\n"
|
|
47
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"
|
|
48
|
-
@text_styles = "<text white-space='pre' alignment-baseline='text-top' style='fill: COLOR;
|
|
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"
|
|
49
59
|
@tree_data = String.new
|
|
50
60
|
@visited_x = {}
|
|
51
61
|
@visited_y = {}
|
|
@@ -90,10 +100,50 @@ module RSyntaxTree
|
|
|
90
100
|
y2 = new_y2
|
|
91
101
|
end
|
|
92
102
|
|
|
103
|
+
# A sheared figure needs a canvas of its own shape. The plane is laid
|
|
104
|
+
# out in the space the tree was drawn in — everything inside the shear
|
|
105
|
+
# group shares it — so it is the union of all the ink there: the
|
|
106
|
+
# elements, the region shades, the movement rails, and a rule's name
|
|
107
|
+
# hanging past the last box. The canvas is that plane's four corners
|
|
108
|
+
# after the shear, with the usual margin around them.
|
|
109
|
+
unless @shear_k.zero?
|
|
110
|
+
gap = @global[:h_gap_between_nodes]
|
|
111
|
+
els = @element_list.get_elements
|
|
112
|
+
ink_l = els.map(&:horizontal_indent).min
|
|
113
|
+
ink_t = els.map(&:vertical_indent).min
|
|
114
|
+
ink_r = els.map { |e| e.horizontal_indent + e.content_width }.max
|
|
115
|
+
ink_b = els.map { |e| e.vertical_indent + e.content_height }.max
|
|
116
|
+
ink_r = @rule_name_edge if @rule_name_edge.to_f > ink_r
|
|
117
|
+
[@region_bounds, @path_bounds].compact.each do |b|
|
|
118
|
+
ink_l = [ink_l, b[:min_x]].min
|
|
119
|
+
ink_t = [ink_t, b[:min_y]].min
|
|
120
|
+
ink_r = [ink_r, b[:max_x]].max
|
|
121
|
+
ink_b = [ink_b, b[:max_y]].max
|
|
122
|
+
end
|
|
123
|
+
pad = gap * 1.5
|
|
124
|
+
@plane_rect = { x: (ink_l - pad).round(3), y: (ink_t - pad).round(3),
|
|
125
|
+
w: (ink_r - ink_l + pad * 2).round(3), h: (ink_b - ink_t + pad * 2).round(3) }
|
|
126
|
+
cx0 = @plane_rect[:x] - gap
|
|
127
|
+
cx1 = @plane_rect[:x] + @plane_rect[:w] + gap
|
|
128
|
+
cy0 = @plane_rect[:y] - gap
|
|
129
|
+
cy1 = @plane_rect[:y] + @plane_rect[:h] + gap
|
|
130
|
+
sheared = [cx0 + @shear_k * cy0, cx0 + @shear_k * cy1,
|
|
131
|
+
cx1 + @shear_k * cy0, cx1 + @shear_k * cy1]
|
|
132
|
+
x1 = sheared.min.round(3)
|
|
133
|
+
x2 = (sheared.max - sheared.min).round(3)
|
|
134
|
+
y1 = cy0.round(3)
|
|
135
|
+
y2 = (cy1 - cy0).round(3)
|
|
136
|
+
@width = x2
|
|
137
|
+
@height = y2
|
|
138
|
+
end
|
|
139
|
+
|
|
93
140
|
extra_lines = @extra_lines.join("\n")
|
|
94
141
|
|
|
95
|
-
|
|
96
|
-
|
|
142
|
+
# Rounded like the stroke widths are: the arithmetic leaves float dust
|
|
143
|
+
# (13.600000000000001) in an attribute of every figure, and the SVG is a
|
|
144
|
+
# deliverable people open in editors.
|
|
145
|
+
as2 = (@global[:h_gap_between_nodes] * 1.0).round(3)
|
|
146
|
+
as4 = (as2 * 3).round(3)
|
|
97
147
|
# The hatch density follows the type size the way the enclosure it
|
|
98
148
|
# fills does: the cell is 10/32 of the font size, the line 4/32 —
|
|
99
149
|
# the ratios the absolute 10 and 4 had at the default size.
|
|
@@ -142,10 +192,40 @@ module RSyntaxTree
|
|
|
142
192
|
# below tree connectors and labels.
|
|
143
193
|
shades = @region_shades.join
|
|
144
194
|
|
|
145
|
-
if @
|
|
146
|
-
|
|
195
|
+
if @shear_k.zero?
|
|
196
|
+
if @transparent
|
|
197
|
+
header + shades + @tree_data + extra_lines + footer
|
|
198
|
+
else
|
|
199
|
+
header + rect + shades + @tree_data + extra_lines + footer
|
|
200
|
+
end
|
|
147
201
|
else
|
|
148
|
-
|
|
202
|
+
# The whole picture shears as one: the plane, the shades, the tree
|
|
203
|
+
# and the rails all live inside the group, so an affine map carries
|
|
204
|
+
# every incidence with it — nothing can newly touch or cross. The
|
|
205
|
+
# background stays outside, painted to the sheared canvas.
|
|
206
|
+
# No plane on a transparent background: what a clear background is
|
|
207
|
+
# for is laying the figure over something else, and an opaque sheet
|
|
208
|
+
# under it is the one thing that would defeat that.
|
|
209
|
+
plane = +""
|
|
210
|
+
if @shear_plane != "off" && !@transparent
|
|
211
|
+
color = @shear_plane == "on" ? REGION_DEFAULT_COLOR : @shear_plane
|
|
212
|
+
radius = @global[:height_connector_to_text] / 2.0
|
|
213
|
+
# Fill alone, no edge. A region shade is bounded because it marks
|
|
214
|
+
# one part of a figure off from the rest; the plane is under all of
|
|
215
|
+
# it, and a line around the whole drawing reads as a frame.
|
|
216
|
+
plane = "<rect x=\"#{@plane_rect[:x]}\" y=\"#{@plane_rect[:y]}\" " \
|
|
217
|
+
"width=\"#{@plane_rect[:w]}\" height=\"#{@plane_rect[:h]}\" " \
|
|
218
|
+
"rx=\"#{radius}\" ry=\"#{radius}\" " \
|
|
219
|
+
"fill=\"#{color}\" fill-opacity=\"#{SHEAR_PLANE_FILL_OPACITY}\" " \
|
|
220
|
+
"stroke=\"none\" />\n"
|
|
221
|
+
end
|
|
222
|
+
body = "<g transform=\"matrix(1,0,#{@shear_k},1,0,0)\">\n" +
|
|
223
|
+
plane + shades + @tree_data + extra_lines + "</g>\n"
|
|
224
|
+
if @transparent
|
|
225
|
+
header + body + footer
|
|
226
|
+
else
|
|
227
|
+
header + rect + body + footer
|
|
228
|
+
end
|
|
149
229
|
end
|
|
150
230
|
end
|
|
151
231
|
|
|
@@ -333,6 +413,20 @@ module RSyntaxTree
|
|
|
333
413
|
"#{dasharray}#{markers}/>"
|
|
334
414
|
end
|
|
335
415
|
|
|
416
|
+
# Where the movement paths have been, as a box. The canvas learns of a
|
|
417
|
+
# rail through @width and @height, which only say how far right and down
|
|
418
|
+
# anything got; the plane behind a sheared figure has to enclose the rail
|
|
419
|
+
# on every side, so the corners themselves are kept.
|
|
420
|
+
def note_path_extent(points)
|
|
421
|
+
xs = points.map(&:first)
|
|
422
|
+
ys = points.map(&:last)
|
|
423
|
+
b = (@path_bounds ||= { min_x: xs.min, min_y: ys.min, max_x: xs.max, max_y: ys.max })
|
|
424
|
+
b[:min_x] = [b[:min_x], xs.min].min
|
|
425
|
+
b[:min_y] = [b[:min_y], ys.min].min
|
|
426
|
+
b[:max_x] = [b[:max_x], xs.max].max
|
|
427
|
+
b[:max_y] = [b[:max_y], ys.max].max
|
|
428
|
+
end
|
|
429
|
+
|
|
336
430
|
def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none)
|
|
337
431
|
spacing = @global[:h_gap_between_nodes] * 1.25
|
|
338
432
|
min_bulge = @global[:height_connector_to_text]
|
|
@@ -365,6 +459,8 @@ module RSyntaxTree
|
|
|
365
459
|
new_s_y = s_y + s_offset
|
|
366
460
|
new_t_y = t_y + t_offset
|
|
367
461
|
|
|
462
|
+
note_path_extent([[s_x, new_s_y], [new_x, new_s_y],
|
|
463
|
+
[new_x, new_t_y], [t_x, new_t_y]])
|
|
368
464
|
@extra_lines << generate_path([[s_x, new_s_y], [new_x, new_s_y],
|
|
369
465
|
[new_x, new_t_y], [t_x, new_t_y]],
|
|
370
466
|
@col_path, dashed: dashed, radius: corner_radius,
|
|
@@ -383,6 +479,8 @@ module RSyntaxTree
|
|
|
383
479
|
new_s_x = s_x - s_offset
|
|
384
480
|
new_t_x = t_x - t_offset
|
|
385
481
|
|
|
482
|
+
note_path_extent([[new_s_x, s_y], [new_s_x, new_y],
|
|
483
|
+
[new_t_x, new_y], [new_t_x, t_y]])
|
|
386
484
|
@extra_lines << generate_path([[new_s_x, s_y], [new_s_x, new_y],
|
|
387
485
|
[new_t_x, new_y], [new_t_x, t_y]],
|
|
388
486
|
@col_path, dashed: dashed, radius: corner_radius,
|
|
@@ -421,6 +519,7 @@ module RSyntaxTree
|
|
|
421
519
|
this_x = 0
|
|
422
520
|
this_y = 0
|
|
423
521
|
prev_line_height = nil
|
|
522
|
+
first_baseline = nil
|
|
424
523
|
bc = { x: text_x - enclosure_room, y: top, width: element.content_width, height: nil }
|
|
425
524
|
element.content.each_with_index do |l, idx|
|
|
426
525
|
case l[:type]
|
|
@@ -461,6 +560,7 @@ module RSyntaxTree
|
|
|
461
560
|
# after it has to clear all of it.
|
|
462
561
|
text_y += prev_line_height if idx != 0 && prev_line_height
|
|
463
562
|
text_y += l[:top_room].to_f
|
|
563
|
+
first_baseline ||= text_y
|
|
464
564
|
prev_line_height = l[:elements].map { |e| e[:height] }.max
|
|
465
565
|
|
|
466
566
|
l[:elements].each do |e|
|
|
@@ -493,6 +593,36 @@ module RSyntaxTree
|
|
|
493
593
|
end
|
|
494
594
|
|
|
495
595
|
element.content_height = bc[:height]
|
|
596
|
+
|
|
597
|
+
# Where the label's ink is, for the symmetric clearances of vmargin.
|
|
598
|
+
# A drawn shape — an enclosure, or a label that is one matrix — hands
|
|
599
|
+
# over its own edges; a plain label hands over the font's band around
|
|
600
|
+
# its first and last baselines, so the connectors of one level start
|
|
601
|
+
# level with each other whatever letters happen to hang below.
|
|
602
|
+
drawn_shape = element.enclosure != :none ||
|
|
603
|
+
element.content.any? do |line|
|
|
604
|
+
line[:type] == :text &&
|
|
605
|
+
line[:elements].any? { |e| e[:decoration].include?(:matrix) }
|
|
606
|
+
end
|
|
607
|
+
if drawn_shape
|
|
608
|
+
element.ink_top = bc[:y]
|
|
609
|
+
element.ink_bottom = bc[:y] + bc[:height]
|
|
610
|
+
elsif first_baseline
|
|
611
|
+
# The label's own ink, not the font's whole band: measured from the
|
|
612
|
+
# first and last lines of text as drawn. What the connectors then
|
|
613
|
+
# keep clear of is the union of these across a row (see row_ink), so
|
|
614
|
+
# the ends stay level within a row while a row of capitals pays no
|
|
615
|
+
# tax for descenders nobody wrote.
|
|
616
|
+
text_lines = element.content.select { |l| l[:type] == :text }
|
|
617
|
+
first_text = text_lines.first[:elements].map { |e| e[:text].to_s }.join.gsub(WHITESPACE_BLOCK, " ")
|
|
618
|
+
last_text = text_lines.last[:elements].map { |e| e[:text].to_s }.join.gsub(WHITESPACE_BLOCK, " ")
|
|
619
|
+
f_m = FontMetrics.get_metrics(first_text.strip.empty? ? "X" : first_text,
|
|
620
|
+
@fontset[:family], @fontsize.to_f, :normal, :normal)
|
|
621
|
+
l_m = FontMetrics.get_metrics(last_text.strip.empty? ? "X" : last_text,
|
|
622
|
+
@fontset[:family], @fontsize.to_f, :normal, :normal)
|
|
623
|
+
element.ink_top = first_baseline - f_m.ink_above
|
|
624
|
+
element.ink_bottom = text_y + (l_m.ink_height - l_m.ink_above)
|
|
625
|
+
end
|
|
496
626
|
@tree_data += text_data.sub(/CONTENT/, new_text)
|
|
497
627
|
end
|
|
498
628
|
|
|
@@ -621,11 +751,42 @@ module RSyntaxTree
|
|
|
621
751
|
# the sizes Element#measure_lines worked out, inside its own brackets.
|
|
622
752
|
def render_matrix(e, this_x, text_y, element, col)
|
|
623
753
|
out = +""
|
|
754
|
+
advance = e[:width]
|
|
755
|
+
# The room measured in front of the block, if it follows something in
|
|
756
|
+
# the same cell. Taken off this_x here so the bracket starts after it,
|
|
757
|
+
# and left in the advance so the row is as wide as it was measured.
|
|
758
|
+
lead = e[:matrix_lead].to_f
|
|
759
|
+
this_x += lead
|
|
624
760
|
inner_x = this_x + @global[:width_half_x] * MATRIX_BRACKET_ROOM
|
|
625
761
|
baseline = text_y
|
|
626
762
|
prev_height = nil
|
|
627
763
|
|
|
628
764
|
e[:matrix].each_with_index do |line, idx|
|
|
765
|
+
# A rule across the matrix, the way one runs across a label: it takes
|
|
766
|
+
# a row of its own and spans the width inside the brackets, so a
|
|
767
|
+
# feature structure can separate its type from its features and a
|
|
768
|
+
# tableau its header from its candidates.
|
|
769
|
+
if %i[border bborder].include?(line[:type])
|
|
770
|
+
# Placed from the baseline above it, the way a rule across a label
|
|
771
|
+
# is: a third of a line under the row before, the rest of the gap
|
|
772
|
+
# under the rule. Measuring from the row it is about to open
|
|
773
|
+
# instead put the space on the wrong side — more above the rule
|
|
774
|
+
# than below, where the ascenders of the next row need it.
|
|
775
|
+
above = baseline
|
|
776
|
+
baseline += prev_height if idx.positive? && prev_height
|
|
777
|
+
prev_height = line[:height].to_f
|
|
778
|
+
y = if idx.zero?
|
|
779
|
+
above - prev_height - @global[:single_line_height] / 8
|
|
780
|
+
else
|
|
781
|
+
above + prev_height - @global[:single_line_height] / 8
|
|
782
|
+
end
|
|
783
|
+
inner_w = e[:width] - (inner_x - this_x) * 2
|
|
784
|
+
stroke = line[:type] == :bborder ? @global[:stroke_bold] : @global[:stroke_normal]
|
|
785
|
+
@extra_lines << "<line style=\"stroke:#{col}; fill:none; stroke-linecap:round; " \
|
|
786
|
+
"stroke-width:#{stroke}; \" x1=\"#{inner_x}\" y1=\"#{y}\" " \
|
|
787
|
+
"x2=\"#{inner_x + inner_w}\" y2=\"#{y}\"></line>"
|
|
788
|
+
next
|
|
789
|
+
end
|
|
629
790
|
next unless line[:type] == :text
|
|
630
791
|
|
|
631
792
|
baseline += prev_height if idx.positive? && prev_height
|
|
@@ -648,8 +809,8 @@ module RSyntaxTree
|
|
|
648
809
|
# stacked blocks touching.
|
|
649
810
|
padding = @global[:single_x_metrics].height * MATRIX_VERTICAL_ROOM
|
|
650
811
|
top = text_y - @global[:single_x_metrics].height * 0.8 - padding
|
|
651
|
-
draw_bracket(this_x, top, e[:width], e[:matrix_height] + padding * 2, col)
|
|
652
|
-
[out, this_x +
|
|
812
|
+
draw_bracket(this_x, top, e[:width] - lead, e[:matrix_height] + padding * 2, col)
|
|
813
|
+
[out, this_x + advance - lead]
|
|
653
814
|
end
|
|
654
815
|
|
|
655
816
|
def draw_rectangle(x1, y1, width, height, col, bline = false)
|
|
@@ -1013,14 +1174,62 @@ module RSyntaxTree
|
|
|
1013
1174
|
# the parent is the lower of the two, and keeping the top-to-bottom edges
|
|
1014
1175
|
# sent the line back up through both labels. An empty label has no text to
|
|
1015
1176
|
# clear, so the line runs to its middle.
|
|
1177
|
+
# The ink extents of a row: the union, across every plain label standing
|
|
1178
|
+
# at one height, of where its type actually reaches. An endpoint measured
|
|
1179
|
+
# from the label's own ink alone gives every label equal air but leaves
|
|
1180
|
+
# the ends of a row's connectors ragged — a line reaches lower over `on`
|
|
1181
|
+
# than over `the`; measured from the whole font's band the ends are level
|
|
1182
|
+
# but a row of capitals pays for descenders nobody wrote. The row's own
|
|
1183
|
+
# union keeps the ends level and charges only for what the row contains.
|
|
1184
|
+
def row_ink
|
|
1185
|
+
@row_ink ||= @element_list.get_elements.each_with_object({}) do |e, rows|
|
|
1186
|
+
next unless e.ink_top && e.ink_bottom
|
|
1187
|
+
next if e.enclosure != :none
|
|
1188
|
+
|
|
1189
|
+
row = (rows[e.vertical_indent.round(1)] ||= { top: e.ink_top, bottom: e.ink_bottom })
|
|
1190
|
+
row[:top] = [row[:top], e.ink_top].min
|
|
1191
|
+
row[:bottom] = [row[:bottom], e.ink_bottom].max
|
|
1192
|
+
end
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
# Where a connector clears an element on the vmargin geometry: the row's
|
|
1196
|
+
# ink for a plain label, the drawn edge for an enclosed one.
|
|
1197
|
+
def ink_over(element)
|
|
1198
|
+
row = element.enclosure == :none && row_ink[element.vertical_indent.round(1)]
|
|
1199
|
+
row ? row[:top] : element.ink_top
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
def ink_under(element)
|
|
1203
|
+
row = element.enclosure == :none && row_ink[element.vertical_indent.round(1)]
|
|
1204
|
+
row ? row[:bottom] : element.ink_bottom
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1016
1207
|
def connector_edges(child, parent)
|
|
1017
1208
|
hctt = @global[:height_connector_to_text]
|
|
1018
1209
|
child_above = child.vertical_indent <= parent.vertical_indent
|
|
1019
1210
|
|
|
1211
|
+
# Each endpoint decides for itself. A line through a chain of invisible
|
|
1212
|
+
# joints keeps one real label at most, and when the pair had to agree
|
|
1213
|
+
# on a geometry, the segments beside a joint fell back to the box
|
|
1214
|
+
# measure while their neighbours took the ink one — a row of leaves
|
|
1215
|
+
# under example 080's joints ended thirteen units apart.
|
|
1216
|
+
#
|
|
1217
|
+
# An element whose ink went unrecorded falls back to the measure the
|
|
1218
|
+
# boxes gave. Nothing reaches that now — every drawn label records its
|
|
1219
|
+
# ink — and it is kept as the answer for a label that somehow has none
|
|
1220
|
+
# rather than as a second geometry anyone can ask for.
|
|
1221
|
+
m = @global[:single_x_metrics].height * @global[:vmargin]
|
|
1222
|
+
|
|
1020
1223
|
child_y = if child.empty_label?
|
|
1021
1224
|
child.vertical_indent + child.content_height / 2
|
|
1022
1225
|
elsif child_above
|
|
1023
|
-
|
|
1226
|
+
if m && child.ink_bottom
|
|
1227
|
+
ink_under(child) + m
|
|
1228
|
+
else
|
|
1229
|
+
child.vertical_indent + child.content_height + hctt
|
|
1230
|
+
end
|
|
1231
|
+
elsif m && child.ink_top
|
|
1232
|
+
ink_over(child) - m
|
|
1024
1233
|
else
|
|
1025
1234
|
child.vertical_indent + hctt / 2
|
|
1026
1235
|
end
|
|
@@ -1028,11 +1237,31 @@ module RSyntaxTree
|
|
|
1028
1237
|
parent_y = if parent.empty_label?
|
|
1029
1238
|
parent.vertical_indent + parent.content_height / 2
|
|
1030
1239
|
elsif child_above
|
|
1031
|
-
|
|
1240
|
+
if m && parent.ink_top
|
|
1241
|
+
ink_over(parent) - m
|
|
1242
|
+
else
|
|
1243
|
+
parent.vertical_indent + hctt / 2
|
|
1244
|
+
end
|
|
1245
|
+
elsif m && parent.ink_bottom
|
|
1246
|
+
ink_under(parent) + m
|
|
1032
1247
|
else
|
|
1033
1248
|
parent.vertical_indent + parent.content_height + hctt
|
|
1034
1249
|
end
|
|
1035
1250
|
|
|
1251
|
+
# A tight pitch can put the two endpoints past each other — the gap
|
|
1252
|
+
# between the inks smaller than the clearance on both sides — and a
|
|
1253
|
+
# crossed pair draws the stroke back up through a label. Both then
|
|
1254
|
+
# settle at the middle of the gap, which is the one point that is
|
|
1255
|
+
# clear of the ink on either side whenever the gap exists at all.
|
|
1256
|
+
if m
|
|
1257
|
+
upper_pt, lower_pt = child_above ? [child_y, parent_y] : [parent_y, child_y]
|
|
1258
|
+
if upper_pt > lower_pt
|
|
1259
|
+
mid = (upper_pt + lower_pt) / 2.0
|
|
1260
|
+
child_y = mid
|
|
1261
|
+
parent_y = mid
|
|
1262
|
+
end
|
|
1263
|
+
end
|
|
1264
|
+
|
|
1036
1265
|
[child_y, parent_y]
|
|
1037
1266
|
end
|
|
1038
1267
|
|
|
@@ -1122,14 +1351,41 @@ module RSyntaxTree
|
|
|
1122
1351
|
x3 = parent.horizontal_indent + parent.content_width + @global[:height_connector_to_text]
|
|
1123
1352
|
y3 = parent_y_center
|
|
1124
1353
|
else
|
|
1125
|
-
#
|
|
1126
|
-
#
|
|
1354
|
+
# The triangle spans the child's text and points at the parent. Which
|
|
1355
|
+
# way it points is the question the connectors ask as `child_above`,
|
|
1356
|
+
# and the answer is not always the same: bottom-to-top turns the
|
|
1357
|
+
# finished layout over, so the child sits above the parent there and
|
|
1358
|
+
# the base and the apex trade sides. Drawn as though the child were
|
|
1359
|
+
# always below, a bottom-to-top triangle came out inside out — its
|
|
1360
|
+
# base struck through the leaf and its apex through the node — for as
|
|
1361
|
+
# long as the option has existed. No gallery figure carries one: the
|
|
1362
|
+
# bottom-to-top examples are derivations, which draw rules instead.
|
|
1127
1363
|
x1 = child.horizontal_indent
|
|
1128
|
-
y1 = child.vertical_indent + @global[:height_connector_to_text] / 2
|
|
1129
1364
|
x2 = child.horizontal_indent + child.content_width
|
|
1130
|
-
y2 = child.vertical_indent + @global[:height_connector_to_text] / 2
|
|
1131
1365
|
x3 = parent.horizontal_indent + parent.content_width / 2
|
|
1132
|
-
|
|
1366
|
+
child_above = child.vertical_indent <= parent.vertical_indent
|
|
1367
|
+
m = child.ink_top && parent.ink_bottom ? @global[:single_x_metrics].height * @global[:vmargin] : nil
|
|
1368
|
+
if child_above
|
|
1369
|
+
y1 = m ? ink_under(child) + m : child.vertical_indent + child.content_height + @global[:height_connector_to_text]
|
|
1370
|
+
y3 = m ? ink_over(parent) - m : parent.vertical_indent + @global[:height_connector_to_text] / 2
|
|
1371
|
+
# The apex must stay clear of the base, or the figure folds through
|
|
1372
|
+
# itself; where there is no room for both clearances they meet in
|
|
1373
|
+
# the middle of what room there is.
|
|
1374
|
+
if m && y3 < y1
|
|
1375
|
+
mid = (y1 + y3) / 2.0
|
|
1376
|
+
y1 = mid
|
|
1377
|
+
y3 = mid
|
|
1378
|
+
end
|
|
1379
|
+
else
|
|
1380
|
+
y1 = m ? ink_over(child) - m : child.vertical_indent + @global[:height_connector_to_text] / 2
|
|
1381
|
+
y3 = m ? ink_under(parent) + m : parent.vertical_indent + parent.content_height + @global[:height_connector_to_text]
|
|
1382
|
+
if m && y3 > y1
|
|
1383
|
+
mid = (y1 + y3) / 2.0
|
|
1384
|
+
y1 = mid
|
|
1385
|
+
y3 = mid
|
|
1386
|
+
end
|
|
1387
|
+
end
|
|
1388
|
+
y2 = y1
|
|
1133
1389
|
end
|
|
1134
1390
|
|
|
1135
1391
|
polygon_data = @polygon_styles.sub(/X1/, x1.to_s)
|
|
@@ -40,6 +40,15 @@ module RSyntaxTree
|
|
|
40
40
|
"at the top. Use png, svg or pdf for this figure.",
|
|
41
41
|
retryable: false)
|
|
42
42
|
end
|
|
43
|
+
# Refused rather than quietly drawn straight: a shear is the whole point
|
|
44
|
+
# of the figure that asks for it, and TikZ's own slant (xslant) leaves
|
|
45
|
+
# the bounding box unaware of the lean, so standalone output crops.
|
|
46
|
+
if params[:shear].to_f != 0
|
|
47
|
+
raise RSTError.new(+"Error: TikZ output cannot draw a sheared figure",
|
|
48
|
+
code: :invalid_option,
|
|
49
|
+
hint: "Use png, svg or pdf for this figure, or drop the shear option.",
|
|
50
|
+
retryable: false)
|
|
51
|
+
end
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
# Generate TikZ forest code
|
data/lib/rsyntaxtree/version.rb
CHANGED