rsyntaxtree 0.8.8 → 1.0.1

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,314 +6,504 @@
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 = "sans-serif"
31
- when /(?:serif|math)/
32
- @fontstyle = "serif"
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)
33
36
  end
34
- @margin = margin.to_i
35
-
36
- super(e_list, metrics, symmetrize, color, leafstyle, multibyte, @font, @font_size)
37
37
 
38
- @line_styles = "<line style='stroke:black; stroke-width:#{FONT_SCALING};' x1='X1' y1='Y1' x2='X2' y2='Y2' />\n"
39
- @polygon_styles = "<polygon style='fill: none; stroke: black; stroke-width:#{FONT_SCALING};' points='X1 Y1 X2 Y2 X3 Y3' />\n"
40
- @text_styles = "<text style='fill: COLOR; font-size: FONT_SIZEpx; ST; WA;' x='X_VALUE' y='Y_VALUE' TD font-family='#{@fontstyle}'>CONTENT</text>\n"
41
- @tree_data = String.new
42
- end
38
+ def svg_data
39
+ metrics = parse_list
40
+ @height = metrics[:height] + @margin * 2
41
+ @width = metrics[:width] + @margin * 2
43
42
 
44
- def get_left_most(tree_data)
45
- xs = @tree_data.scan(/x1?=['"]([^'"]+)['"]/).map{|m| m.first.to_i}
46
- xs.min
47
- end
43
+ x1 = 0 - @margin
44
+ y1 = 0 - @margin
45
+ x2 = @width + @margin
46
+ y2 = @height + @margin
47
+ extra_lines = @extra_lines.join("\n")
48
48
 
49
- def svg_data
50
- parse_list
51
- lm = get_left_most(@tree_data)
52
- width = @width - lm + @margin * 2
53
- height = @height + @margin * 2
49
+ as2 = $h_gap_between_nodes / 2 * 0.8
50
+ as = as2 / 2
54
51
 
55
- header =<<EOD
52
+ header =<<EOD
56
53
  <?xml version="1.0" standalone="no"?>
57
- <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
58
- "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
59
- <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">
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>
60
70
  EOD
61
71
 
62
- rect =<<EOD
63
- <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" />"
64
74
  EOD
65
75
 
66
- footer = "</svg>"
76
+ footer = "</svg>"
67
77
 
68
- if @transparent
69
- header + @tree_data + footer
70
- else
71
- header + rect + @tree_data + footer
72
- end
73
- end
74
-
75
- # Create a temporary file and returns only its filename
76
- def create_tempf(basename, ext, num = 10)
77
- flags = File::RDWR | File::CREAT | File::EXCL
78
- tfname = ""
79
- num.times do |i|
80
- begin
81
- tfname = "#{basename}.#{$$}.#{i}.#{ext}"
82
- tfile = File.open(tfname, flags, 0600)
83
- rescue Errno::EEXIST
84
- next
78
+ if @transparent
79
+ header + @tree_data + extra_lines + footer
80
+ else
81
+ header + rect + @tree_data + extra_lines + footer
85
82
  end
86
- tfile.close
87
- return tfname
88
83
  end
89
- end
90
84
 
91
- :private
92
-
93
- # Add the element into the tree (draw it)
94
- def draw_element(x, y, w, string, type)
95
- string = string.sub(/\^\z/){""}
96
- # Calculate element dimensions and position
97
- if (type == ETYPE_LEAF) and @leafstyle == "nothing"
98
- top = row2px(y - 1) + (@font_size * 1.5)
99
- else
100
- top = row2px(y)
101
- end
102
- left = x + @m[:b_side]
103
- bottom = top + @e_height
104
- right = left + w
105
-
106
- # Split the string into the main part and the
107
- # subscript part of the element (if any)
108
- parts = string.split("_", 2)
109
- if(parts.length > 1 )
110
- main = parts[0].strip
111
- sub = parts[1].gsub(/_/, " ").strip
112
- else
113
- main = parts[0].strip
114
- sub = ""
115
- end
85
+ def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none)
116
86
 
117
- if /\A\=(.+)\=\z/ =~ main
118
- main = $1
119
- main_decoration= "overline"
120
- elsif /\A\-(.+)\-\z/ =~ main
121
- main = $1
122
- main_decoration= "underline"
123
- elsif /\A\~(.+)\~\z/ =~ main
124
- main = $1
125
- main_decoration= "line-through"
126
- else
127
- main_decoration= ""
128
- end
87
+ x_spacing = $h_gap_between_nodes * 1.25
88
+ y_spacing = $height_connector * 0.65
129
89
 
130
- if /\A\*\*\*(.+)\*\*\*\z/ =~ main
131
- main = $1
132
- main_style = "font-style: italic"
133
- main_weight = "font-weight: bold"
134
- elsif /\A\*\*(.+)\*\*\z/ =~ main
135
- main = $1
136
- main_style = ""
137
- main_weight = "font-weight: bold"
138
- elsif /\A\*(.+)\*\z/ =~ main
139
- main = $1
140
- main_style = "font-style: italic"
141
- main_weight = ""
142
- else
143
- main_style = ""
144
- main_weight = ""
145
- 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
146
96
 
147
- if /\A#(.+)#\z/ =~ main
148
- main = $1
149
- end
150
97
 
151
- # Calculate text size for the main and the
152
- # subscript part of the element
153
- # symbols for underline/overline removed temporarily
154
-
155
- main_width = img_get_txt_width(main, @font, @font_size)
156
-
157
- if sub != ""
158
- if /\A\=(.+)\=\z/ =~ sub
159
- sub = $1
160
- sub_decoration= "overline"
161
- elsif /\A\-(.+)\-\z/ =~ sub
162
- sub = $1
163
- sub_decoration= "underline"
164
- elsif /\A\~(.+)\~\z/ =~ sub
165
- sub = $1
166
- sub_decoration= "line-through"
98
+ if @visited_x[s_x]
99
+ new_s_x = s_x - x_spacing * @visited_x[s_x]
100
+ @visited_x[s_x] += 1
167
101
  else
168
- sub_decoration= ""
102
+ new_s_x = s_x
103
+ @visited_x[s_x] = 1
169
104
  end
170
105
 
171
- if /\A\*\*\*(.+)\*\*\*\z/ =~ sub
172
- sub = $1
173
- sub_style = "font-style: italic"
174
- sub_weight = "font-weight: bold"
175
- elsif /\A\*\*(.+)\*\*\z/ =~ sub
176
- sub = $1
177
- sub_style = ""
178
- sub_weight = "font-weight: bold"
179
- elsif /\A\*(.+)\*\z/ =~ sub
180
- sub = $1
181
- sub_style = "font-style: italic"
182
- sub_weight = ""
106
+ if @visited_x[t_x]
107
+ new_t_x = t_x - x_spacing * @visited_x[t_x]
108
+ @visited_x[t_x] += 1
183
109
  else
184
- sub_style = ""
185
- sub_weight = ""
110
+ new_t_x = t_x
111
+ @visited_x[t_x] = 1
186
112
  end
187
- sub_width = img_get_txt_width(sub.to_s, @font, @sub_size)
188
- else
189
- sub_width = 0
190
- end
191
113
 
192
- if /\A#(.+)#\z/ =~ sub
193
- sub = $1
194
- 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
195
117
 
196
- # Center text in the element
197
- txt_width = main_width + sub_width
198
- txt_pos = left + (right - left) / 2 - txt_width / 2
118
+ dashed = true if target_arrow == :none
199
119
 
200
- # Select apropriate color
201
- if(type == ETYPE_LEAF)
202
- col = @col_leaf
203
- else
204
- col = @col_node
205
- end
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)
128
+ else
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)
132
+ end
206
133
 
207
- if(main[0].chr == "<" && main[-1].chr == ">")
208
- col = @col_trace
134
+ @height = new_y if new_y > @height
209
135
  end
210
136
 
211
- # Draw main text
212
- main_data = @text_styles.sub(/COLOR/, col)
213
- main_data = main_data.sub(/FONT_SIZE/, @font_size.to_s)
214
- main_x = txt_pos
215
- main_y = top + @e_height - @m[:e_padd] * 1.5
216
- main_data = main_data.sub(/X_VALUE/, main_x.to_s)
217
- main_data = main_data.sub(/Y_VALUE/, main_y.to_s)
218
-
219
- @tree_data += main_data.sub(/TD/, "text-decoration='#{main_decoration}'")
220
- .sub(/ST/, main_style)
221
- .sub(/WA/, main_weight)
222
- .sub(/CONTENT/, main)
223
-
224
- # Draw subscript text
225
- sub_data = @text_styles.sub(/COLOR/, col)
226
- sub_data = sub_data.sub(/FONT_SIZE/, @sub_size.to_s)
227
- sub_x = main_x + main_width
228
- sub_y = top + (@e_height - @m[:e_padd] + @sub_size / 10)
229
- if (sub.length > 0 )
230
- sub_data = sub_data.sub(/X_VALUE/, sub_x.ceil.to_s)
231
- sub_data = sub_data.sub(/Y_VALUE/, sub_y.ceil.to_s)
232
- @tree_data += sub_data.sub(/TD/, "text-decoration='#{sub_decoration}'")
233
- .sub(/ST/, sub_style)
234
- .sub(/WA/, sub_weight)
235
- .sub(/CONTENT/, sub)
236
- end
237
- end
137
+ def draw_element(element)
138
+ top = element.vertical_indent
238
139
 
239
- # Draw a line between child/parent elements
240
- def line_to_parent(fromX, fromY, fromW, toX, toW)
140
+ left = element.horizontal_indent
141
+ bottom = top +$single_line_height
142
+ right = left + element.content_width
241
143
 
242
- if (fromY == 0 )
243
- return
244
- end
144
+ txt_pos = left + (right - left) / 2
245
145
 
246
- fromTop = row2px(fromY)
247
- fromLeft = (fromX + fromW / 2 + @m[:b_side])
248
- toBot = (row2px(fromY - 1 ) + @e_height)
249
- toLeft = (toX + toW / 2 + @m[:b_side])
146
+ if(element.type == ETYPE_LEAF)
147
+ col = @col_leaf
148
+ else
149
+ col = @col_node
150
+ end
250
151
 
251
- line_data = @line_styles.sub(/X1/, fromLeft.ceil.to_s)
252
- line_data = line_data.sub(/Y1/, fromTop.ceil.to_s)
253
- line_data = line_data.sub(/X2/, toLeft.ceil.to_s)
254
- @tree_data += line_data.sub(/Y2/, toBot.ceil.to_s)
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>"
182
+ else
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
348
+ end
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)
370
+ end
255
371
 
256
- end
372
+ element.content_height = bc[:height]
373
+ @tree_data += text_data.sub(/CONTENT/, new_text)
374
+ end
257
375
 
258
- # Draw a triangle between child/parent elements
259
- def triangle_to_parent(fromX, fromY, fromW, textW, symmetrize = true)
260
- if (fromY == 0)
261
- 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"
262
382
  end
263
383
 
264
- toX = fromX
265
- fromCenter = (fromX + fromW / 2 + @m[:b_side])
266
384
 
267
- fromTop = row2px(fromY).ceil
268
- fromLeft1 = (fromCenter + textW / 2).ceil
269
- fromLeft2 = (fromCenter - textW / 2).ceil
270
- toBot = (row2px(fromY - 1) + @e_height)
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
+
393
+ elist.each do |element|
394
+ x1 = element.horizontal_indent + element.content_width / 2
395
+ y1 = element.vertical_indent + element.content_height
396
+ y1 += $height_connector_to_text if element.enclosure != :none
397
+ et = element.path
398
+ et.each do |tr|
399
+ if /\A>(\d+)\z/ =~ tr
400
+ tr = $1
401
+ if path_pool_target[tr]
402
+ path_pool_target[tr] << [x1, y1]
403
+ else
404
+ path_pool_target[tr] = [[x1, y1]]
405
+ end
406
+ elsif path_pool_source[tr]
407
+ if path_pool_other[tr]
408
+ path_pool_other[tr] << [x1, y1]
409
+ else
410
+ path_pool_other[tr] = [[x1, y1]]
411
+ end
412
+ else
413
+ path_pool_source[tr] = [x1, y1]
414
+ end
415
+ path_flags << tr
416
+ if path_flags.tally.any?{|k, v| v > 2}
417
+ raise RSTError, "Error: input text contains a path having more than two ends:\n > #{tr}"
418
+ end
419
+ end
420
+ end
421
+
422
+ path_flags.tally.each do |k, v|
423
+ if v == 1
424
+ raise RSTError, "Error: input text contains a path having only one end:\n > #{k}"
425
+ end
426
+ end
271
427
 
272
- if symmetrize
273
- toLeft = (toX + textW / 2 + @m[:b_side])
274
- else
275
- toLeft = (toX + textW / 2 + @m[:b_side] * 3)
428
+ paths = []
429
+ path_pool_source.each do |k, v|
430
+ path_flags.delete(k)
431
+ if targets = path_pool_target[k]
432
+ targets.each do |t|
433
+ paths << {x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :single}
434
+ end
435
+ elsif others = path_pool_other[k]
436
+ others.each do |t|
437
+ paths << {x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :none}
438
+ end
439
+ end
440
+ end
441
+
442
+ path_flags.uniq.each do |k|
443
+ targets = path_pool_target[k]
444
+ fst = targets.shift
445
+ targets.each do |t|
446
+ paths << {x1: fst[0], y1: fst[1], x2: t[0], y2: t[1], arrow: :double}
447
+ end
448
+ end
449
+
450
+ paths.each do |t|
451
+ draw_a_path(t[:x1], t[:y1] + $height_connector_to_text / 2,
452
+ t[:x2], t[:y2] + $height_connector_to_text / 2,
453
+ t[:arrow])
454
+ end
455
+
456
+ paths.size
276
457
  end
277
458
 
278
- polygon_data = @polygon_styles.sub(/X1/, fromLeft1.ceil.to_s)
279
- polygon_data = polygon_data.sub(/Y1/, fromTop.ceil.to_s)
280
- polygon_data = polygon_data.sub(/X2/, fromLeft2.ceil.to_s)
281
- polygon_data = polygon_data.sub(/Y2/, fromTop.ceil.to_s)
282
- polygon_data = polygon_data.sub(/X3/, toLeft.ceil.to_s)
283
- @tree_data += polygon_data.sub(/Y3/, toBot.ceil.to_s)
284
- end
459
+ def generate_line(x1, y1, x2, y2, col, dashed = false, arrow = false, stroke_width = 1)
460
+ if arrow
461
+ string = "marker-end='url(#arrow)' "
462
+ else
463
+ string = ""
464
+ end
465
+ dasharray = dashed ? "stroke-dasharray='8 8'" : ""
466
+ swidth = FONT_SCALING * stroke_width
285
467
 
286
- # If a node element text is wider than the sum of it's
287
- # child elements, then the child elements need to
288
- # be resized to even out the space. This function
289
- # recurses down the a child tree and sizes the
290
- # children appropriately.
291
- def fix_child_size(id, current, target)
292
- children = @e_list.get_children(id)
293
- @e_list.set_element_width(id, target)
294
-
295
- if(children.length > 0 )
296
- delta = target - current
297
- target_delta = delta / children.length
298
-
299
- children.each do |child|
300
- child_width = @e_list.get_element_width(child)
301
- fix_child_size(child, child_width, child_width + target_delta)
468
+ "<line x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}' #{dasharray} #{string}/>"
469
+ end
470
+
471
+ # Draw a line between child/parent elements
472
+ def line_to_parent(parent, child)
473
+ if (child.horizontal_indent == 0 )
474
+ return
302
475
  end
476
+
477
+ x1 = child.horizontal_indent + child.content_width / 2
478
+ y1 = child.vertical_indent + $height_connector_to_text / 2
479
+ x2 = parent.horizontal_indent + parent.content_width / 2
480
+ y2 = parent.vertical_indent + parent.content_height + $height_connector_to_text
481
+
482
+ line_data = @line_styles.sub(/X1/, x1.to_s)
483
+ line_data = line_data.sub(/Y1/, y1.to_s)
484
+ line_data = line_data.sub(/X2/, x2.to_s)
485
+ @tree_data += line_data.sub(/Y2/, y2.to_s)
303
486
  end
304
- end
305
487
 
306
- def img_get_txt_width(text, font, font_size, multiline = false)
307
- parts = text.split("_", 2)
308
- main_before = parts[0].strip
309
- sub = parts[1]
310
- main = get_txt_only(main_before)
311
- main_metrics = img_get_txt_metrics(main, font, font_size, multiline)
312
- width = main_metrics.width
313
- if sub
314
- sub_metrics = img_get_txt_metrics(sub, font, font_size * SUBSCRIPT_CONST, multiline)
315
- width += sub_metrics.width
488
+ # Draw a triangle between child/parent elements
489
+ def triangle_to_parent(parent, child)
490
+ if (child.horizontal_indent == 0)
491
+ return
492
+ end
493
+
494
+ x1 = child.horizontal_indent
495
+ y1 = child.vertical_indent + $height_connector_to_text / 2
496
+ x2 = child.horizontal_indent + child.content_width
497
+ y2 = child.vertical_indent + $height_connector_to_text / 2
498
+ x3 = parent.horizontal_indent + parent.content_width / 2
499
+ y3 = parent.vertical_indent + parent.content_height + $height_connector_to_text
500
+
501
+ polygon_data = @polygon_styles.sub(/X1/, x1.to_s)
502
+ polygon_data = polygon_data.sub(/Y1/, y1.to_s)
503
+ polygon_data = polygon_data.sub(/X2/, x2.to_s)
504
+ polygon_data = polygon_data.sub(/Y2/, y2.to_s)
505
+ polygon_data = polygon_data.sub(/X3/, x3.to_s)
506
+ @tree_data += polygon_data.sub(/Y3/, y3.to_s)
316
507
  end
317
- return width
318
508
  end
319
509
  end