SgfParser 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ class SGF::StrictErrorChecker
2
+ def check_for_errors_before_parsing string
3
+ unless string[/\A\s*\(\s*;/]
4
+ msg = "The first two non-whitespace characters of the string should be (;"
5
+ msg << " but they were #{string[0..1]} instead."
6
+ raise(SGF::MalformedDataError, msg)
7
+ end
8
+ end
9
+ end
10
+
11
+ class SGF::LaxErrorChecker
12
+ def check_for_errors_before_parsing string
13
+ # just look the other way
14
+ end
15
+ end
@@ -0,0 +1,64 @@
1
+ require_relative 'writer'
2
+
3
+ class SGF::Gametree
4
+ include Enumerable
5
+
6
+ SGF::Gametree::PROPERTIES.each do |human_readable_method, sgf_identity|
7
+ define_method(human_readable_method.to_sym) do
8
+ @root[sgf_identity] || raise(SGF::NoIdentityError, "This gametree does not have '#{human_readable_method}' available")
9
+ end
10
+ end
11
+
12
+ attr_reader :root
13
+
14
+ attr_accessor :current_node
15
+
16
+ # Takes a SGF::Node as an argument. It will be a problem if that node isn't
17
+ # really the first node of of a game (ie: no FF property)
18
+ def initialize node
19
+ raise ArgumentError, "Expected SGF::Node argument but received #{node.class}" unless node.instance_of? SGF::Node
20
+ @root = node
21
+ @current_node = node
22
+ end
23
+
24
+ # A simple way to go to the next node in the same branch of the tree
25
+ def next_node
26
+ @current_node = @current_node.children[0]
27
+ end
28
+
29
+ # Iterate through all the nodes in preorder fashion
30
+ def each &block
31
+ @root.each(&block)
32
+ end
33
+
34
+ def inspect
35
+ "<SGF::Gametree:#{object_id}>"
36
+ end
37
+
38
+ def to_s
39
+ SGF::Writer.new.stringify_tree_from @root
40
+ end
41
+
42
+ def slice range
43
+ new_root = nil
44
+ each do |node|
45
+ if node.depth == range.begin
46
+ new_root = node.dup
47
+ break
48
+ end
49
+ end
50
+
51
+ new_root ||= @root.dup
52
+ new_root.depth = 0
53
+ new_root.parent = nil
54
+ SGF::Gametree.new new_root
55
+ end
56
+
57
+ private
58
+
59
+ def method_missing method_name, *args
60
+ human_readable_method = method_name.to_s.downcase
61
+ sgf_identity = SGF::Gametree::PROPERTIES[human_readable_method]
62
+ return @root[sgf_identity] || raise(SGF::NoIdentityError, "This gametree does not have '#{human_readable_method}' available")
63
+ end
64
+ end
@@ -1,109 +1,165 @@
1
- module SGF
2
-
3
- #Your basic node. It holds information about itself, its parent, and its children.
4
- class Node
5
-
6
- attr_accessor :parent, :children, :properties
7
-
8
- # Creates a new node. Arguments which can be passed in are:
9
- # :parent => parent_node (nil by default)
10
- # :children => [list, of, children] (empty array if nothing is passed)
11
- # :properties => {hash_of => properties} (empty hash if nothing is passed)
12
- def initialize args={}
13
- @parent = args[:parent]
14
- @children = []
15
- add_children args[:children] if args[:children]
16
- @properties = Hash.new
17
- add_properties args[:properties] if args[:properties]
18
- end
1
+ require 'observer'
2
+ # Your basic node. It holds information about itself, its parent, and its children.
3
+ class SGF::Node
4
+ include Enumerable, Observable
5
+
6
+ attr_accessor :children, :properties
7
+ attr_reader :parent, :depth
8
+
9
+ # Creates a new node. You can pass in a hash. There are two special keys, parent and children.
10
+ # * parent: parent_node (nil by default)
11
+ # * children: [list, of, children] (empty array if nothing is passed)
12
+ # Anything else passed to the hash will become an SGF identity/property pair on the node.
13
+ def initialize children: [], parent: nil, **opts
14
+ # opts = { children: [], parent: nil }
15
+ # opts.merge! args
16
+ @depth = 0
17
+ @children = []
18
+ @properties = {}
19
+ @parent = nil
20
+ set_parent parent #opts.delete :parent
21
+ add_children children #opts.delete :children
22
+ add_properties opts
23
+ end
19
24
 
20
- #Takes an arbitrary number of child nodes, adds them to the list of children, and make this node their parent.
21
- def add_children *nodes
22
- nodes.flatten!
23
- raise "Non-node child given!" if nodes.any? { |node| node.class != Node }
24
- nodes.each do |node|
25
- node.parent = self
26
- @children << node
27
- end
25
+ # Set the given node as a parent and self as one of that node's children
26
+ def parent= parent
27
+ if @parent
28
+ @parent.children.delete self
29
+ @parent.delete_observer self
28
30
  end
29
31
 
30
- #Takes a hash {identity => property} and adds those to the current node.
31
- #If a property already exists, it will append to it.
32
- def add_properties hash
33
- hash.each do |identity, property|
34
- @properties[identity] ||= property.class.new
35
- @properties[identity].concat property
36
- end
37
- update_human_readable_methods
32
+ case @parent = parent
33
+ when nil then set_depth 0
34
+ else
35
+ @parent.children << self
36
+ @parent.add_observer self
37
+ set_depth @parent.depth + 1
38
38
  end
39
+ end
39
40
 
40
- #Iterate through each child. Yields a child node, if one exists.
41
- def each_child
42
- @children.each { |child| yield child }
43
- end
41
+ alias :set_parent :parent=
44
42
 
45
- #Compare to another node.
46
- def == other_node
47
- @properties == other_node.properties
48
- end
43
+ def remove_parent
44
+ set_parent nil
45
+ end
46
+
47
+ def depth= new_depth
48
+ @depth = new_depth
49
+ changed
50
+ notify_observers :depth_change, @depth
51
+ end
52
+
53
+ alias :set_depth :depth=
49
54
 
50
- #Syntactic sugar for node.properties["XX"]
51
- def [] identity
52
- identity = identity.to_s
53
- @properties[identity]
55
+ # Takes an arbitrary number of child nodes, adds them to the list of children,
56
+ # and make this node their parent.
57
+ def add_children *nodes
58
+ nodes.flatten.each do |node|
59
+ node.set_parent self
54
60
  end
61
+ changed
62
+ notify_observers :new_children, nodes.flatten
63
+ end
55
64
 
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 << ">"
65
+ # Takes a hash {identity => property} and adds those to the current node.
66
+ # If a property already exists, it will append to it.
67
+ def add_properties hash
68
+ hash.each do |identity, property|
69
+ @properties[flexible identity] ||= property.class.new
70
+ @properties[flexible identity].concat property
62
71
  end
72
+ update_human_readable_methods
73
+ end
63
74
 
64
- alias :inspect :to_s
75
+ def each &block
76
+ preorder self, &block
77
+ end
65
78
 
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}")}"
79
+ # Iterate through and yield each child.
80
+ def each_child
81
+ @children.each { |child| yield child }
82
+ end
83
+
84
+ # Compare to another node.
85
+ def == other_node
86
+ @properties == other_node.properties
87
+ end
88
+
89
+ # Syntactic sugar for node.properties["XX"]
90
+ def [] identity
91
+ @properties[flexible(identity)]
92
+ end
93
+
94
+ def []= identity, new_value
95
+ @properties[flexible(identity)] = new_value
96
+ end
97
+
98
+ def inspect
99
+ out = "#<#{self.class}:#{self.object_id}, "
100
+ out << (@parent ? "Has a parent, " : "Has no parent, ")
101
+ out << "#{@children.size} Children, "
102
+ out << "#{@properties.keys.size} Properties"
103
+ out << ">"
104
+ end
105
+
106
+ def to_s(indent = 0)
107
+ properties = []
108
+ @properties.each do |identity, property|
109
+ properties << stringify_identity_and_property(identity, property)
73
110
  end
111
+ whitespace = leading_whitespace(indent)
112
+ "#{whitespace};#{properties.join("\n#{whitespace}")}"
113
+ end
74
114
 
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}]"
115
+ # Observer pattern
116
+ def update(message, data)
117
+ case message
118
+ when :depth_change then set_depth(data + 1)
79
119
  end
120
+ end
80
121
 
81
- private
122
+ private
82
123
 
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
124
+ def flexible id
125
+ id.to_s.upcase
126
+ end
127
+
128
+ def update_human_readable_methods
129
+ SGF::Node::PROPERTIES.reject do
130
+ |method_name, sgf_identity| defined? method_name
131
+ end.each do |human_readable_method, sgf_identity|
132
+ define_method(human_readable_method.to_sym) do
133
+ @properties[sgf_identity] ? @properties[sgf_identity] : raise(SGF::NoIdentityError, "This node does not have #{sgf_identity} available")
89
134
  end
90
135
  end
136
+ end
91
137
 
92
- def leading_whitespace(indent)
93
- "#{" " * indent}"
138
+ def preorder node=self, &block
139
+ yield node
140
+ node.each_child do |child|
141
+ preorder child, &block
94
142
  end
143
+ end
95
144
 
96
- def method_missing method_name, *args
97
- property = method_name.to_s.upcase
98
- if property[/(.*?)=$/]
99
- @properties[$1] = args[0]
100
- else
101
- output = @properties[property]
102
- super(method_name, args) if output.nil?
103
- output
104
- end
145
+ def leading_whitespace(indent)
146
+ ' ' * indent
147
+ end
148
+
149
+ def method_missing method_name, *args
150
+ property = flexible(method_name)
151
+ if property[/(.*?)=$/]
152
+ @properties[$1] = args[0]
153
+ else
154
+ @properties.fetch(property, nil) || super(method_name, args)
105
155
  end
156
+ end
106
157
 
158
+ def stringify_identity_and_property(identity, property)
159
+ new_property = property.instance_of?(Array) ? property.join("][") : property
160
+ new_id = flexible identity
161
+ new_property = new_property.gsub("]", "\\]") if new_id == "C"
162
+ "#{new_id}[#{new_property}]"
107
163
  end
108
164
 
109
- end
165
+ end
@@ -1,167 +1,67 @@
1
- require 'stringio'
2
-
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
8
- class Parser
9
-
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
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
1
+ require_relative 'collection_assembler'
2
+ require_relative 'parsing_tokens'
3
+ require_relative 'error_checkers'
4
+ require_relative 'stream'
5
+
6
+ # The parser returns a SGF::Collection representation of the SGF file
7
+ # parser = SGF::Parser.new
8
+ # collection = parser.parse sgf_in_string_form
9
+ class SGF::Parser
10
+ NEW_NODE = ";"
11
+ BRANCHING = %w{( )}
12
+ END_OF_FILE = false
13
+ NODE_DELIMITERS = [NEW_NODE].concat(BRANCHING).concat([END_OF_FILE])
14
+ PROPERTY = %w([ ])
15
+ LIST_IDENTITIES = %w(AW AB AE AR CR DD LB LN MA SL SQ TR VW TB TW)
16
+
17
+ # This takes as argument an SGF and returns an SGF::Collection object
18
+ # It accepts a local path (String), a stringified SGF (String),
19
+ # or a file handler (File).
20
+ # The second argument is optional, in case you don't want this to raise errors.
21
+ # You probably shouldn't use it, but who's gonna stop you?
22
+ def parse sgf, strict_parsing = true
23
+ error_checker = strict_parsing ? SGF::StrictErrorChecker.new : SGF::LaxErrorChecker.new
24
+ @sgf_stream = SGF::Stream.new(sgf, error_checker)
25
+ @assembler = SGF::CollectionAssembler.new
26
+ until @sgf_stream.eof?
27
+ case @sgf_stream.next_character
28
+ when "(" then @assembler.open_branch
29
+ when ";" then
30
+ parse_node_data
31
+ @assembler.create_node_with_properties @node_properties
32
+ when ")" then @assembler.close_branch
33
+ else next
40
34
  end
41
- @tree
42
- end
43
-
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'
52
- end
53
-
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)
59
- end
60
- end
61
-
62
- def clean sgf
63
- sgf.gsub! "\\\\n\\\\r", ''
64
- sgf.gsub! "\\\\r\\\\n", ''
65
- sgf.gsub! "\\\\r", ''
66
- sgf.gsub! "\\\\n", ''
67
- sgf
68
- end
69
-
70
- def open_branch
71
- @branches.unshift @current_node
72
- end
73
-
74
- def close_branch
75
- @current_node = @branches.shift
76
- end
77
-
78
- def create_new_node
79
- node = Node.new
80
- @current_node.add_children node
81
- @current_node = node
82
- end
83
-
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
92
-
93
- def add_properties_to_current_node
94
- @current_node.add_properties @node_properties
95
- end
96
-
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
106
- end
107
-
108
- def parse_identity
109
- @identity = ""
110
- while char = next_character and char != "["
111
- @identity << char unless char == "\n"
112
- end
113
- end
114
-
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
123
-
124
- def parse_comment
125
- while char = next_character and still_inside_comment? char
126
- @property << char
127
- end
128
- @property.gsub! "\\]", "]"
129
- end
130
-
131
- def parse_multi_property
132
- while char = next_character and still_inside_multi_property? char
133
- @property << char
134
- end
135
- @property = @property.gsub("][", ",").split(",")
136
35
  end
36
+ @assembler.collection
37
+ end
137
38
 
138
- def parse_generic_property
139
- while char = next_character and char != "]"
140
- @property << char
39
+ private
40
+
41
+ def parse_node_data
42
+ @node_properties = {}
43
+ while still_inside_node?
44
+ identity = @sgf_stream.read_token SGF::IdentityToken.new
45
+ property_format = property_token_type identity
46
+ property = @sgf_stream.read_token property_format
47
+ if @node_properties[identity]
48
+ @node_properties[identity].concat property
49
+ @assembler.add_error "Multiple #{identity} identities are present in a single node. A property should only exist once per node."
50
+ else
51
+ @node_properties[identity] = property
141
52
  end
142
53
  end
54
+ end
143
55
 
144
- def still_inside_comment? char
145
- char != "]" || (char == "]" && @property[-1..-1] == "\\")
146
- end
147
-
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
158
- end
56
+ def still_inside_node?
57
+ !NODE_DELIMITERS.include?(@sgf_stream.peek_skipping_whitespace)
58
+ end
159
59
 
160
- def next_character
161
- !@stream.eof? && @stream.sysread(1)
60
+ def property_token_type identity
61
+ case identity.upcase
62
+ when "C" then SGF::CommentToken.new
63
+ when *LIST_IDENTITIES then SGF::MultiPropertyToken.new
64
+ else SGF::GenericPropertyToken.new
162
65
  end
163
-
164
66
  end
165
-
166
67
  end
167
-