SgfParser 1.0.0 → 2.0.0

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.
@@ -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