SgfParser 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +13 -0
- data/.gitignore +9 -0
- data/.rspec +3 -1
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/CHANGELOG +12 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +42 -16
- data/README.md +97 -0
- data/SgfParser.gemspec +5 -8
- data/bin/sgf_indent.rb +5 -4
- data/examples/simple_iteration_of_games_in_a_directory.rb +3 -3
- data/lib/sgf.rb +18 -9
- data/lib/sgf/collection.rb +75 -0
- data/lib/sgf/collection_assembler.rb +32 -0
- data/lib/sgf/error.rb +1 -2
- data/lib/sgf/error_checkers.rb +15 -0
- data/lib/sgf/gametree.rb +64 -0
- data/lib/sgf/node.rb +139 -83
- data/lib/sgf/parser.rb +57 -157
- data/lib/sgf/parsing_tokens.rb +40 -0
- data/lib/sgf/properties.rb +80 -83
- data/lib/sgf/stream.rb +50 -0
- data/lib/sgf/variation.rb +16 -0
- data/lib/sgf/version.rb +1 -1
- data/lib/sgf/writer.rb +35 -37
- data/spec/acceptance_spec.rb +27 -0
- data/spec/collection_spec.rb +65 -0
- data/spec/gametree_spec.rb +99 -0
- data/spec/node_spec.rb +174 -68
- data/spec/parser_spec.rb +76 -74
- data/spec/spec_helper.rb +90 -9
- data/spec/variation_spec.rb +15 -0
- data/spec/writer_spec.rb +22 -21
- data/wercker.yml +21 -0
- metadata +77 -39
- data/.rvmrc +0 -1
- data/README.rdoc +0 -18
- data/lib/sgf/game.rb +0 -64
- data/lib/sgf/tree.rb +0 -77
- data/spec/game_spec.rb +0 -70
- data/spec/tree_spec.rb +0 -36
@@ -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
|
data/lib/sgf/gametree.rb
ADDED
@@ -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
|
data/lib/sgf/node.rb
CHANGED
@@ -1,109 +1,165 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
41
|
-
def each_child
|
42
|
-
@children.each { |child| yield child }
|
43
|
-
end
|
41
|
+
alias :set_parent :parent=
|
44
42
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
75
|
+
def each &block
|
76
|
+
preorder self, &block
|
77
|
+
end
|
65
78
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
122
|
+
private
|
82
123
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
data/lib/sgf/parser.rb
CHANGED
@@ -1,167 +1,67 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
161
|
-
|
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
|
-
|