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
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ #==========================
5
+ # graph.rb
6
+ #==========================
7
+ #
8
+ # Image utility functions to inspect text font metrics
9
+ # Copyright (c) 2007-2021 Yoichiro Hasebe <yohasebe@gmail.com>
10
+
11
+ require 'utils'
12
+
13
+ module RSyntaxTree
14
+ class BaseGraph
15
+
16
+ def initialize(element_list, params)
17
+
18
+ @element_list = element_list
19
+ @symmetrize = params[:symmetrize]
20
+
21
+ if params[:color]
22
+ @col_node = "blue"
23
+ @col_leaf = "green"
24
+ @col_path = "purple"
25
+ else
26
+ @col_node = "black"
27
+ @col_leaf = "black"
28
+ @col_path = "black"
29
+ end
30
+
31
+ @col_bg = "none"
32
+ @col_fg = "black"
33
+ @col_line = "black"
34
+
35
+ @leafstyle = params[:leafstyle]
36
+ @fontset = params[:fontset]
37
+ @fontsize = params[:fontsize]
38
+ end
39
+
40
+ def calculate_level
41
+ @element_list.get_elements.select{|e| e.type == 2}.each do |e|
42
+ e.level = @element_list.get_id(e.parent).level + 1
43
+ end
44
+ end
45
+
46
+ def calculate_width(id = 1)
47
+ target = @element_list.get_id(id)
48
+ if target.children.empty?
49
+ target.width = target.content_width + $h_gap_between_nodes * 4
50
+
51
+ parent = @element_list.get_id(target.parent)
52
+ while parent && parent.children.size == 1
53
+ w = parent.content_width
54
+ target.width = w + $h_gap_between_nodes * 4 if w > target.content_width
55
+ parent = @element_list.get_id(parent.parent)
56
+ end
57
+ return target.width
58
+ else
59
+ if target.width != 0
60
+ return target.width
61
+ else
62
+ accum_array = []
63
+ target.children.each do |c|
64
+ accum_array << calculate_width(c)
65
+ end
66
+ if @symmetrize
67
+ accum_width = accum_array.max * target.children.size
68
+ else
69
+ accum_width = accum_array.sum
70
+ end
71
+
72
+ target.width = [accum_width, target.content_width].max
73
+ end
74
+
75
+ return target.width
76
+ end
77
+ end
78
+
79
+ def calculate_height(id = 1)
80
+ target = @element_list.get_id(id)
81
+ if id == 1
82
+ target.vertical_indent = 0
83
+ else
84
+ parent = @element_list.get_id(target.parent)
85
+
86
+ if !target.triangle && @leafstyle == "nothing" && ETYPE_LEAF == target.type && parent.children.size == 1
87
+ vertical_indent = parent.vertical_indent + parent.content_height
88
+ else
89
+ vertical_indent = parent.vertical_indent + parent.content_height + $height_connector
90
+ end
91
+
92
+ target.vertical_indent = vertical_indent
93
+ end
94
+
95
+ if target.children.empty?
96
+ target.height = target.content_height
97
+ vertical_end = target.vertical_indent + target.content_height
98
+ else
99
+ accum_array = []
100
+ target.children.each do |c|
101
+ accum_array << calculate_height(c)
102
+ end
103
+ target.height = accum_array.max - target.vertical_indent
104
+ vertical_end = accum_array.max
105
+ end
106
+ return vertical_end
107
+ end
108
+
109
+ def make_balance(id = 1)
110
+ target = @element_list.get_id(id)
111
+ if target.children.empty?
112
+ parent = @element_list.get_id(target.parent)
113
+ accum_array = []
114
+ parent.children.each do |c|
115
+ accum_array << @element_list.get_id(c).width
116
+ end
117
+ max = accum_array.max
118
+ parent.children.each do |c|
119
+ @element_list.get_id(c).width = max
120
+ end
121
+ return max
122
+ else
123
+ accum_array = []
124
+ target.children.each do |c|
125
+ accum_array << make_balance(c)
126
+ end
127
+ accum_width = accum_array.max
128
+ max = [accum_width, target.content_width].max
129
+ target.children.each do |c|
130
+ @element_list.get_id(c).width = max
131
+ end
132
+ return target.width
133
+ end
134
+ end
135
+
136
+ def calculate_indent
137
+ node_groups = @element_list.get_elements.group_by {|e| e.parent}
138
+ node_groups.each do |k, v|
139
+ next if k == 0
140
+ parent = @element_list.get_id(k)
141
+ if @symmetrize
142
+ num_leaves = v.size
143
+ partition_width = parent.width / num_leaves
144
+ left_offset = parent.horizontal_indent + parent.content_width / 2.0 - parent.width / 2.0
145
+ v.each do |e|
146
+ indent = left_offset + (partition_width - e.content_width) / 2.0
147
+ e.horizontal_indent = indent
148
+ left_offset += partition_width
149
+ end
150
+ else
151
+ left_offset = parent.horizontal_indent + parent.content_width / 2.0 - parent.width / 2.0
152
+ v.each do |e|
153
+ indent = left_offset + (e.width - e.content_width) / 2.0
154
+ e.horizontal_indent = indent
155
+ left_offset += e.width
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ def draw_elements
162
+ @element_list.get_elements.each do |element|
163
+ draw_element(element)
164
+ end
165
+ end
166
+
167
+ def draw_connector(id = 1)
168
+ parent = @element_list.get_id(id)
169
+ children = parent.children.map{|c| @element_list.get_id(c)}
170
+
171
+ if children.empty?
172
+ ;
173
+ elsif children.size == 1
174
+ child = children[0]
175
+ if (@leafstyle == "auto")
176
+ if child.contains_phrase || child.triangle
177
+ triangle_to_parent(parent, child)
178
+ else
179
+ line_to_parent(parent, child)
180
+ end
181
+ elsif(@leafstyle == "bar")
182
+ if child.triangle
183
+ triangle_to_parent(parent, child)
184
+ else
185
+ line_to_parent(parent, child)
186
+ end
187
+ elsif(@leafstyle == "nothing")
188
+ if child.triangle
189
+ triangle_to_parent(parent, child)
190
+ elsif ETYPE_LEAF == child.type
191
+ child.vertical_indent = parent.vertical_indent + parent.content_height + $height_connector / 2
192
+ end
193
+ end
194
+ else
195
+ children.each do |child|
196
+ line_to_parent(parent, child)
197
+ end
198
+ end
199
+
200
+ parent.children.each do |c|
201
+ draw_connector(c)
202
+ end
203
+ end
204
+
205
+ def get_leftmost(id = 1)
206
+ target = @element_list.get_id(id)
207
+ target_indent = target.horizontal_indent
208
+ children_indent = target.children.map{|c| get_leftmost(c)}
209
+ (children_indent << target_indent).min
210
+ end
211
+
212
+ def get_rightmost(id = 1)
213
+ target = @element_list.get_id(id)
214
+ target_right_end = target.horizontal_indent + target.content_width
215
+ children_right_end = target.children.map{|c| get_rightmost(c)}
216
+ (children_right_end << target_right_end).max
217
+ end
218
+
219
+ def node_centering
220
+ node_groups = @element_list.get_elements.group_by {|e| e.parent}
221
+ node_groups.sort_by{|k, v| -k}.each do |k, v|
222
+ next if k == 0
223
+ parent = @element_list.get_id(k)
224
+ child_positions =v.map {|child| child.horizontal_indent + child.content_width / 2 }
225
+ parent.horizontal_indent = child_positions.min + (child_positions.max - child_positions.min - parent.content_width) / 2
226
+ end
227
+ end
228
+
229
+ def parse_list
230
+ calculate_level
231
+ calculate_width
232
+ make_balance if @symmetrize
233
+ calculate_indent
234
+ node_centering
235
+
236
+ top = @element_list.get_id(1)
237
+ diff = top.horizontal_indent
238
+ @element_list.get_elements.each do |e|
239
+ e.horizontal_indent -= diff
240
+ end
241
+
242
+ offset_l = (top.horizontal_indent - get_leftmost) + $h_gap_between_nodes
243
+ offset_r = top.width / 2 - offset_l - $h_gap_between_nodes
244
+
245
+ @element_list.get_elements.each do |e|
246
+ e.horizontal_indent += offset_l
247
+ end
248
+
249
+ calculate_height
250
+ draw_elements
251
+ draw_connector
252
+ num_paths = draw_paths
253
+
254
+ # width = get_rightmost - get_leftmost + $h_gap_between_nodes * 2
255
+ width = get_rightmost - get_leftmost + $h_gap_between_nodes
256
+ height = @element_list.get_id(1).height
257
+ height = @height if @height > height
258
+
259
+ return {height: height, width: width}
260
+ end
261
+ end
262
+ end
@@ -6,43 +6,174 @@
6
6
  #==========================
7
7
  #
8
8
  # Aa class that represents a basic tree element, either node or leaf.
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
- class Element
11
+ require "markup_parser"
12
+ require 'utils'
13
+
14
+ module RSyntaxTree
15
+ class Element
17
16
 
18
- attr_accessor :id, :parent, :type, :content, :level, :width, :indent, :triangle
19
- def initialize(id = 0, parent = 0, content = "", level = 0, type = ETYPE_LEAF)
17
+ attr_accessor :id,
18
+ :parent, :type, :level,
19
+ :width, :height,
20
+ :content, :content_width, :content_height,
21
+ :horizontal_indent, :vertical_indent,
22
+ :triangle, :enclosure, :children, :parent,
23
+ :font, :fontsize, :contains_phrase,
24
+ :path
25
+
26
+ def initialize(id, parent, content, level, fontset, fontsize)
27
+ @type = ETYPE_LEAF
20
28
  @id = id # Unique element id
21
29
  @parent = parent # Parent element id
30
+ @children = [] # Child element ids
22
31
  @type = type # Element type
23
32
  @level = level # Element level in the tree (0=top etc...)
24
- @width = 0 # Width of the element in pixels
25
- @indent = 0 # Drawing offset
33
+ @width = 0 # Width of the part of the tree including itself and it governs
34
+ @content_width = 0 # Width of the content
35
+ @horizontal_indent = 0 # Drawing offset
36
+ @vertical_indent = 0 # Drawing offset
26
37
  content = content.strip
27
- if /\A.+\^\z/m =~ content
28
- @content = content.gsub("^"){""} # The actual element content
29
- @triangle = true # draw triangle instead of stright bar when in auto mode
38
+
39
+ if /.+?\^?((?:\+\>?\d+)+)\^?\z/m =~ content
40
+ @path = $1.sub(/\A\+/, "").split("+")
30
41
  else
31
- @content = content.gsub("^"){""}.strip # The actual element content
32
- @triangle = false # draw triangle instead of stright bar when in auto mode
42
+ @path = []
33
43
  end
34
- # workaround to save "[A [B [C] [D] ] [E [F] [G [H] [J] ] ] ]"
44
+
45
+ @fontset = fontset
46
+ @fontsize = fontsize
47
+
48
+ parsed = Markup.parse(content)
49
+
50
+ if parsed[:status] == :success
51
+ results = parsed[:results]
52
+ else
53
+ error_text = "Error: input text contains an invalid string"
54
+ error_text += "\n > " + content
55
+ raise RSTError, error_text
56
+ end
57
+ @content = results[:contents]
58
+ @paths = results[:paths]
59
+ @enclosure = results[:enclosure]
60
+ @triangle = results[:triangle]
61
+
62
+ @contains_phrase = false
63
+ setup
35
64
  end
36
65
 
37
- # Debug helper function
38
- def dump
39
- printf( "ID : %d\n", @id );
40
- printf( "Parent : %d\n", @parent );
41
- printf( "Level : %d\n", @level );
42
- printf( "Type : %d\n", @type );
43
- printf( "Width : %d\n", @width );
44
- printf( "Indent : %d\n", @indent );
45
- printf( "Content : %s\n\n", @content );
66
+ def setup
67
+ total_width = 0
68
+ total_height = 0
69
+ @content.each_with_index do |content, idx|
70
+ content_width = 0
71
+ case content[:type]
72
+ when :border, :bborder
73
+ height = $single_line_height / 2
74
+ content[:height] = height
75
+ total_height += height
76
+ when :text
77
+ row_width = 0
78
+ elements_height = []
79
+ content[:elements].each do |e|
80
+ text = e[:text]
81
+ e[:text] = text.gsub(" ", WHITESPACE_BLOCK).gsub(">", '&#62;').gsub("<", '&#60;')
82
+
83
+
84
+ @contains_phrase = true if text.include?(" ")
85
+ decoration = e[:decoration]
86
+ fontsize = decoration.include?(:small) || decoration.include?(:small) ? @fontsize * SUBSCRIPT_CONST : @fontsize
87
+ fontsize = decoration.include?(:subscript) || decoration.include?(:superscript) ? fontsize * SUBSCRIPT_CONST : fontsize
88
+ style = decoration.include?(:italic) || decoration.include?(:bolditalic) ? :italic : :normal
89
+ weight = decoration.include?(:bold) || decoration.include?(:bolditalic) ? :bold : :normal
90
+
91
+ # e[:cjk] = false
92
+ # if e[:decoration].include?(:math)
93
+ # font = @fontset[:math]
94
+ # elsif text.contains_cjk?
95
+ # font = @fontset[:cjk]
96
+ # e[:cjk] = true
97
+ # elsif decoration.include? :bolditalic
98
+ if decoration.include? :bolditalic
99
+ font = @fontset[:bolditalic]
100
+ elsif decoration.include? :bold
101
+ font = @fontset[:bold]
102
+ elsif decoration.include? :italic
103
+ font = @fontset[:italic]
104
+ else
105
+ font = @fontset[:normal]
106
+ end
107
+
108
+ standard_metrics = FontMetrics.get_metrics('X', @fontset[:normal], fontsize, :normal, :normal)
109
+ height = standard_metrics.height
110
+ if /\A[\<\>]+\z/ =~ text
111
+ width = standard_metrics.width * text.size / 2
112
+ elsif text.contains_emoji?
113
+ segments = text.split_by_emoji
114
+ width = 0
115
+ segments.each do |seg|
116
+ if /\s/ =~ seg[:char]
117
+ ch = 't'
118
+ else
119
+ ch = seg[:char]
120
+ end
121
+ if seg[:type] == :emoji
122
+ this_font = @fontset[:emoji]
123
+ metrics = FontMetrics.get_metrics(ch, this_font, fontsize, style, weight)
124
+ width += metrics.width
125
+ else
126
+ this_font = font
127
+ metrics = FontMetrics.get_metrics(ch, this_font, fontsize, style, weight)
128
+ width += metrics.width
129
+ end
130
+ end
131
+ else
132
+ text.gsub!("\\\\", 'i')
133
+ text.gsub!("\\", "")
134
+ text.gsub!(" ", "x")
135
+ text.gsub!("%", "X")
136
+ metrics = FontMetrics.get_metrics(text, font, fontsize, style, weight)
137
+ width = metrics.width
138
+ end
139
+
140
+ if e[:decoration].include?(:box) || e[:decoration].include?(:circle) || e[:decoration].include?(:bar)
141
+ if e[:text].size == 1
142
+ e[:content_width] = width
143
+ width += (height - width)
144
+ else
145
+ e[:content_width] = width
146
+ width += $width_half_X
147
+ end
148
+ end
149
+
150
+ if e[:decoration].include?(:whitespace)
151
+ width = $width_half_X / 2 * e[:text].size / 4
152
+ e[:text] = ""
153
+ end
154
+
155
+ e[:height] = height
156
+ elements_height << height + $box_vertical_margin / 2
157
+
158
+ e[:width] = width
159
+ row_width += width
160
+ end
161
+
162
+ if @enclosure != :none
163
+ total_height += (elements_height.max + $height_connector_to_text)
164
+ else
165
+ total_height += elements_height.max
166
+ end
167
+ content_width += row_width
168
+ end
169
+ total_width = content_width if total_width < content_width
170
+ end
171
+ @content_width = total_width
172
+ @content_height = total_height
46
173
  end
47
174
 
175
+ def add_child(child_id)
176
+ @children << child_id
177
+ end
178
+ end
48
179
  end
@@ -6,23 +6,25 @@
6
6
  #==========================
7
7
  #
8
8
  # Contains a list of unordered tree elements with a defined parent
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 'element'
17
12
 
18
- class ElementList
13
+ module RSyntaxTree
14
+ class ElementList
19
15
 
20
- attr_accessor :elements, :iterator
16
+ attr_accessor :elements, :iterator
21
17
  def initialize
22
18
  @elements = Array.new # The element array
23
19
  @iterator = -1 # Iterator index (used for get_first / get_next)
24
20
  end
25
21
 
22
+ def set_hierarchy
23
+ @elements.each do |e|
24
+ get_id(e.parent).add_child(e.id) unless e.parent == 0
25
+ end
26
+ end
27
+
26
28
  def add(element)
27
29
  @elements << element
28
30
  if(element.parent != 0)
@@ -41,12 +43,12 @@ class ElementList
41
43
  end
42
44
 
43
45
  def get_next
44
- @iterator += 1
45
- if @elements[@iterator]
46
- return @elements[@iterator]
47
- else
48
- return nil
49
- end
46
+ @iterator += 1
47
+ if @elements[@iterator]
48
+ return @elements[@iterator]
49
+ else
50
+ return nil
51
+ end
50
52
  end
51
53
 
52
54
  def get_id(id)
@@ -118,4 +120,5 @@ class ElementList
118
120
  end
119
121
  return maxlevel + 1;
120
122
  end
123
+ end
121
124
  end