-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.js
201 lines (160 loc) · 4.55 KB
/
index.js
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/**
* Convert SGF files to a JS object
* @param {string} sgf A valid SGF file.
* @see http://www.red-bean.com/sgf/sgf4.html
* @return {object} The SGF file represented as a JS object
*/
exports.parse = function (sgf) {
'use strict';
var parse;
var parser;
var collection = {};
// tracks the current sequence
var sequence;
// tracks the current node
var node;
// tracks the last PropIdent
var lastPropIdent;
// A map of functions to parse the different components of an SGF file
parser = {
beginSequence: function (sgf) {
var key = 'sequences';
// Top-level sequences are gameTrees
if (!sequence) {
sequence = collection;
key = 'gameTrees';
}
if (sequence.gameTrees) {
key = 'gameTrees';
}
var newSequence = {
parent: sequence
};
sequence[key] = sequence[key] || [];
sequence[key].push(newSequence);
sequence = newSequence;
return sgf.substring(1);
},
endSequence: function (sgf) {
if (sequence.parent) {
sequence = sequence.parent;
} else {
sequence = null;
}
return sgf.substring(1);
},
node: function (sgf) {
node = {};
sequence.nodes = sequence.nodes || [];
sequence.nodes.push(node);
return sgf.substring(1);
},
property: function (sgf) {
var propValue;
// Search for the first unescaped ]
var firstPropEnd = sgf.match(/([^\\\]]|\\(.|\n|\r))*\]/);
if (!firstPropEnd.length) {
throw new Error('malformed sgf');
}
firstPropEnd = firstPropEnd[0].length;
var property = sgf.substring(0, firstPropEnd);
var propValueBegin = property.indexOf('[');
var propIdent = property.substring(0, propValueBegin);
// Point lists don't declare a PropIdent for each PropValue
// Instead, they should use the last declared property
// See: http://www.red-bean.com/sgf/sgf4.html#move/pos
if (!propIdent) {
propIdent = lastPropIdent;
// If this is the first property in a list of multiple
// properties, we need to wrap the PropValue in an array
if (!Array.isArray(node[propIdent])) {
node[propIdent] = [node[propIdent]];
}
}
lastPropIdent = propIdent;
propValue = property.substring(propValueBegin + 1, property.length - 1);
// We have no problem parsing PropIdents of any length, but the spec
// says they should be no longer than two characters.
//
// http://www.red-bean.com/sgf/sgf4.html#2.2
if (propIdent.length > 2) {
// TODO: What's the best way to issue a warning?
console.warn(
'SGF PropIdents should be no longer than two characters:', propIdent
);
}
if (Array.isArray(node[propIdent])) {
node[propIdent].push(propValue);
} else {
node[propIdent] = propValue;
}
return sgf.substring(firstPropEnd);
},
// Whitespace, tabs, or anything else we don't recognize
unrecognized: function (sgf) {
// March ahead to the next character
return sgf.substring(1);
}
};
// Processes an SGF file character by character
parse = function (sgf) {
while (sgf) {
var initial = sgf.substring(0, 1);
var type;
// Use the initial (the first character in the remaining sgf file) to
// decide which parser function to use
if (initial === '(') {
type = 'beginSequence';
} else if (initial === ')') {
type = 'endSequence';
} else if (initial === ';') {
type = 'node';
} else if (initial.search(/[A-Z\[]/) !== -1) {
type = 'property';
} else {
type = 'unrecognized';
}
sgf = parser[type](sgf);
}
return collection;
};
// Begin parsing the SGF file
return parse(sgf);
};
/**
* Generate an SGF file from a SmartGame Record JavaScript Object
* @param {object} record A record object.
* @return {string} The record as a string suitable for saving as an SGF file
*/
exports.generate = function (record) {
'use strict';
function stringifySequences(sequences) {
var contents = '';
sequences.forEach(function (sequence) {
contents += '(';
// Parse all nodes in this sequence
if (sequence.nodes) {
sequence.nodes.forEach(function (node) {
var nodeString = ';';
for (var property in node) {
if (node.hasOwnProperty(property)) {
var prop = node[property];
if (Array.isArray(prop)) {
prop = prop.join('][');
}
nodeString += property + '[' + prop + ']';
}
}
contents += nodeString;
});
}
// Call the function we're in recursively for any child sequences
if (sequence.sequences) {
contents += stringifySequences(sequence.sequences);
}
contents += ')';
});
return contents;
}
return stringifySequences(record.gameTrees);
};