rsyntaxtree 0.9.1 → 1.0.4

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.
@@ -6,358 +6,505 @@
6
6
  #==========================
7
7
  #
8
8
  # Parses an element list into an SVG tree.
9
- #
10
- # This file is part of RSyntaxTree, which is a ruby port of Andre Eisenbach's
11
- # excellent program phpSyntaxTree.
12
- #
13
9
  # Copyright (c) 2007-2021 Yoichiro Hasebe <yohasebe@gmail.com>
14
- # Copyright (c) 2003-2004 Andre Eisenbach <andre@ironcreek.net>
15
10
 
16
11
  require "tempfile"
17
- require 'graph'
18
-
19
- class SVGGraph < Graph
20
-
21
- def initialize(e_list, metrics, symmetrize, color, leafstyle, multibyte, fontstyle, font, font_cjk, font_size, margin, transparent)
22
-
23
- # Store class-specific parameters
24
- @font = multibyte ? font_cjk : font
25
- @font_size = font_size
26
- @transparent = transparent
27
-
28
- case fontstyle
29
- when /(?:sans|cjk)/
30
- @fontstyle = "\"'Noto Sans JP', 'Noto Sans', sans-serif\""
31
- @fontcss = "http://fonts.googleapis.com/earlyaccess/notosansjp.css"
32
- when /(?:serif)/
33
- @fontstyle = "\"'Noto Serif JP', 'Noto Serif', serif\""
34
- @fontcss = "https://fonts.googleapis.com/css?family=Noto+Serif+JP"
35
- when /(?:math)/
36
- @fontstyle = "\"Latin Modern Roman', sans-serif\""
37
- @fontcss = "https://cdn.jsdelivr.net/gh/sugina-dev/latin-modern-web@1.0.1/style/latinmodern-roman.css"
12
+ require 'base_graph'
13
+ require 'utils'
14
+
15
+ module RSyntaxTree
16
+ class SVGGraph < BaseGraph
17
+ attr_accessor :width, :height
18
+
19
+ def initialize(element_list, params)
20
+ @height = 0
21
+ @width = 0
22
+ @extra_lines = []
23
+ @fontset = params[:fontset]
24
+ @fontsize = params[:fontsize]
25
+ @transparent = params[:transparent]
26
+ @color = params[:color]
27
+ @fontstyle = params[:fontstyle]
28
+ @margin = params[:margin].to_i
29
+ @line_styles = "<line style='stroke:black; stroke-width:#{FONT_SCALING};' x1='X1' y1='Y1' x2='X2' y2='Y2' />\n"
30
+ @polygon_styles = "<polygon style='fill: none; stroke: black; stroke-width:#{FONT_SCALING};' points='X1 Y1 X2 Y2 X3 Y3' />\n"
31
+ @text_styles = "<text white-space='pre' alignment-baseline='text-top' style='fill: COLOR; font-size: fontsize' x='X_VALUE' y='Y_VALUE'>CONTENT</text>\n"
32
+ @tree_data = String.new
33
+ @visited_x = {}
34
+ @visited_y = {}
35
+ super(element_list, params)
38
36
  end
39
37
 
40
- @margin = margin.to_i
41
-
42
- super(e_list, metrics, symmetrize, color, leafstyle, multibyte, @fontstyle, @font_size)
43
-
44
- @line_styles = "<line style='stroke:black; stroke-width:#{FONT_SCALING};' x1='X1' y1='Y1' x2='X2' y2='Y2' />\n"
45
- @polygon_styles = "<polygon style='fill: none; stroke: black; stroke-width:#{FONT_SCALING};' points='X1 Y1 X2 Y2 X3 Y3' />\n"
46
- @text_styles = "<text letter-spacing='0' word-spacing='0' kerning='0' style='fill: COLOR; font-size: FONT_SIZE ST WA' x='X_VALUE' y='Y_VALUE' TD font-family=#{@fontstyle}>CONTENT</text>\n"
47
- @tree_data = String.new
48
- end
38
+ def svg_data
39
+ metrics = parse_list
40
+ @height = metrics[:height] + @margin * 2
41
+ @width = metrics[:width] + @margin * 2
49
42
 
50
- def get_left_most(tree_data)
51
- xs = @tree_data.scan(/x1?=['"]([^'"]+)['"]/).map{|m| m.first.to_i}
52
- xs.min
53
- end
43
+ x1 = 0 - @margin
44
+ y1 = 0 - @margin
45
+ x2 = @width + @margin
46
+ y2 = @height + @margin
47
+ extra_lines = @extra_lines.join("\n")
54
48
 
55
- def svg_data
56
- parse_list
57
- lm = get_left_most(@tree_data)
58
- width = @width - lm + @margin * 2
59
- height = @height + @margin * 2
49
+ as2 = $h_gap_between_nodes / 2 * 0.8
50
+ as = as2 / 2
60
51
 
61
- header =<<EOD
52
+ header =<<EOD
62
53
  <?xml version="1.0" standalone="no"?>
63
- <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
64
- "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
65
- <svg width="#{width}" height="#{height}" viewBox="#{-@margin + lm}, -#{@margin}, #{@width - lm + @margin * 2}, #{@height + @margin * 2}" version="1.1" xmlns="http://www.w3.org/2000/svg">
66
- <defs>
67
- <style>
68
- @import url(#{@fontcss});
69
- </style>
70
- </defs>
54
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
55
+ <svg width="#{@width}" height="#{@height}" viewBox="#{x1}, #{y1}, #{x2}, #{y2}" version="1.1" xmlns="http://www.w3.org/2000/svg">
56
+ <defs>
57
+ <marker id="arrow" markerUnits="strokeWidth" markerWidth="#{as2}" markerHeight="#{as2}" viewBox="0 0 #{as2} #{as2}" refX="#{as}" refY="0">
58
+ <polyline fill="none" stroke="#{@col_path}" stroke-width="1" points="0,#{as2},#{as},0,#{as2},#{as2}" />
59
+ </marker>
60
+ <pattern id="hatchBlack" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
61
+ <line x1="0" y="0" x2="0" y2="10" stroke="black" stroke-width="4"></line>
62
+ </pattern>
63
+ <pattern id="hatchForNode" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
64
+ <line x1="0" y="0" x2="0" y2="10" stroke="#{@col_node}" stroke-width="4"></line>
65
+ </pattern>
66
+ <pattern id="hatchForLeaf" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
67
+ <line x1="0" y="0" x2="0" y2="10" stroke="#{@col_leaf}" stroke-width="4"></line>
68
+ </pattern>
69
+ </defs>
71
70
  EOD
72
71
 
73
-
74
- rect =<<EOD
75
- <rect x="#{-@margin + lm}" y="-#{@margin}" width="#{@width - lm + @margin * 2}" height="#{@height + @margin * 2}" stroke="none" fill="white" />"
72
+ rect =<<EOD
73
+ <rect x="#{x1}" y="#{y1}" width="#{x2}" height="#{y2}" stroke="none" fill="white" />"
76
74
  EOD
77
75
 
78
- footer = "</svg>"
79
-
80
- if @transparent
81
- header + @tree_data + footer
82
- else
83
- header + rect + @tree_data + footer
84
- end
85
- end
76
+ footer = "</svg>"
86
77
 
87
- # Create a temporary file and returns only its filename
88
- def create_tempf(basename, ext, num = 10)
89
- flags = File::RDWR | File::CREAT | File::EXCL
90
- tfname = ""
91
- num.times do |i|
92
- begin
93
- tfname = "#{basename}.#{$$}.#{i}.#{ext}"
94
- tfile = File.open(tfname, flags, 0600)
95
- rescue Errno::EEXIST
96
- next
78
+ if @transparent
79
+ header + @tree_data + extra_lines + footer
80
+ else
81
+ header + rect + @tree_data + extra_lines + footer
97
82
  end
98
- tfile.close
99
- return tfname
100
83
  end
101
- end
102
84
 
103
- :private
85
+ def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none)
104
86
 
105
- # Add the element into the tree (draw it)
106
- def draw_element(x, y, w, string, type)
107
- string = string.sub(/\^\z/){""}
108
- # Calculate element dimensions and position
109
- if (type == ETYPE_LEAF) and @leafstyle == "nothing"
110
- top = row2px(y - 1) + (@font_size * 1.5)
111
- else
112
- top = row2px(y)
113
- end
114
- left = x + @m[:b_side]
115
- bottom = top + @e_height
116
- right = left + w
117
-
118
- # Split the string into the main part and the
119
- # subscript part of the element (if any)
120
- parts = string.split("_", 2)
121
- if(parts.length > 1 )
122
- main = parts[0].strip
123
- sub = parts[1].gsub(/_/, " ").strip
124
- else
125
- main = parts[0].strip
126
- sub = ""
127
- end
87
+ x_spacing = $h_gap_between_nodes * 1.25
88
+ y_spacing = $height_connector * 0.65
128
89
 
129
- if /\A\=(.+)\=\z/ =~ main
130
- main = $1
131
- main_decoration= "overline"
132
- elsif /\A\-(.+)\-\z/ =~ main
133
- main = $1
134
- main_decoration= "underline"
135
- elsif /\A\~(.+)\~\z/ =~ main
136
- main = $1
137
- main_decoration= "line-through"
138
- else
139
- main_decoration= ""
140
- end
90
+ ymax = [s_y, t_y].max
91
+ if ymax < @height
92
+ new_y = @height + y_spacing
93
+ else
94
+ new_y = ymax + y_spacing
95
+ end
141
96
 
142
- if /\A\*\*\*(.+)\*\*\*\z/ =~ main
143
- main = $1
144
- main_style = "font-style: italic"
145
- main_weight = "font-weight: bold"
146
- elsif /\A\*\*(.+)\*\*\z/ =~ main
147
- main = $1
148
- main_style = ""
149
- main_weight = "font-weight: bold"
150
- elsif /\A\*(.+)\*\z/ =~ main
151
- main = $1
152
- main_style = "font-style: italic"
153
- main_weight = ""
154
- else
155
- main_style = ""
156
- main_weight = ""
157
- end
158
97
 
159
- if /\A#(.+)#\z/ =~ main
160
- main = $1
161
- end
98
+ if @visited_x[s_x]
99
+ new_s_x = s_x - x_spacing * @visited_x[s_x]
100
+ @visited_x[s_x] += 1
101
+ else
102
+ new_s_x = s_x
103
+ @visited_x[s_x] = 1
104
+ end
162
105
 
163
- # Calculate text size for the main and the
164
- # subscript part of the element
165
- # symbols for underline/overline removed temporarily
106
+ if @visited_x[t_x]
107
+ new_t_x = t_x - x_spacing * @visited_x[t_x]
108
+ @visited_x[t_x] += 1
109
+ else
110
+ new_t_x = t_x
111
+ @visited_x[t_x] = 1
112
+ end
166
113
 
167
- main_width = 0
168
- main_height = 0
169
- main.split(/\\n/).each do |l|
170
- l_width = img_get_txt_width(l, @font, @font_size)
171
- main_width = l_width if main_width < l_width
172
- main_height += img_get_txt_height(l, @font, @font_size)
173
- end
114
+ s_y += $h_gap_between_nodes / 2
115
+ t_y += $h_gap_between_nodes / 2
116
+ new_y += $h_gap_between_nodes / 2
174
117
 
118
+ dashed = true if target_arrow == :none
175
119
 
176
- if sub != ""
177
- if /\A\=(.+)\=\z/ =~ sub
178
- sub = $1
179
- sub_decoration= "overline"
180
- elsif /\A\-(.+)\-\z/ =~ sub
181
- sub = $1
182
- sub_decoration= "underline"
183
- elsif /\A\~(.+)\~\z/ =~ sub
184
- sub = $1
185
- sub_decoration= "line-through"
120
+ if target_arrow == :single
121
+ @extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
122
+ @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
123
+ @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path ,dashed, true)
124
+ elsif target_arrow == :double
125
+ @extra_lines << generate_line(new_s_x, new_y, new_s_x, s_y, @col_path, dashed, true)
126
+ @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
127
+ @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path ,dashed, true)
186
128
  else
187
- sub_decoration= ""
129
+ @extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
130
+ @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
131
+ @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path ,dashed)
188
132
  end
189
133
 
190
- if /\A\*\*\*(.+)\*\*\*\z/ =~ sub
191
- sub = $1
192
- sub_style = "font-style: italic"
193
- sub_weight = "font-weight: bold"
194
- elsif /\A\*\*(.+)\*\*\z/ =~ sub
195
- sub = $1
196
- sub_style = ""
197
- sub_weight = "font-weight: bold"
198
- elsif /\A\*(.+)\*\z/ =~ sub
199
- sub = $1
200
- sub_style = "font-style: italic"
201
- sub_weight = ""
202
- else
203
- sub_style = ""
204
- sub_weight = ""
205
- end
206
- sub_height = img_get_txt_height(sub, @font, @font_size)
207
- sub_width = img_get_txt_width(sub.to_s, @font, @sub_size)
208
- else
209
- sub_width = 0
210
- sub_height = 0
134
+ @height = new_y if new_y > @height
211
135
  end
212
136
 
213
- if /\A#(.+)#\z/ =~ sub
214
- sub = $1
215
- end
137
+ def draw_element(element)
138
+ top = element.vertical_indent
216
139
 
217
- # Center text in the element
218
- txt_pos = left + (right - left) / 2
140
+ left = element.horizontal_indent
141
+ bottom = top +$single_line_height
142
+ right = left + element.content_width
219
143
 
220
- # Select apropriate color
221
- if(type == ETYPE_LEAF)
222
- col = @col_leaf
223
- else
224
- col = @col_node
225
- end
144
+ txt_pos = left + (right - left) / 2
226
145
 
227
- if(main[0].chr == "<" && main[-1].chr == ">")
228
- col = @col_trace
229
- end
146
+ if(element.type == ETYPE_LEAF)
147
+ col = @col_leaf
148
+ else
149
+ col = @col_node
150
+ end
230
151
 
231
- # Draw main text
232
- main_data = @text_styles.sub(/COLOR/, col)
233
- main_data = main_data.sub(/FONT_SIZE/, @font_size.to_s + "px;")
234
- main_x = txt_pos - (main_width + sub_width) / 2
235
- main_y = top + @e_height - @m[:e_padd]
236
- main_data = main_data.sub(/X_VALUE/, main_x.to_s)
237
- main_data = main_data.sub(/Y_VALUE/, main_y.to_s)
238
- if /\\n/ =~ main
239
- lines = main.split(/\\n/)
240
- new_main = ""
241
- dy = 0
242
- lines.each_with_index do |l, idx|
243
- if idx == 0
244
- dy = 0
152
+ text_data = @text_styles.sub(/COLOR/, col)
153
+ text_data = text_data.sub(/fontsize/, @fontsize.to_s + "px;")
154
+ text_x = txt_pos - element.content_width / 2
155
+ text_y = top + $single_line_height - $height_connector_to_text
156
+ text_data = text_data.sub(/X_VALUE/, text_x.to_s)
157
+ text_data = text_data.sub(/Y_VALUE/, text_y.to_s)
158
+ new_text = ""
159
+ this_x = 0
160
+ this_y = 0
161
+ bc = {:x => text_x - $h_gap_between_nodes / 2 , :y => top, :width => element.content_width + $h_gap_between_nodes, :height => nil}
162
+ element.content.each_with_index do |l, idx|
163
+ case l[:type]
164
+ when :border, :bborder
165
+ x1 = text_x
166
+ if idx == 0
167
+ text_y -= l[:height]
168
+ elsif
169
+ text_y += l[:height]
170
+ end
171
+ y1 = text_y - $single_line_height / 8
172
+ x2 = text_x + element.content_width
173
+ y2 = y1
174
+ this_width = x2 - x1
175
+ case l[:type]
176
+ when :border
177
+ stroke_width = FONT_SCALING
178
+ when :bborder
179
+ stroke_width = FONT_SCALING * 2
180
+ end
181
+ @extra_lines << "<line style=\"stroke:#{col}; stroke-width:#{stroke_width}; \" x1=\"#{x1}\" y1=\"#{y1}\" x2=\"#{x2}\" y2=\"#{y2}\"></line>"
245
182
  else
246
- dy = 1
247
- main_y += img_get_txt_height(l, @font, @font_size)
183
+ if element.enclosure == :brackets
184
+ this_x = txt_pos - element.content_width / 2
185
+ else
186
+ ewidth = 0
187
+ l[:elements].each do |e|
188
+ ewidth += e[:width]
189
+ end
190
+ this_x = txt_pos - (ewidth / 2)
191
+ end
192
+ text_y += l[:elements].map{|e| e[:height]}.max if idx != 0
193
+
194
+ l[:elements].each_with_index do |e, idx|
195
+ escaped_text = e[:text].gsub('>', '&gt;').gsub('<', '&lt;');
196
+ decorations = []
197
+ if e[:decoration].include?(:overline)
198
+ decorations << "overline"
199
+ end
200
+
201
+ if e[:decoration].include?(:underline)
202
+ decorations << "underline"
203
+ end
204
+
205
+ if e[:decoration].include?(:linethrough)
206
+ decorations << "line-through"
207
+ end
208
+ decoration ="text-decoration=\"" + decorations.join(" ") + "\""
209
+
210
+ style = "style=\""
211
+ if e[:decoration].include?(:small)
212
+ style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
213
+ this_y = text_y - (($single_X_metrics.height - $single_X_metrics.height * SUBSCRIPT_CONST) / 4) + 2
214
+ elsif e[:decoration].include?(:superscript)
215
+ style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
216
+ this_y = text_y - ($single_X_metrics.height / 4) + 1
217
+ elsif e[:decoration].include?(:subscript)
218
+ style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
219
+ this_y = text_y + 4
220
+ else
221
+ this_y = text_y
222
+ end
223
+
224
+ if e[:decoration].include?(:bold) || e[:decoration].include?(:bolditalic)
225
+ style += "font-weight: bold; "
226
+ end
227
+
228
+ if e[:decoration].include?(:italic) || e[:decoration].include?(:bolditalic)
229
+ style += "font-style: italic; "
230
+ end
231
+
232
+ style += "\""
233
+
234
+ case @fontstyle
235
+ when /(?:cjk)/
236
+ fontstyle = "'WenQuanYi Zen Hei', 'Noto Sans', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
237
+ when /(?:sans)/
238
+ if e[:cjk]
239
+ fontstyle = "'Noto Sans JP', 'Noto Sans', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
240
+ else
241
+ fontstyle = "'Noto Sans', 'Noto Sans JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
242
+ end
243
+ when /(?:serif)/
244
+ if e[:cjk]
245
+ fontstyle = "'Noto Serif JP', 'Noto Serif', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', serif"
246
+ else
247
+ fontstyle = "'Noto Serif', 'Noto Serif JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', serif"
248
+ end
249
+ end
250
+
251
+ if e[:decoration].include?(:box) || e[:decoration].include?(:circle) || e[:decoration].include?(:bar)
252
+ enc_height = e[:height]
253
+ enc_y = this_y - e[:height] * 0.8 + FONT_SCALING
254
+
255
+ if e[:text].size == 1
256
+ enc_width = e[:width]
257
+ enc_x = this_x
258
+ else
259
+ enc_width = e[:width]
260
+ enc_x = this_x
261
+ end
262
+
263
+ if e[:decoration].include?(:hatched)
264
+ case element.type
265
+ when ETYPE_LEAF
266
+ if @color
267
+ fill = "url(#hatchForLeaf)"
268
+ else
269
+ fill = "url(#hatchBlack)"
270
+ end
271
+ when ETYPE_NODE
272
+ if @color
273
+ fill = "url(#hatchForNode)"
274
+ else
275
+ fill = "url(#hatchBlack)"
276
+ end
277
+ end
278
+ else
279
+ fill = "none"
280
+ end
281
+
282
+ enc = nil
283
+ bar = nil
284
+
285
+ if e[:decoration].include?(:bstroke)
286
+ stroke_width = FONT_SCALING * 2.5
287
+ else
288
+ stroke_width = FONT_SCALING
289
+ end
290
+
291
+ if e[:decoration].include?(:box)
292
+ enc = "<rect style='stroke: #{col}; stroke-width:#{stroke_width};'
293
+ x='#{enc_x}' y='#{enc_y}'
294
+ width='#{enc_width}' height='#{enc_height}'
295
+ fill='#{fill}' />\n"
296
+ elsif e[:decoration].include?(:circle)
297
+ enc = "<rect style='stroke: #{col}; stroke-width:#{stroke_width};'
298
+ x='#{enc_x}' y='#{enc_y}' rx='#{enc_height / 2}' ry='#{enc_height / 2}'
299
+ width='#{enc_width}' height='#{enc_height}'
300
+ fill='#{fill}' />\n"
301
+ elsif e[:decoration].include?(:bar)
302
+ x1 = enc_x
303
+ y1 = enc_y + enc_height / 2
304
+ x2 = enc_x + enc_width
305
+ y2 = y1
306
+ ar_hwidth = e[:width] / 4.0
307
+ bar = "<line style='stroke:#{col}; stroke-width:#{stroke_width};' x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}'></line>\n"
308
+ @extra_lines << bar
309
+
310
+ if e[:decoration].include?(:arrow_to_l)
311
+ l_arrowhead = "<polyline stroke-linejoin='bevel' fill='none' stroke='#{col}' stroke-width='#{stroke_width}' points='#{x1 + ar_hwidth},#{y1 + ar_hwidth / 2} #{x1},#{y1} #{x1 + ar_hwidth},#{y1 - ar_hwidth / 2}' />\n"
312
+ @extra_lines << l_arrowhead
313
+ end
314
+
315
+ if e[:decoration].include?(:arrow_to_r)
316
+ r_arrowhead = "<polyline stroke-linejoin='bevel' fill='none' stroke='#{col}' stroke-width='#{stroke_width}' points='#{x2 - ar_hwidth},#{y2 - ar_hwidth / 2} #{x2},#{y2} #{x2 - ar_hwidth},#{y2 + ar_hwidth / 2}' />\n"
317
+ @extra_lines << r_arrowhead
318
+ end
319
+
320
+
321
+ end
322
+
323
+ @extra_lines << enc if enc
324
+
325
+ if e[:text].size == 1
326
+ this_x += (e[:height] - e[:content_width]) / 2
327
+ else
328
+ this_x += $width_half_X / 2
329
+ end
330
+ new_text << set_tspan(this_x, this_y, style, decoration, fontstyle, escaped_text)
331
+ if e[:text].size == 1
332
+ this_x += e[:content_width]
333
+ this_x += (e[:height] - e[:content_width]) / 2
334
+ else
335
+ this_x += e[:content_width]
336
+ this_x += $width_half_X / 2
337
+ end
338
+
339
+ elsif e[:decoration].include?(:whitespace)
340
+ this_x += e[:width]
341
+ next
342
+ else
343
+ new_text << set_tspan(this_x, this_y, style, decoration, fontstyle, escaped_text)
344
+ this_x += e[:width]
345
+ end
346
+
347
+ end
248
348
  end
249
- this_width = img_get_txt_width(l, @font, @font_size)
250
- this_x = txt_pos - (this_width + sub_width) / 2
251
- new_main << "<tspan x='#{this_x}' y='#{main_y}'>#{l}</tspan>"
252
- @height = main_y if main_y > @height
349
+ @height = text_y if text_y > @height
350
+ end
351
+ bc[:y] = bc[:y] + $height_connector_to_text * 3 / 4
352
+ bc[:height] = text_y - bc[:y] + $height_connector_to_text
353
+ if element.enclosure == :brackets
354
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + $h_gap_between_nodes / 2, bc[:y], col)
355
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col)
356
+ @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + $h_gap_between_nodes / 2, bc[:y] + bc[:height], col)
357
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width] - $h_gap_between_nodes / 2, bc[:y], col)
358
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col)
359
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y] + bc[:height], bc[:x] + bc[:width] - $h_gap_between_nodes / 2, bc[:y] + bc[:height], col)
360
+ elsif element.enclosure == :rectangle
361
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + bc[:width], bc[:y], col)
362
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col)
363
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col)
364
+ @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + bc[:width], bc[:y] + bc[:height], col)
365
+ elsif element.enclosure == :brectangle
366
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + bc[:width], bc[:y], col, false, false, 2)
367
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col, false, false, 2)
368
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col, false, false, 2)
369
+ @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + bc[:width], bc[:y] + bc[:height], col, false, false, 2)
253
370
  end
254
- main = new_main
255
- end
256
- @tree_data += main_data.sub(/TD/, "text-decoration='#{main_decoration}'")
257
- .sub(/ST/, main_style + ";")
258
- .sub(/WA/, main_weight + ";")
259
- .sub(/CONTENT/, main)
260
-
261
- # Draw subscript text
262
- if sub && sub != ""
263
- sub_data = @text_styles.sub(/COLOR/, col)
264
- sub_data = sub_data.sub(/FONT_SIZE/, @sub_size.to_s)
265
- sub_x = right - sub_width
266
- sub_y = main_y + sub_height / 4
267
- sub_data = sub_data.sub(/X_VALUE/, sub_x.to_s)
268
- sub_data = sub_data.sub(/Y_VALUE/, sub_y.to_s)
269
- @tree_data += sub_data.sub(/TD/, "text-decoration='#{sub_decoration}'")
270
- .sub(/ST/, sub_style)
271
- .sub(/WA/, sub_weight)
272
- .sub(/CONTENT/, sub)
273
- @height += sub_height / 4
274
- end
275
- end
276
371
 
277
- # Draw a line between child/parent elements
278
- def line_to_parent(fromX, fromY, fromW, toX, toW)
372
+ element.content_height = bc[:height]
373
+ @tree_data += text_data.sub(/CONTENT/, new_text)
374
+ end
279
375
 
280
- if (fromY == 0 )
281
- return
376
+ def set_tspan(this_x, this_y, style, decoration, fontstyle, text)
377
+ text.gsub!(/■+/) do |x|
378
+ num_spaces = x.size
379
+ "<tspan style='fill:none;'>" + "■" * num_spaces + "</tspan>"
380
+ end
381
+ "<tspan x='#{this_x}' y='#{this_y}' #{style} #{decoration} font-family=\"#{fontstyle}\">#{text}</tspan>\n"
282
382
  end
283
383
 
284
- fromTop = row2px(fromY)
285
- fromLeft = (fromX + fromW / 2 + @m[:b_side])
286
- toBot = (row2px(fromY - 1 ) + @e_height)
287
- toLeft = (toX + toW / 2 + @m[:b_side])
288
384
 
289
- line_data = @line_styles.sub(/X1/, fromLeft.to_s)
290
- line_data = line_data.sub(/Y1/, fromTop.to_s)
291
- line_data = line_data.sub(/X2/, toLeft.to_s)
292
- @tree_data += line_data.sub(/Y2/, toBot.to_s)
385
+ def draw_paths
386
+ rockbottom = 0
387
+ path_pool_target = {}
388
+ path_pool_other = {}
389
+ path_pool_source = {}
390
+ path_flags = []
391
+ # elist = @element_list.get_elements.reverse
392
+ elist = @element_list.get_elements
393
+
394
+ elist.each do |element|
395
+ x1 = element.horizontal_indent + element.content_width / 2
396
+ y1 = element.vertical_indent + element.content_height
397
+ y1 += $height_connector_to_text if element.enclosure != :none
398
+ et = element.path
399
+ et.each do |tr|
400
+ if /\A>(\d+)\z/ =~ tr
401
+ tr = $1
402
+ if path_pool_target[tr]
403
+ path_pool_target[tr] << [x1, y1]
404
+ else
405
+ path_pool_target[tr] = [[x1, y1]]
406
+ end
407
+ elsif path_pool_source[tr]
408
+ if path_pool_other[tr]
409
+ path_pool_other[tr] << [x1, y1]
410
+ else
411
+ path_pool_other[tr] = [[x1, y1]]
412
+ end
413
+ else
414
+ path_pool_source[tr] = [x1, y1]
415
+ end
416
+ path_flags << tr
417
+ if path_flags.tally.any?{|k, v| v > 2}
418
+ raise RSTError, "Error: input text contains a path having more than two ends:\n > #{tr}"
419
+ end
420
+ end
421
+ end
293
422
 
294
- end
423
+ path_flags.tally.each do |k, v|
424
+ if v == 1
425
+ raise RSTError, "Error: input text contains a path having only one end:\n > #{k}"
426
+ end
427
+ end
295
428
 
296
- # Draw a triangle between child/parent elements
297
- def triangle_to_parent(fromX, fromY, fromW, textW, symmetrize = true)
298
- if (fromY == 0)
299
- return
300
- end
429
+ paths = []
430
+ path_pool_source.each do |k, v|
431
+ path_flags.delete(k)
432
+ if targets = path_pool_target[k]
433
+ targets.each do |t|
434
+ paths << {x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :single}
435
+ end
436
+ elsif others = path_pool_other[k]
437
+ others.each do |t|
438
+ paths << {x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :none}
439
+ end
440
+ end
441
+ end
301
442
 
302
- toX = fromX
303
- fromCenter = (fromX + fromW / 2 + @m[:b_side])
443
+ path_flags.uniq.each do |k|
444
+ targets = path_pool_target[k]
445
+ fst = targets.shift
446
+ targets.each do |t|
447
+ paths << {x1: fst[0], y1: fst[1], x2: t[0], y2: t[1], arrow: :double}
448
+ end
449
+ end
304
450
 
305
- fromTop = row2px(fromY)
306
- fromLeft1 = (fromCenter + textW / 2)
307
- fromLeft2 = (fromCenter - textW / 2)
308
- toBot = (row2px(fromY - 1) + @e_height)
451
+ paths.each do |t|
452
+ draw_a_path(t[:x1], t[:y1] + $height_connector_to_text / 2,
453
+ t[:x2], t[:y2] + $height_connector_to_text / 2,
454
+ t[:arrow])
455
+ end
309
456
 
310
- if symmetrize
311
- toLeft = (toX + textW / 2 + @m[:b_side])
312
- else
313
- toLeft = (toX + textW / 2 + @m[:b_side] * 3)
457
+ paths.size
314
458
  end
315
459
 
316
- polygon_data = @polygon_styles.sub(/X1/, fromLeft1.to_s)
317
- polygon_data = polygon_data.sub(/Y1/, fromTop.to_s)
318
- polygon_data = polygon_data.sub(/X2/, fromLeft2.to_s)
319
- polygon_data = polygon_data.sub(/Y2/, fromTop.to_s)
320
- polygon_data = polygon_data.sub(/X3/, toLeft.to_s)
321
- @tree_data += polygon_data.sub(/Y3/, toBot.to_s)
322
- end
323
-
324
- # If a node element text is wider than the sum of it's
325
- # child elements, then the child elements need to
326
- # be resized to even out the space. This function
327
- # recurses down the a child tree and sizes the
328
- # children appropriately.
329
- def fix_child_size(id, current, target)
330
- children = @e_list.get_children(id)
331
- @e_list.set_element_width(id, target)
332
-
333
- if(children.length > 0 )
334
- delta = target - current
335
- target_delta = delta / children.length
336
-
337
- children.each do |child|
338
- child_width = @e_list.get_element_width(child)
339
- fix_child_size(child, child_width, child_width + target_delta)
460
+ def generate_line(x1, y1, x2, y2, col, dashed = false, arrow = false, stroke_width = 1)
461
+ if arrow
462
+ string = "marker-end='url(#arrow)' "
463
+ else
464
+ string = ""
340
465
  end
466
+ dasharray = dashed ? "stroke-dasharray='8 8'" : ""
467
+ swidth = FONT_SCALING * stroke_width
468
+
469
+ "<line x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}' #{dasharray} #{string}/>"
341
470
  end
342
- end
343
471
 
344
- def img_get_txt_width(text, font, font_size, multiline = true)
345
- parts = text.split("_", 2)
346
- main_before = parts[0].strip
347
- sub = parts[1]
348
- main = get_txt_only(main_before)
349
- main_metrics = img_get_txt_metrics(main, font, font_size, multiline)
350
- width = main_metrics.width
351
- if sub
352
- sub_metrics = img_get_txt_metrics(sub.strip, font, font_size * SUBSCRIPT_CONST, multiline)
353
- width += sub_metrics.width
472
+ # Draw a line between child/parent elements
473
+ def line_to_parent(parent, child)
474
+ if (child.horizontal_indent == 0 )
475
+ return
476
+ end
477
+
478
+ x1 = child.horizontal_indent + child.content_width / 2
479
+ y1 = child.vertical_indent + $height_connector_to_text / 2
480
+ x2 = parent.horizontal_indent + parent.content_width / 2
481
+ y2 = parent.vertical_indent + parent.content_height + $height_connector_to_text
482
+
483
+ line_data = @line_styles.sub(/X1/, x1.to_s)
484
+ line_data = line_data.sub(/Y1/, y1.to_s)
485
+ line_data = line_data.sub(/X2/, x2.to_s)
486
+ @tree_data += line_data.sub(/Y2/, y2.to_s)
354
487
  end
355
- return width
356
- end
357
488
 
358
- def img_get_txt_height(text, font, font_size)
359
- main_metrics = img_get_txt_metrics(text, font, font_size, false)
360
- main_metrics.height
361
- end
489
+ # Draw a triangle between child/parent elements
490
+ def triangle_to_parent(parent, child)
491
+ if (child.horizontal_indent == 0)
492
+ return
493
+ end
362
494
 
495
+ x1 = child.horizontal_indent
496
+ y1 = child.vertical_indent + $height_connector_to_text / 2
497
+ x2 = child.horizontal_indent + child.content_width
498
+ y2 = child.vertical_indent + $height_connector_to_text / 2
499
+ x3 = parent.horizontal_indent + parent.content_width / 2
500
+ y3 = parent.vertical_indent + parent.content_height + $height_connector_to_text
501
+
502
+ polygon_data = @polygon_styles.sub(/X1/, x1.to_s)
503
+ polygon_data = polygon_data.sub(/Y1/, y1.to_s)
504
+ polygon_data = polygon_data.sub(/X2/, x2.to_s)
505
+ polygon_data = polygon_data.sub(/Y2/, y2.to_s)
506
+ polygon_data = polygon_data.sub(/X3/, x3.to_s)
507
+ @tree_data += polygon_data.sub(/Y3/, y3.to_s)
508
+ end
509
+ end
363
510
  end