rsyntaxtree 0.9.3 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
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