SgfParser 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,51 +1,98 @@
1
1
  module SGF
2
2
 
3
+ #Your basic node. It holds information about itself, its parent, and its children.
3
4
  class Node
4
5
 
5
6
  attr_accessor :parent, :children, :properties
6
7
 
7
8
  # Creates a new node. Arguments which can be passed in are:
8
9
  # :parent => parent_node (nil by default)
9
- # :children => [list, of, children] (empty array by default)
10
- # :properties => {hash_of => properties} (empty hash by default)
10
+ # :children => [list, of, children] (empty array if nothing is passed)
11
+ # :properties => {hash_of => properties} (empty hash if nothing is passed)
11
12
  def initialize args={}
12
13
  @parent = args[:parent]
13
14
  @children = []
14
15
  add_children args[:children] if args[:children]
15
16
  @properties = Hash.new
16
- @properties.merge! args[:properties] if args[:properties]
17
+ add_properties args[:properties] if args[:properties]
17
18
  end
18
19
 
20
+ #Takes an arbitrary number of child nodes, adds them to the list of children, and make this node their parent.
19
21
  def add_children *nodes
20
22
  nodes.flatten!
21
23
  raise "Non-node child given!" if nodes.any? { |node| node.class != Node }
22
- nodes.each { |node| node.parent = self }
23
- @children.concat nodes
24
+ nodes.each do |node|
25
+ node.parent = self
26
+ @children << node
27
+ end
24
28
  end
25
29
 
30
+ #Takes a hash {identity => property} and adds those to the current node.
31
+ #If a property already exists, it will append to it.
26
32
  def add_properties hash
27
- hash.each do |key, value|
28
- @properties[key] ||= ""
29
- @properties[key].concat value
33
+ hash.each do |identity, property|
34
+ @properties[identity] ||= property.class.new
35
+ @properties[identity].concat property
30
36
  end
37
+ update_human_readable_methods
31
38
  end
32
39
 
40
+ #Iterate through each child. Yields a child node, if one exists.
33
41
  def each_child
34
42
  @children.each { |child| yield child }
35
43
  end
36
44
 
45
+ #Compare to another node.
37
46
  def == other_node
38
47
  @properties == other_node.properties
39
48
  end
40
49
 
41
- def comments
42
- @properties["C"]
50
+ #Syntactic sugar for node.properties["XX"]
51
+ def [] identity
52
+ identity = identity.to_s
53
+ @properties[identity]
54
+ end
55
+
56
+ def to_s
57
+ out = "#<#{self.class}:#{self.object_id}, "
58
+ out << (@parent ? "Has a parent, " : "Has no parent, ")
59
+ out << "#{@children.size} Children, "
60
+ out << "#{@properties.keys.size} Properties"
61
+ out << ">"
43
62
  end
44
63
 
45
- alias :comment :comments
64
+ alias :inspect :to_s
65
+
66
+ def to_str(indent = 0)
67
+ properties = []
68
+ @properties.each do |identity, property|
69
+ properties << stringify_identity_and_property(identity, property)
70
+ end
71
+ whitespace = leading_whitespace(indent)
72
+ "#{whitespace};#{properties.join("\n#{whitespace}")}"
73
+ end
74
+
75
+ def stringify_identity_and_property(identity, property)
76
+ new_property = property.instance_of?(Array) ? property.join("][") : property
77
+ new_property = new_property.gsub("]", "\\]") if identity == "C"
78
+ "#{identity.to_s}[#{new_property}]"
79
+ end
46
80
 
47
81
  private
48
82
 
83
+ def update_human_readable_methods
84
+ SGF::Node::PROPERTIES.each do |human_readable_method, sgf_identity|
85
+ next if defined? human_readable_method.to_sym
86
+ define_method(human_readable_method.to_sym) do
87
+ @properties[sgf_identity] ? @properties[sgf_identity] : raise(SGF::NoIdentityError)
88
+ end
89
+ end
90
+ end
91
+
92
+ def leading_whitespace(indent)
93
+ "#{" " * indent}"
94
+ end
95
+
49
96
  def method_missing method_name, *args
50
97
  property = method_name.to_s.upcase
51
98
  if property[/(.*?)=$/]
@@ -1,120 +1,167 @@
1
1
  require 'stringio'
2
2
 
3
3
  module SGF
4
+
5
+ #The parser returns a SGF::Tree representation of the SGF file
6
+ #parser = SGF::Parser.new
7
+ #tree = parser.parse sgf_in_string_form
4
8
  class Parser
5
9
 
6
- def initialize sgf
7
- @sgf = stringified(sgf)
8
- @tree = Tree.new sgf
10
+ NEW_NODE = ";"
11
+ BRANCHING = ["(", ")"]
12
+ PROPERTY = ["[", "]"]
13
+ NODE_DELIMITERS = [NEW_NODE].concat BRANCHING
14
+ LIST_IDENTITIES = ["AW", "AB", "AE", "AR", "CR", "DD",
15
+ "LB", "LN", "MA", "SL", "SQ", "TR", "VW",
16
+ "TB", "TW"]
17
+
18
+ # This takes as argument an SGF and returns an SGF::Tree object
19
+ # It accepts a local path (String), a stringified SGF (String),
20
+ # or a file handler (File).
21
+ # The second argument is optional, in case you don't want this to raise errors.
22
+ # You probably shouldn't use it, but who's gonna stop you?
23
+ def parse sgf, strict_parsing = true
24
+ @strict_parsing = strict_parsing
25
+ @stream = streamably_stringify sgf
26
+ @tree = Tree.new
9
27
  @root = @tree.root
28
+ @current_node = @root
29
+ @branches = []
30
+ until @stream.eof?
31
+ case next_character
32
+ when "(" then open_branch
33
+ when ";" then
34
+ create_new_node
35
+ parse_node_data
36
+ add_properties_to_current_node
37
+ when ")" then close_branch
38
+ else next
39
+ end
40
+ end
41
+ @tree
10
42
  end
11
43
 
12
- def stringified sgf
13
- File.exist?(sgf) ? File.read(sgf) : sgf
44
+ private
45
+
46
+ def streamably_stringify sgf
47
+ sgf = sgf.read if sgf.instance_of?(File)
48
+ sgf = File.read(sgf) if File.exist?(sgf)
49
+
50
+ check_for_errors_before_parsing sgf if @strict_parsing
51
+ StringIO.new clean(sgf), 'r'
14
52
  end
15
53
 
16
- def parse
17
- while char = next_character
18
- case char
19
- when '(' then store_branch
20
- when ')' then fetch_branch
21
- when ';' then store_node_and_create_new_node
22
- when '[' then get_and_store_property
23
- else store_character(char)
24
- end
54
+ def check_for_errors_before_parsing string
55
+ msg = "The first two non-whitespace characters of the string should be (;"
56
+ unless string[/\A\s*\(\s*;/]
57
+ msg << " but they were #{string[0..1]} instead."
58
+ raise(SGF::MalformedDataError, msg)
25
59
  end
26
- @tree
27
60
  end
28
61
 
29
- def next_character
30
- character_available? && @stream.sysread(1)
62
+ def clean sgf
63
+ sgf.gsub! "\\\\n\\\\r", ''
64
+ sgf.gsub! "\\\\r\\\\n", ''
65
+ sgf.gsub! "\\\\r", ''
66
+ sgf.gsub! "\\\\n", ''
67
+ sgf
31
68
  end
32
69
 
33
- def character_available?
34
- @stream ||= StringIO.new clean_string, 'r'
35
- !@stream.eof?
70
+ def open_branch
71
+ @branches.unshift @current_node
36
72
  end
37
73
 
38
- def clean_string
39
- @sgf.gsub! "\\\\n\\\\r", ""
40
- @sgf.gsub! "\\\\r\\\\n", ""
41
- @sgf.gsub! "\\\\r", ""
42
- @sgf.gsub! "\\\\n", ""
43
- @sgf
44
- end
74
+ def close_branch
75
+ @current_node = @branches.shift
76
+ end
45
77
 
46
- def store_branch
47
- @branches ||= []
48
- @branches.unshift @current_node
78
+ def create_new_node
79
+ node = Node.new
80
+ @current_node.add_children node
81
+ @current_node = node
49
82
  end
50
83
 
51
- def current_node
52
- @current_node ||= @root
53
- end
84
+ def parse_node_data
85
+ @node_properties = {}
86
+ while still_inside_node?
87
+ parse_identity
88
+ parse_property
89
+ @node_properties[@identity] = @property
90
+ end
91
+ end
54
92
 
55
- def fetch_branch
56
- @current_node = @branches.shift
57
- clear_temporary_data
93
+ def add_properties_to_current_node
94
+ @current_node.add_properties @node_properties
58
95
  end
59
96
 
60
- def store_node_and_create_new_node
61
- parent = current_node
62
- @current_node = Node.new :parent => parent
63
- parent.add_properties content
64
- parent.add_children @current_node
65
- clear_temporary_data
97
+ def still_inside_node?
98
+ inside_a_node = false
99
+ while char = next_character
100
+ next if char[/\s/]
101
+ inside_a_node = !NODE_DELIMITERS.include?(char)
102
+ break
103
+ end
104
+ @stream.pos -= 1 if char
105
+ inside_a_node
66
106
  end
67
107
 
68
- def get_and_store_property
69
- @content[@identity] ||= ""
70
- @content[@identity] << get_property
108
+ def parse_identity
71
109
  @identity = ""
110
+ while char = next_character and char != "["
111
+ @identity << char unless char == "\n"
112
+ end
72
113
  end
73
114
 
74
- def get_property
75
- buffer = ""
76
- while char = next_character
77
- case char
78
- when "]" then break unless multiple_properties?
79
- when "\\" then
80
- char << next_character
81
- char = "]" if char == "\\]"
82
- end
115
+ def parse_property
116
+ @property = ""
117
+ case @identity.upcase
118
+ when "C" then parse_comment
119
+ when *LIST_IDENTITIES then parse_multi_property
120
+ else parse_generic_property
121
+ end
122
+ end
83
123
 
84
- buffer << char
124
+ def parse_comment
125
+ while char = next_character and still_inside_comment? char
126
+ @property << char
85
127
  end
86
- "[#{buffer}]"
128
+ @property.gsub! "\\]", "]"
87
129
  end
88
130
 
89
- def multiple_properties?
90
- multiple_properties = false
91
- if char = next_character
92
- char = next_character if char == "\n"
93
- if char == "["
94
- multiple_properties = true
95
- end
96
- @stream.pos -= 1
97
- multiple_properties
131
+ def parse_multi_property
132
+ while char = next_character and still_inside_multi_property? char
133
+ @property << char
98
134
  end
135
+ @property = @property.gsub("][", ",").split(",")
99
136
  end
100
137
 
101
- def store_character(char)
102
- @identity << char unless char == "\n"
138
+ def parse_generic_property
139
+ while char = next_character and char != "]"
140
+ @property << char
141
+ end
103
142
  end
104
143
 
105
- def clear_temporary_data
106
- @content.clear
107
- @identity = ""
144
+ def still_inside_comment? char
145
+ char != "]" || (char == "]" && @property[-1..-1] == "\\")
108
146
  end
109
147
 
110
- def content
111
- @content ||= {}
148
+ def still_inside_multi_property? char
149
+ return true if char != "]"
150
+ inside_multi_property = false
151
+ while char = next_character
152
+ next if char[/\s/]
153
+ inside_multi_property = char == "["
154
+ break
155
+ end
156
+ @stream.pos -= 1 if char
157
+ inside_multi_property
112
158
  end
113
159
 
114
- def identity
115
- @identity ||= ""
160
+ def next_character
161
+ !@stream.eof? && @stream.sysread(1)
116
162
  end
117
163
 
118
164
  end
165
+
119
166
  end
120
167
 
@@ -1,97 +1,90 @@
1
- # A parser for SGF Files. Main usage: SGF::Tree.new :filename => file_name
2
- module SgfParser
1
+ module SGF
3
2
 
4
3
  # http://www.red-bean.com/sgf/proplist.html
5
4
 
6
- # Here we define SGF::Properties, so we can figure out what each property
7
- # is and does.
5
+ class Game
8
6
 
9
- property_string = %Q{AB Add Black setup list of stone
10
- AE Add Empty setup list of point
11
- AN Annotation game-info simpletext
12
- AP Application root composed simpletext ':' simpletext
13
- AR Arrow - list of composed point ':' point
14
- AS Who adds stones - (LOA) simpletext
15
- AW Add White setup list of stone
16
- B Black move move
17
- BL Black time left move real
18
- BM Bad move move double
19
- BR Black rank game-info simpletext
20
- BT Black team game-info simpletext
21
- C Comment - text
22
- CA Charset root simpletext
23
- CP Copyright game-info simpletext
24
- CR Circle - list of point
25
- DD Dim points - (inherit) elist of point
26
- DM Even position - double
27
- DO Doubtful move none
28
- DT Date game-info simpletext
29
- EV Event game-info simpletext
30
- FF Fileformat root number (range: 1-4)
31
- FG Figure - none | composed number ":" simpletext
32
- GB Good for Black - double
33
- GC Game comment game-info text
34
- GM Game root number (range: 1-5,7-16)
35
- GN Game name game-info simpletext
36
- GW Good for White - double
37
- HA Handicap game-info (Go) number
38
- HO Hotspot - double
39
- IP Initial pos. game-info (LOA) simpletext
40
- IT Interesting move none
41
- IY Invert Y-axis game-info (LOA) simpletext
42
- KM Komi game-info (Go) real
43
- KO Ko move none
44
- LB Label - list of composed point ':' simpletext
45
- LN Line - list of composed point ':' point
46
- MA Mark - list of point
47
- MN set move number move number
48
- N Nodename - simpletext
49
- OB OtStones Black move number
50
- ON Opening game-info simpletext
51
- OT Overtime game-info simpletext
52
- OW OtStones White move number
53
- PB Player Black game-info simpletext
54
- PC Place game-info simpletext
55
- PL Player to play setup color
56
- PM Print move mode - (inherit) number
57
- PW Player White game-info simpletext
58
- RE Result game-info simpletext
59
- RO Round game-info simpletext
60
- RU Rules game-info simpletext
61
- SE Markup - (LOA) point
62
- SL Selected - list of point
63
- SO Source game-info simpletext
64
- SQ Square - list of point
65
- ST Style root number (range: 0-3)
66
- SU Setup type game-info (LOA) simpletext
67
- SZ Size root (number | composed number ':' number)
68
- TB Territory Black - (Go) elist of point
69
- TE Tesuji move double
70
- TM Timelimit game-info real
71
- TR Triangle - list of point
72
- TW Territory White - (Go) elist of point
73
- UC Unclear pos - double
74
- US User game-info simpletext
75
- V Value - real
76
- VW View - (inherit) elist of point
77
- W White move move
78
- WL White time left move real
79
- WR White rank game-info simpletext
80
- WT White team game-info simpletext }
81
-
82
- property_array = property_string.split("\n")
83
- hash = {}
84
- property_array.each do |set|
85
- temp = set.gsub("\t", " ")
86
- id = temp[0..3].strip
87
- desc = temp[4..19].strip
88
- property_type = temp[20..35].strip
89
- property_value = temp[37..-1].strip
90
- hash[id] = [desc, property_type, property_value]
7
+ PROPERTIES = {"annotator"=>"AN",
8
+ "black_octisquares"=>"BO", #Octi
9
+ "black_rank"=>"BR",
10
+ "black_team"=>"BT",
11
+ "copyright"=>"CP",
12
+ "date"=>"DT",
13
+ "event"=>"EV",
14
+ "game_content"=>"GC",
15
+ "handicap"=>"HA", #Go
16
+ "initial_position"=>"IP", #Lines of Action
17
+ "invert_y_axis"=>"IY", #Lines of Action
18
+ "komi"=>"KM", #Go
19
+ "match_information"=>"MI", #Backgammon
20
+ "name"=>"GN",
21
+ "prongs"=>"NP", #Octi
22
+ "reserve"=>"NR", #Octi
23
+ "superprongs"=>"NS", #Octi
24
+ "opening"=>"ON",
25
+ "overtime"=>"OT",
26
+ "black_player"=>"PB",
27
+ "place"=>"PC",
28
+ "puzzle"=>"PZ",
29
+ "white_player"=>"PW",
30
+ "result"=>"RE",
31
+ "round"=>"RO",
32
+ "rules"=>"RU",
33
+ "setup_type"=>"SU", #Lines of Action
34
+ "source"=>"SO",
35
+ "time"=>"TM",
36
+ "data_entry"=>"US",
37
+ "white_octisquares"=>"WO", #Octi
38
+ "white_rank"=>"WR",
39
+ "white_team"=>"WT"}
91
40
  end
92
41
 
93
- # All this work for this minuscule line!
94
- PROPERTIES = hash
42
+ class Node
43
+ PROPERTIES = {
44
+ "black_move" => "B",
45
+ "black_time_left" => "BL",
46
+ "bad_move" => "BM",
47
+ "doubtful" => "DO",
48
+ "interesting" => "IT",
49
+ "ko" => "KO",
50
+ "set_move_number" => "MN",
51
+ "otstones_black" => "OB", # What?
52
+ "otstones_white" => "OW", # Again! What?
53
+ "tesuji" => "TE",
54
+ "white_move" => "W",
55
+ "white_time_left" => "WL",
56
+ "add_black" => "AB",
57
+ "add_empty" => "AE",
58
+ "add_white" => "AW",
59
+ "player" => "PL",
60
+ "arrow" => "AR",
61
+ "comment" => "C",
62
+ "circle" => "CR",
63
+ "dim_points" => "DD",
64
+ "even_position" => "DM", #Yep. No idea how that makes sense.
65
+ "figure" => "FG",
66
+ "good_for_black" => "GB",
67
+ "good_for_white" => "GW",
68
+ "hotspot" => "HO",
69
+ "label" => "LB",
70
+ "line" => "LN",
71
+ "mark" => "MA",
72
+ "node_name" => "N",
73
+ "print_move_node" => "PM", #Am I going to have to code this?
74
+ "selected" => "SL",
75
+ "square" => "SQ",
76
+ "triangle" => "TR",
77
+ "unclear_position" => "UC",
78
+ "value" => "V",
79
+ "view" => "VW",
80
+ "application" => "AP",
81
+ "charset" => "CA",
82
+ "file_format" => "FF",
83
+ "game" => "GM",
84
+ "style" => "ST",
85
+ "size" => "SZ"
86
+ }
87
+ end
95
88
 
96
89
  end
97
90