rsyntaxtree 0.9.3 → 1.0.6

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.tags +179 -0
  4. data/Gemfile +2 -0
  5. data/README.md +35 -29
  6. data/Rakefile +7 -0
  7. data/bin/rsyntaxtree +42 -31
  8. data/fonts/OpenMoji-Black.ttf +0 -0
  9. data/fonts/OpenMoji-Color.ttf +0 -0
  10. data/img/elements/circle.png +0 -0
  11. data/img/elements/circle_abc.png +0 -0
  12. data/img/elements/circle_bold.png +0 -0
  13. data/img/elements/circle_hatched.png +0 -0
  14. data/img/elements/circle_one.png +0 -0
  15. data/img/elements/connector.png +0 -0
  16. data/img/elements/connector_bold.png +0 -0
  17. data/img/elements/square.png +0 -0
  18. data/img/elements/square_abc.png +0 -0
  19. data/img/elements/square_bold.png +0 -0
  20. data/img/elements/square_hatched.png +0 -0
  21. data/img/elements/square_one.png +0 -0
  22. data/img/rsyntaxtree.png +0 -0
  23. data/img/sample.png +0 -0
  24. data/img/sample.svg +38 -0
  25. data/lib/rsyntaxtree/base_graph.rb +262 -0
  26. data/lib/rsyntaxtree/element.rb +156 -25
  27. data/lib/rsyntaxtree/elementlist.rb +16 -13
  28. data/lib/rsyntaxtree/markup_parser.rb +208 -0
  29. data/lib/rsyntaxtree/string_parser.rb +187 -204
  30. data/lib/rsyntaxtree/svg_graph.rb +471 -298
  31. data/lib/rsyntaxtree/utils.rb +49 -6
  32. data/lib/rsyntaxtree/version.rb +1 -1
  33. data/lib/rsyntaxtree.rb +144 -161
  34. data/rsyntaxtree.gemspec +2 -0
  35. data/test/markup_parser_test.rb +207 -0
  36. metadata +52 -11
  37. data/fonts/latinmodern-math.otf +0 -0
  38. data/fonts/lmroman10-bold.otf +0 -0
  39. data/fonts/lmroman10-bolditalic.otf +0 -0
  40. data/fonts/lmroman10-italic.otf +0 -0
  41. data/fonts/lmroman10-regular.otf +0 -0
  42. data/lib/rsyntaxtree/error_message.rb +0 -68
  43. data/lib/rsyntaxtree/graph.rb +0 -312
  44. data/lib/rsyntaxtree/tree_graph.rb +0 -327
@@ -6,358 +6,531 @@
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
+ @polyline = params[:polyline]
30
+ @line_styles = "<line style='stroke:black; stroke-width:#{FONT_SCALING};' x1='X1' y1='Y1' x2='X2' y2='Y2' />\n"
31
+ @polyline_styles = "<polyline style='stroke:black; stroke-width:#{FONT_SCALING}; fill:none;'
32
+ points='CHIX CHIY MIDX1 MIDY1 MIDX2 MIDY2 PARX PARY' />\n"
33
+ @polygon_styles = "<polygon style='fill: none; stroke: black; stroke-width:#{FONT_SCALING};' points='X1 Y1 X2 Y2 X3 Y3' />\n"
34
+ @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"
35
+ @tree_data = String.new
36
+ @visited_x = {}
37
+ @visited_y = {}
38
+ super(element_list, params)
38
39
  end
39
40
 
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
41
+ def svg_data
42
+ metrics = parse_list
43
+ @height = metrics[:height] + @margin * 2
44
+ @width = metrics[:width] + @margin * 2
49
45
 
50
- def get_left_most(tree_data)
51
- xs = @tree_data.scan(/x1?=['"]([^'"]+)['"]/).map{|m| m.first.to_i}
52
- xs.min
53
- end
46
+ x1 = 0 - @margin
47
+ y1 = 0 - @margin
48
+ x2 = @width + @margin
49
+ y2 = @height + @margin
50
+ extra_lines = @extra_lines.join("\n")
54
51
 
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
52
+ as2 = $h_gap_between_nodes / 2 * 0.8
53
+ as = as2 / 2
60
54
 
61
- header =<<EOD
55
+ header =<<EOD
62
56
  <?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>
57
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
58
+ <svg width="#{@width}" height="#{@height}" viewBox="#{x1}, #{y1}, #{x2}, #{y2}" version="1.1" xmlns="http://www.w3.org/2000/svg">
59
+ <defs>
60
+ <marker id="arrow" markerUnits="strokeWidth" markerWidth="#{as2}" markerHeight="#{as2}" viewBox="0 0 #{as2} #{as2}" refX="#{as}" refY="0">
61
+ <polyline fill="none" stroke="#{@col_path}" stroke-width="1" points="0,#{as2},#{as},0,#{as2},#{as2}" />
62
+ </marker>
63
+ <pattern id="hatchBlack" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
64
+ <line x1="0" y="0" x2="0" y2="10" stroke="black" stroke-width="4"></line>
65
+ </pattern>
66
+ <pattern id="hatchForNode" 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_node}" stroke-width="4"></line>
68
+ </pattern>
69
+ <pattern id="hatchForLeaf" x="10" y="10" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
70
+ <line x1="0" y="0" x2="0" y2="10" stroke="#{@col_leaf}" stroke-width="4"></line>
71
+ </pattern>
72
+ </defs>
71
73
  EOD
72
74
 
73
-
74
- rect =<<EOD
75
- <rect x="#{-@margin + lm}" y="-#{@margin}" width="#{@width - lm + @margin * 2}" height="#{@height + @margin * 2}" stroke="none" fill="white" />"
75
+ rect =<<EOD
76
+ <rect x="#{x1}" y="#{y1}" width="#{x2}" height="#{y2}" stroke="none" fill="white" />"
76
77
  EOD
77
78
 
78
- footer = "</svg>"
79
-
80
- if @transparent
81
- header + @tree_data + footer
82
- else
83
- header + rect + @tree_data + footer
84
- end
85
- end
79
+ footer = "</svg>"
86
80
 
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
81
+ if @transparent
82
+ header + @tree_data + extra_lines + footer
83
+ else
84
+ header + rect + @tree_data + extra_lines + footer
97
85
  end
98
- tfile.close
99
- return tfname
100
86
  end
101
- end
102
87
 
103
- :private
88
+ def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none)
104
89
 
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
90
+ x_spacing = $h_gap_between_nodes * 1.25
91
+ y_spacing = $height_connector * 0.65
128
92
 
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
93
+ ymax = [s_y, t_y].max
94
+ if ymax < @height
95
+ new_y = @height + y_spacing
96
+ else
97
+ new_y = ymax + y_spacing
98
+ end
141
99
 
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
100
 
159
- if /\A#(.+)#\z/ =~ main
160
- main = $1
161
- end
101
+ if @visited_x[s_x]
102
+ new_s_x = s_x - x_spacing * @visited_x[s_x]
103
+ @visited_x[s_x] += 1
104
+ else
105
+ new_s_x = s_x
106
+ @visited_x[s_x] = 1
107
+ end
162
108
 
163
- # Calculate text size for the main and the
164
- # subscript part of the element
165
- # symbols for underline/overline removed temporarily
109
+ if @visited_x[t_x]
110
+ new_t_x = t_x - x_spacing * @visited_x[t_x]
111
+ @visited_x[t_x] += 1
112
+ else
113
+ new_t_x = t_x
114
+ @visited_x[t_x] = 1
115
+ end
166
116
 
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
117
+ s_y += $h_gap_between_nodes / 2
118
+ t_y += $h_gap_between_nodes / 2
119
+ new_y += $h_gap_between_nodes / 2
174
120
 
121
+ dashed = true if target_arrow == :none
175
122
 
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"
123
+ if target_arrow == :single
124
+ @extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
125
+ @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
126
+ @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path ,dashed, true)
127
+ elsif target_arrow == :double
128
+ @extra_lines << generate_line(new_s_x, new_y, new_s_x, s_y, @col_path, dashed, true)
129
+ @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
130
+ @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path ,dashed, true)
186
131
  else
187
- sub_decoration= ""
132
+ @extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
133
+ @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
134
+ @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path ,dashed)
188
135
  end
189
136
 
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
137
+ @height = new_y if new_y > @height
211
138
  end
212
139
 
213
- if /\A#(.+)#\z/ =~ sub
214
- sub = $1
215
- end
140
+ def draw_element(element)
141
+ top = element.vertical_indent
216
142
 
217
- # Center text in the element
218
- txt_pos = left + (right - left) / 2
143
+ left = element.horizontal_indent
144
+ bottom = top +$single_line_height
145
+ right = left + element.content_width
219
146
 
220
- # Select apropriate color
221
- if(type == ETYPE_LEAF)
222
- col = @col_leaf
223
- else
224
- col = @col_node
225
- end
147
+ txt_pos = left + (right - left) / 2
226
148
 
227
- if(main[0].chr == "<" && main[-1].chr == ">")
228
- col = @col_trace
229
- end
149
+ if(element.type == ETYPE_LEAF)
150
+ col = @col_leaf
151
+ else
152
+ col = @col_node
153
+ end
230
154
 
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
155
+ text_data = @text_styles.sub(/COLOR/, col)
156
+ text_data = text_data.sub(/fontsize/, @fontsize.to_s + "px;")
157
+ text_x = txt_pos - element.content_width / 2
158
+ text_y = top + $single_line_height - $height_connector_to_text
159
+ text_data = text_data.sub(/X_VALUE/, text_x.to_s)
160
+ text_data = text_data.sub(/Y_VALUE/, text_y.to_s)
161
+ new_text = ""
162
+ this_x = 0
163
+ this_y = 0
164
+ bc = {:x => text_x - $h_gap_between_nodes / 2 , :y => top, :width => element.content_width + $h_gap_between_nodes, :height => nil}
165
+ element.content.each_with_index do |l, idx|
166
+ case l[:type]
167
+ when :border, :bborder
168
+ x1 = text_x
169
+ if idx == 0
170
+ text_y -= l[:height]
171
+ elsif
172
+ text_y += l[:height]
173
+ end
174
+ y1 = text_y - $single_line_height / 8
175
+ x2 = text_x + element.content_width
176
+ y2 = y1
177
+ this_width = x2 - x1
178
+ case l[:type]
179
+ when :border
180
+ stroke_width = FONT_SCALING
181
+ when :bborder
182
+ stroke_width = FONT_SCALING * 2
183
+ end
184
+ @extra_lines << "<line style=\"stroke:#{col}; stroke-width:#{stroke_width}; \" x1=\"#{x1}\" y1=\"#{y1}\" x2=\"#{x2}\" y2=\"#{y2}\"></line>"
245
185
  else
246
- dy = 1
247
- main_y += img_get_txt_height(l, @font, @font_size)
186
+ if element.enclosure == :brackets
187
+ this_x = txt_pos - element.content_width / 2
188
+ else
189
+ ewidth = 0
190
+ l[:elements].each do |e|
191
+ ewidth += e[:width]
192
+ end
193
+ this_x = txt_pos - (ewidth / 2)
194
+ end
195
+ text_y += l[:elements].map{|e| e[:height]}.max if idx != 0
196
+
197
+ l[:elements].each_with_index do |e, idx|
198
+ escaped_text = e[:text].gsub('>', '&gt;').gsub('<', '&lt;');
199
+ decorations = []
200
+ if e[:decoration].include?(:overline)
201
+ decorations << "overline"
202
+ end
203
+
204
+ if e[:decoration].include?(:underline)
205
+ decorations << "underline"
206
+ end
207
+
208
+ if e[:decoration].include?(:linethrough)
209
+ decorations << "line-through"
210
+ end
211
+ decoration ="text-decoration=\"" + decorations.join(" ") + "\""
212
+
213
+ style = "style=\""
214
+ if e[:decoration].include?(:small)
215
+ style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
216
+ this_y = text_y - (($single_X_metrics.height - $single_X_metrics.height * SUBSCRIPT_CONST) / 4) + 2
217
+ elsif e[:decoration].include?(:superscript)
218
+ style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
219
+ this_y = text_y - ($single_X_metrics.height / 4) + 1
220
+ elsif e[:decoration].include?(:subscript)
221
+ style += "font-size: #{(SUBSCRIPT_CONST.to_f * 100).to_i}%; "
222
+ this_y = text_y + 4
223
+ else
224
+ this_y = text_y
225
+ end
226
+
227
+ if e[:decoration].include?(:bold) || e[:decoration].include?(:bolditalic)
228
+ style += "font-weight: bold; "
229
+ end
230
+
231
+ if e[:decoration].include?(:italic) || e[:decoration].include?(:bolditalic)
232
+ style += "font-style: italic; "
233
+ end
234
+
235
+ style += "\""
236
+
237
+ case @fontstyle
238
+ when /(?:cjk)/
239
+ fontstyle = "'WenQuanYi Zen Hei', 'Noto Sans', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
240
+ when /(?:sans)/
241
+ if e[:cjk]
242
+ fontstyle = "'Noto Sans JP', 'Noto Sans', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
243
+ else
244
+ fontstyle = "'Noto Sans', 'Noto Sans JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', sans-serif"
245
+ end
246
+ when /(?:serif)/
247
+ if e[:cjk]
248
+ fontstyle = "'Noto Serif JP', 'Noto Serif', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', serif"
249
+ else
250
+ fontstyle = "'Noto Serif', 'Noto Serif JP', OpenMoji, 'OpenMoji Color', 'OpenMoji Black', serif"
251
+ end
252
+ end
253
+
254
+ if e[:decoration].include?(:box) || e[:decoration].include?(:circle) || e[:decoration].include?(:bar)
255
+ enc_height = e[:height]
256
+ enc_y = this_y - e[:height] * 0.8 + FONT_SCALING
257
+
258
+ if e[:text].size == 1
259
+ enc_width = e[:width]
260
+ enc_x = this_x
261
+ else
262
+ enc_width = e[:width]
263
+ enc_x = this_x
264
+ end
265
+
266
+ if e[:decoration].include?(:hatched)
267
+ case element.type
268
+ when ETYPE_LEAF
269
+ if @color
270
+ fill = "url(#hatchForLeaf)"
271
+ else
272
+ fill = "url(#hatchBlack)"
273
+ end
274
+ when ETYPE_NODE
275
+ if @color
276
+ fill = "url(#hatchForNode)"
277
+ else
278
+ fill = "url(#hatchBlack)"
279
+ end
280
+ end
281
+ else
282
+ fill = "none"
283
+ end
284
+
285
+ enc = nil
286
+ bar = nil
287
+
288
+ if e[:decoration].include?(:bstroke)
289
+ stroke_width = FONT_SCALING * 2.5
290
+ else
291
+ stroke_width = FONT_SCALING
292
+ end
293
+
294
+ if e[:decoration].include?(:box)
295
+ enc = "<rect style='stroke: #{col}; stroke-width:#{stroke_width};'
296
+ x='#{enc_x}' y='#{enc_y}'
297
+ width='#{enc_width}' height='#{enc_height}'
298
+ fill='#{fill}' />\n"
299
+ elsif e[:decoration].include?(:circle)
300
+ enc = "<rect style='stroke: #{col}; stroke-width:#{stroke_width};'
301
+ x='#{enc_x}' y='#{enc_y}' rx='#{enc_height / 2}' ry='#{enc_height / 2}'
302
+ width='#{enc_width}' height='#{enc_height}'
303
+ fill='#{fill}' />\n"
304
+ elsif e[:decoration].include?(:bar)
305
+ x1 = enc_x
306
+ y1 = enc_y + enc_height / 2
307
+ x2 = enc_x + enc_width
308
+ y2 = y1
309
+ ar_hwidth = e[:width] / 4.0
310
+ bar = "<line style='stroke:#{col}; stroke-width:#{stroke_width};' x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}'></line>\n"
311
+ @extra_lines << bar
312
+
313
+ if e[:decoration].include?(:arrow_to_l)
314
+ 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"
315
+ @extra_lines << l_arrowhead
316
+ end
317
+
318
+ if e[:decoration].include?(:arrow_to_r)
319
+ 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"
320
+ @extra_lines << r_arrowhead
321
+ end
322
+
323
+
324
+ end
325
+
326
+ @extra_lines << enc if enc
327
+
328
+ if e[:text].size == 1
329
+ this_x += (e[:height] - e[:content_width]) / 2
330
+ else
331
+ this_x += $width_half_X / 2
332
+ end
333
+ new_text << set_tspan(this_x, this_y, style, decoration, fontstyle, escaped_text)
334
+ if e[:text].size == 1
335
+ this_x += e[:content_width]
336
+ this_x += (e[:height] - e[:content_width]) / 2
337
+ else
338
+ this_x += e[:content_width]
339
+ this_x += $width_half_X / 2
340
+ end
341
+
342
+ elsif e[:decoration].include?(:whitespace)
343
+ this_x += e[:width]
344
+ next
345
+ else
346
+ new_text << set_tspan(this_x, this_y, style, decoration, fontstyle, escaped_text)
347
+ this_x += e[:width]
348
+ end
349
+
350
+ end
248
351
  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
352
+ @height = text_y if text_y > @height
353
+ end
354
+ bc[:y] = bc[:y] + $height_connector_to_text * 3 / 4
355
+ bc[:height] = text_y - bc[:y] + $height_connector_to_text
356
+ if element.enclosure == :brackets
357
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + $h_gap_between_nodes / 2, bc[:y], col)
358
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col)
359
+ @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + $h_gap_between_nodes / 2, bc[:y] + bc[:height], col)
360
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width] - $h_gap_between_nodes / 2, bc[:y], col)
361
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col)
362
+ @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)
363
+ elsif element.enclosure == :rectangle
364
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + bc[:width], bc[:y], col)
365
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col)
366
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col)
367
+ @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + bc[:width], bc[:y] + bc[:height], col)
368
+ elsif element.enclosure == :brectangle
369
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x] + bc[:width], bc[:y], col, false, false, 2)
370
+ @extra_lines << generate_line(bc[:x], bc[:y], bc[:x], bc[:y] + bc[:height], col, false, false, 2)
371
+ @extra_lines << generate_line(bc[:x] + bc[:width], bc[:y], bc[:x] + bc[:width], bc[:y] + bc[:height], col, false, false, 2)
372
+ @extra_lines << generate_line(bc[:x], bc[:y] + bc[:height], bc[:x] + bc[:width], bc[:y] + bc[:height], col, false, false, 2)
253
373
  end
254
- main = new_main
374
+
375
+ element.content_height = bc[:height]
376
+ @tree_data += text_data.sub(/CONTENT/, new_text)
255
377
  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 = txt_pos + (main_width / 2) - (sub_width / 2)
266
- sub_y = main_y + sub_height / 6
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
378
+
379
+ def set_tspan(this_x, this_y, style, decoration, fontstyle, text)
380
+ text.gsub!(/■+/) do |x|
381
+ num_spaces = x.size
382
+ "<tspan style='fill:none;'>" + "■" * num_spaces + "</tspan>"
383
+ end
384
+ "<tspan x='#{this_x}' y='#{this_y}' #{style} #{decoration} font-family=\"#{fontstyle}\">#{text}</tspan>\n"
274
385
  end
275
- end
276
386
 
277
- # Draw a line between child/parent elements
278
- def line_to_parent(fromX, fromY, fromW, toX, toW)
279
387
 
280
- if (fromY == 0 )
281
- return
282
- end
388
+ def draw_paths
389
+ rockbottom = 0
390
+ path_pool_target = {}
391
+ path_pool_other = {}
392
+ path_pool_source = {}
393
+ path_flags = []
394
+ # elist = @element_list.get_elements.reverse
395
+ elist = @element_list.get_elements
396
+
397
+ elist.each do |element|
398
+ x1 = element.horizontal_indent + element.content_width / 2
399
+ y1 = element.vertical_indent + element.content_height
400
+ y1 += $height_connector_to_text if element.enclosure != :none
401
+ et = element.path
402
+ et.each do |tr|
403
+ if /\A>(\d+)\z/ =~ tr
404
+ tr = $1
405
+ if path_pool_target[tr]
406
+ path_pool_target[tr] << [x1, y1]
407
+ else
408
+ path_pool_target[tr] = [[x1, y1]]
409
+ end
410
+ elsif path_pool_source[tr]
411
+ if path_pool_other[tr]
412
+ path_pool_other[tr] << [x1, y1]
413
+ else
414
+ path_pool_other[tr] = [[x1, y1]]
415
+ end
416
+ else
417
+ path_pool_source[tr] = [x1, y1]
418
+ end
419
+ path_flags << tr
420
+ if path_flags.tally.any?{|k, v| v > 2}
421
+ raise RSTError, "Error: input text contains a path having more than two ends:\n > #{tr}"
422
+ end
423
+ end
424
+ end
283
425
 
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])
426
+ path_flags.tally.each do |k, v|
427
+ if v == 1
428
+ raise RSTError, "Error: input text contains a path having only one end:\n > #{k}"
429
+ end
430
+ end
288
431
 
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)
432
+ paths = []
433
+ path_pool_source.each do |k, v|
434
+ path_flags.delete(k)
435
+ if targets = path_pool_target[k]
436
+ targets.each do |t|
437
+ paths << {x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :single}
438
+ end
439
+ elsif others = path_pool_other[k]
440
+ others.each do |t|
441
+ paths << {x1: v[0], y1: v[1], x2: t[0], y2: t[1], arrow: :none}
442
+ end
443
+ end
444
+ end
293
445
 
294
- end
446
+ path_flags.uniq.each do |k|
447
+ targets = path_pool_target[k]
448
+ fst = targets.shift
449
+ targets.each do |t|
450
+ paths << {x1: fst[0], y1: fst[1], x2: t[0], y2: t[1], arrow: :double}
451
+ end
452
+ end
295
453
 
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
454
+ paths.each do |t|
455
+ draw_a_path(t[:x1], t[:y1] + $height_connector_to_text / 2,
456
+ t[:x2], t[:y2] + $height_connector_to_text / 2,
457
+ t[:arrow])
458
+ end
301
459
 
302
- toX = fromX
303
- fromCenter = (fromX + fromW / 2 + @m[:b_side])
460
+ paths.size
461
+ end
304
462
 
305
- fromTop = row2px(fromY)
306
- fromLeft1 = (fromCenter + textW / 2)
307
- fromLeft2 = (fromCenter - textW / 2)
308
- toBot = (row2px(fromY - 1) + @e_height)
463
+ def generate_line(x1, y1, x2, y2, col, dashed = false, arrow = false, stroke_width = 1)
464
+ if arrow
465
+ string = "marker-end='url(#arrow)' "
466
+ else
467
+ string = ""
468
+ end
469
+ dasharray = dashed ? "stroke-dasharray='8 8'" : ""
470
+ swidth = FONT_SCALING * stroke_width
309
471
 
310
- if symmetrize
311
- toLeft = (toX + textW / 2 + @m[:b_side])
312
- else
313
- toLeft = (toX + textW / 2 + @m[:b_side] * 3)
472
+ "<line x1='#{x1}' y1='#{y1}' x2='#{x2}' y2='#{y2}' style='fill: none; stroke: #{col}; stroke-width:#{swidth}' #{dasharray} #{string}/>"
314
473
  end
315
474
 
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
475
+ # Draw a line between child/parent elements
476
+ def line_to_parent(parent, child)
477
+ if (child.horizontal_indent == 0 )
478
+ return
479
+ end
480
+
481
+ if @polyline
482
+ chi_x = child.horizontal_indent + child.content_width / 2
483
+ chi_y = child.vertical_indent + $height_connector_to_text / 2
484
+
485
+ par_x = parent.horizontal_indent + parent.content_width / 2
486
+ par_y = parent.vertical_indent + parent.content_height + $height_connector_to_text
487
+
488
+ mid_x1 = chi_x
489
+ mid_y1 = par_y + (chi_y - par_y) / 2
323
490
 
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)
491
+ mid_x2 = par_x
492
+ mid_y2 = mid_y1
493
+
494
+ @tree_data += @polyline_styles.sub(/CHIX/, chi_x.to_s)
495
+ .sub(/CHIY/, chi_y.to_s)
496
+ .sub(/MIDX1/, mid_x1.to_s)
497
+ .sub(/MIDY1/, mid_y1.to_s)
498
+ .sub(/MIDX2/, mid_x2.to_s)
499
+ .sub(/MIDY2/, mid_y2.to_s)
500
+ .sub(/PARX/, par_x.to_s)
501
+ .sub(/PARY/, par_y.to_s)
502
+ else
503
+ x1 = child.horizontal_indent + child.content_width / 2
504
+ y1 = child.vertical_indent + $height_connector_to_text / 2
505
+ x2 = parent.horizontal_indent + parent.content_width / 2
506
+ y2 = parent.vertical_indent + parent.content_height + $height_connector_to_text
507
+
508
+ line_data = @line_styles.sub(/X1/, x1.to_s)
509
+ line_data = line_data.sub(/Y1/, y1.to_s)
510
+ line_data = line_data.sub(/X2/, x2.to_s)
511
+ @tree_data += line_data.sub(/Y2/, y2.to_s)
340
512
  end
341
513
  end
342
- end
343
514
 
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
354
- end
355
- return width
356
- end
515
+ # Draw a triangle between child/parent elements
516
+ def triangle_to_parent(parent, child)
517
+ if (child.horizontal_indent == 0)
518
+ return
519
+ end
357
520
 
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
521
+ x1 = child.horizontal_indent
522
+ y1 = child.vertical_indent + $height_connector_to_text / 2
523
+ x2 = child.horizontal_indent + child.content_width
524
+ y2 = child.vertical_indent + $height_connector_to_text / 2
525
+ x3 = parent.horizontal_indent + parent.content_width / 2
526
+ y3 = parent.vertical_indent + parent.content_height + $height_connector_to_text
527
+
528
+ polygon_data = @polygon_styles.sub(/X1/, x1.to_s)
529
+ polygon_data = polygon_data.sub(/Y1/, y1.to_s)
530
+ polygon_data = polygon_data.sub(/X2/, x2.to_s)
531
+ polygon_data = polygon_data.sub(/Y2/, y2.to_s)
532
+ polygon_data = polygon_data.sub(/X3/, x3.to_s)
533
+ @tree_data += polygon_data.sub(/Y3/, y3.to_s)
534
+ end
361
535
  end
362
-
363
536
  end