-
Notifications
You must be signed in to change notification settings - Fork 29
/
parseSgf.js
171 lines (149 loc) · 6.01 KB
/
parseSgf.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
besogo.parseSgf = function(text) {
'use strict';
var at = 0, // Current position
ch = text.charAt(at); // Current character at position
findOpenParens(); // Find beginning of game tree
return parseTree(); // Parse game tree
// Builds and throws an error
function error(msg) {
throw {
name: "Syntax Error",
message: msg,
at: at,
text: text
};
}
// Advances text position by one
function next(check) {
if (check && check !== ch) { // Verify current character if param given
error( "Expected '" + check + "' instead of '" + ch + "'");
}
at++;
ch = text.charAt(at);
return ch;
}
// Skips over whitespace until non-whitespace found
function white() {
while (ch && ch <= ' ') {
next();
}
}
// Skips all chars until '(' or end found
function findOpenParens() {
while (ch && ch !== '(') {
next();
}
}
// Returns true if line break (CR, LF, CR+LF, LF+CR) found
// Advances the cursor ONCE for double character (CR+LF, LF+CR) line breaks
function lineBreak() {
if (ch === '\n') { // Line Feed (LF)
if (text.charAt(at + 1) === '\r') { // LF+CR, double character line break
next(); // Advance cursor only once (pointing at second character)
}
return true;
} else if (ch === '\r') { // Carriage Return (CR)
if (text.charAt(at + 1) === '\n') { // CR+LF, double character line break
next(); // Advance cursor only once (pointing at second character)
}
return true;
}
return false; // Did not find a line break or advance
}
// Parses a sub-tree of the game record
function parseTree() {
var rootNode, // Root of this sub-tree
currentNode, // Pointer to parent of the next node
nextNode; // Scratch for parsing the next node or sub-tree
next('('); // Double-check opening parens at start of sub-tree
white(); // Skip whitespace before root node
if (ch !== ";") { // Error on sub-tree missing root node
error("Sub-tree missing root");
}
rootNode = parseNode(); // Get the first node of this sub-tree
white(); // Skip whitespace before parsing next node
currentNode = rootNode; // Parent of the next node parsed
while (ch === ';') { // Get sequence of nodes within this sub-tree
nextNode = parseNode(); // Parse the next node
// Add next node as child of current
currentNode.children.push(nextNode);
currentNode = nextNode; // Advance current pointer to this child
white(); // Skip whitespace between/after sequence nodes
}
// Look for sub-trees of this sub-tree
while (ch === "(") {
nextNode = parseTree(); // Parse the next sub-tree
// Add sub-tree as child of last sequence node
currentNode.children.push(nextNode); // Do NOT advance current
white(); // Skip whitespace between/after sub-trees
}
next(')'); // Expect closing parenthesis at end of this sub-tree
return rootNode;
}
// Parses a node and its properties
function parseNode() {
var property, // Scratch for parsing properties
node = { props: [], children: [] }; // Node to construct
next(';'); // Double-check semi-colon at start of node
white(); // Skip whitespace before properties
// Parse properties until end of node detected
while ( ch && ch !== ';' && ch !== '(' && ch !== ')') {
property = parseProperty(); // Parse the property and values
node.props.push(property); // Add property to node
white(); // Skip whitespace between/after properties
}
return node;
}
// Parses a property and its values
function parseProperty() {
var property = { id: '', values: [] }; // Property to construct
// Look for property ID within letters
while ( ch && /[A-Za-z]/.test(ch) ) {
if (/[A-Z]/.test(ch)) { // Ignores lower case letters
property.id += ch; // Only adds upper case letters
}
next();
}
if (!property.id) { // Error if id empty
error('Missing property ID');
}
white(); // Skip whitespace before values
while(ch === '[') { // Look for values of this property
property.values.push( parseValue() );
white(); // Skip whitespace between/after values
}
if (property.values.length === 0) { // Error on empty list of values
error('Missing property values');
}
return property;
}
// Parses a value
function parseValue() {
var value = '';
next('['); // Double-check opening bracket at start of value
// Read until end of value (unescaped closing bracket)
while ( ch && ch !== ']' ) {
if ( ch === '\\' ) { // Backslash escape handling
next('\\');
if (lineBreak()) { // Soft (escaped) line break
// Nothing, soft line breaks are removed
} else if (ch <= ' ') { // Other whitespace
value += ' '; // Convert to space
} else {
value += ch; // Pass other escaped characters verbatim
}
} else { // Non-escaped character
if (lineBreak()) { // Hard (non-escaped) line break
value += '\n'; // Convert all new lines to just LF
} else if (ch <= ' ') { // Other whitespace
value += ' '; // Convert to space
} else {
value += ch; // Other characters
}
}
next();
}
next(']'); // Expect closing bracket at end of value
return value;
}
};