SgfParser 2.0.0 → 3.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.
@@ -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
-