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