besogo
Version:
Embeddable SGF player for the game of Go (aka Weiqi, Baduk)
172 lines (149 loc) • 6.16 kB
JavaScript
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;
}
};