pgnify
Version:
A lightning fast [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) parser.
466 lines (456 loc) • 12.9 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
GameResults: () => GameResults_default,
parse: () => parse,
parseHeaders: () => parseHeaders,
parseMoveString: () => parseMoveString,
splitPGNs: () => splitPGNs,
stringifyHeaders: () => stringifyHeaders
});
module.exports = __toCommonJS(src_exports);
// src/GameResults.ts
var GameResults = {
NONE: "*",
DRAW: "1/2-1/2",
WHITE_WIN: "1-0",
BLACK_WIN: "0-1"
};
var GameResults_default = GameResults;
// src/headers.ts
var headerRegex = /^\[(?<key>\w+)\s+"(?<value>[^"]*)"\]/;
function parseHeaders(pgn) {
const headers = {};
pgn = pgn.trim();
let matchArr = pgn.match(headerRegex);
while (matchArr) {
const { key, value } = matchArr.groups;
headers[key] = value;
pgn = pgn.slice(matchArr[0].length).trimStart();
matchArr = pgn.match(headerRegex);
}
return {
headers,
moveString: pgn
};
}
function stringifyHeaders(headers) {
return Object.entries(headers).map(([key, value]) => `[${key} "${value}"]`).join("\n");
}
// src/TokenKind.ts
var TokenKind = /* @__PURE__ */ ((TokenKind2) => {
TokenKind2[TokenKind2["EndOfFile"] = 0] = "EndOfFile";
TokenKind2[TokenKind2["Bad"] = 1] = "Bad";
TokenKind2[TokenKind2["Whitespace"] = 2] = "Whitespace";
TokenKind2[TokenKind2["OpeningParenthesis"] = 3] = "OpeningParenthesis";
TokenKind2[TokenKind2["ClosingParenthesis"] = 4] = "ClosingParenthesis";
TokenKind2[TokenKind2["Points"] = 5] = "Points";
TokenKind2[TokenKind2["MoveNumber"] = 6] = "MoveNumber";
TokenKind2[TokenKind2["Notation"] = 7] = "Notation";
TokenKind2[TokenKind2["NumericAnnotationGlyph"] = 8] = "NumericAnnotationGlyph";
TokenKind2[TokenKind2["Comment"] = 9] = "Comment";
TokenKind2[TokenKind2["GameResult"] = 10] = "GameResult";
return TokenKind2;
})(TokenKind || {});
var TokenKind_default = TokenKind;
// src/string-utils.ts
var EOF = "\0";
function isDigit(ch) {
return ch === "0" || ch === "1" || ch === "2" || ch === "3" || ch === "4" || ch === "5" || ch === "6" || ch === "7" || ch === "8" || ch === "9";
}
function isNumeric(str) {
for (const ch of str)
if (!isDigit(ch))
return false;
return true;
}
function isWhiteSpace(ch) {
return ch === " " || ch === "\n" || ch === " " || ch === "\r" || ch === "\f" || ch === "\v" || ch === "\xA0" || ch === "\u1680" || ch === "\u2000" || ch === "\u200A" || ch === "\u2028" || ch === "\u2029" || ch === "\u202F" || ch === "\u205F" || ch === "\u3000" || ch === "\uFEFF";
}
function isBracket(ch) {
return ch === "(" || ch === ")" || ch === "[" || ch === "]" || ch === "{" || ch === "}";
}
function isNotReservedPunctuationOrWhitespace(ch) {
return ch !== "." && ch !== "$" && !isBracket(ch) && !isWhiteSpace(ch);
}
function isGameResult(arg) {
return arg === GameResults_default.NONE || arg === GameResults_default.DRAW || arg === GameResults_default.WHITE_WIN || arg === GameResults_default.BLACK_WIN;
}
function isPieceInitial(ch) {
return ch === "N" || ch === "B" || ch === "R" || ch === "Q" || ch === "K";
}
function isFile(ch) {
return ch === "a" || ch === "b" || ch === "c" || ch === "d" || ch === "e" || ch === "f" || ch === "g" || ch === "h";
}
function isRank(ch) {
return ch === "1" || ch === "2" || ch === "3" || ch === "4" || ch === "5" || ch === "6" || ch === "7" || ch === "8";
}
// src/Lexer.ts
var Lexer = class {
constructor(input) {
this.index = 0;
this.input = input;
}
get current() {
if (this.index < this.input.length)
return this.input[this.index];
return EOF;
}
lex() {
switch (this.current) {
case EOF:
return this.getEndOfFileToken();
case "(":
return this.getParenToken(TokenKind_default.OpeningParenthesis);
case ")":
return this.getParenToken(TokenKind_default.ClosingParenthesis);
case "{":
return this.scanComment();
case ".":
return this.scanPoints();
case "$":
return this.scanNAG();
default:
return this.scanOther();
}
}
advance() {
this.index++;
}
getEndOfFileToken() {
return {
kind: TokenKind_default.EndOfFile,
value: EOF,
index: this.index
};
}
getParenToken(kind) {
const { index } = this;
this.advance();
return {
kind,
value: "",
index
};
}
scanWhile(predicate) {
let substring = "";
for (let ch = this.current; ch !== EOF && predicate(ch, substring); ch = this.current) {
substring += ch;
this.advance();
}
return substring;
}
scanComment() {
const { index } = this;
this.advance();
const comment = this.scanWhile((ch, substring) => {
return ch !== "}" && substring.at(-1) !== "\\";
});
this.advance();
return {
kind: TokenKind_default.Comment,
value: comment.trim(),
index
};
}
scanPoints() {
const { index } = this;
const points = this.scanWhile((ch) => ch === ".");
return {
kind: TokenKind_default.Points,
value: points,
index
};
}
scanNAG() {
const { index } = this;
this.advance();
const digits = this.scanWhile(isDigit);
const kind = digits.length === 0 ? TokenKind_default.Bad : TokenKind_default.NumericAnnotationGlyph;
return { kind, value: "$" + digits, index };
}
scanOther() {
const { index } = this;
if (isWhiteSpace(this.current)) {
this.scanWhile(isWhiteSpace);
return {
kind: TokenKind_default.Whitespace,
value: "",
index
};
}
const value = this.scanWhile(isNotReservedPunctuationOrWhitespace);
const kind = isGameResult(value) ? TokenKind_default.GameResult : isNumeric(value) ? TokenKind_default.MoveNumber : TokenKind_default.Notation;
return {
kind,
value,
index
};
}
};
// src/errors.ts
var UnexpectedTokenError = class extends SyntaxError {
constructor(token, details = {}) {
super("Unexpected token.");
this.cause = {
tokenKind: TokenKind_default[token.kind],
value: token.value,
index: token.index,
...details
};
}
};
// src/move.ts
var files = {
a: 0,
b: 1,
c: 2,
d: 3,
e: 4,
f: 5,
g: 6,
h: 7
};
var ranks = {
"1": 0,
"2": 1,
"3": 2,
"4": 3,
"5": 4,
"6": 5,
"7": 6,
"8": 7
};
var moveCache = /* @__PURE__ */ new Map();
function getMove(notation) {
if (moveCache.has(notation))
return moveCache.get(notation);
const move = getMoveUncached(notation);
moveCache.set(notation, move);
return move;
}
function getMoveUncached(notation) {
if (isPieceInitial(notation[0]))
return getPieceMove(notation.replace("x", ""));
if (isFile(notation[0]))
return getPawnMove(notation);
if (notation[0] === "0" || notation[0] === "O")
return getCastlingMove(notation);
return {
type: "unknown",
notation
};
}
function getPieceMove(notation) {
const pieceInitial = notation[0];
if (isRank(notation[1]))
return {
type: "piece-move",
pieceInitial,
srcY: ranks[notation[1]],
destX: files[notation[2]],
destY: ranks[notation[3]]
};
const srcOrDestX = files[notation[1]];
if (isFile(notation[2]))
return {
type: "piece-move",
pieceInitial,
srcX: srcOrDestX,
destX: files[notation[2]],
destY: ranks[notation[3]]
};
if (isFile(notation[3]))
return {
type: "piece-move",
pieceInitial,
srcX: srcOrDestX,
srcY: ranks[notation[2]],
destX: files[notation[3]],
destY: ranks[notation[4]]
};
return {
type: "piece-move",
pieceInitial,
destX: srcOrDestX,
destY: ranks[notation[2]]
};
}
function getPawnMove(notation) {
const isCapture = notation[1] === "x";
const offset = isCapture ? 2 : 0;
const move = {
type: "pawn-move",
srcX: files[notation[0]],
destX: files[notation[offset]],
destY: ranks[notation[1 + offset]]
};
if (notation[2 + offset] === "=")
move.promotionInitial = notation[3 + offset];
return move;
}
function getCastlingMove(notation) {
return {
type: "castling",
isQueenSide: notation[4] === notation[0]
};
}
// src/parse.ts
function parse(pgn) {
const { headers, moveString } = parseHeaders(pgn);
const { mainLine, result } = parseMoveString(moveString);
return {
headers,
mainLine,
result: result ?? headers.Result ?? GameResults_default.NONE
};
}
function parseMoveString(moveString) {
const tokens = getTokens(moveString);
const stack = [];
let variation = [];
let token;
let result = null;
let commentBefore = "";
for (let i = 0; i < tokens.length; i++) {
token = tokens[i];
switch (token.kind) {
case TokenKind_default.MoveNumber: {
if (i + 2 >= tokens.length)
throw new UnexpectedTokenError(token);
handleMoveNumber(variation, token, tokens[++i], tokens[++i]);
if (commentBefore) {
variation[variation.length - 1].commentBefore = commentBefore;
commentBefore = "";
}
break;
}
case TokenKind_default.Notation: {
handleNotation(variation, token);
break;
}
case TokenKind_default.NumericAnnotationGlyph: {
handleNAG(variation, token);
break;
}
case TokenKind_default.Comment: {
variation.length === 0 ? commentBefore = token.value : variation[variation.length - 1].commentAfter = token.value;
break;
}
case TokenKind_default.GameResult: {
if (stack.length > 0)
throw new UnexpectedTokenError(token);
result = token.value;
break;
}
case TokenKind_default.OpeningParenthesis: {
const lastNode = variation.at(-1);
if (!lastNode)
throw new UnexpectedTokenError(token);
stack.push(variation);
variation = [];
lastNode.variations ?? (lastNode.variations = []);
lastNode.variations.push(variation);
break;
}
case TokenKind_default.ClosingParenthesis: {
const parentVar = stack.pop();
if (!parentVar)
throw new UnexpectedTokenError(token);
variation = parentVar;
break;
}
case TokenKind_default.Points:
case TokenKind_default.Bad: {
throw new UnexpectedTokenError(token);
}
}
}
if (stack.length > 0)
throw new SyntaxError(`Unfinished variation at index ${token.index}.`);
return {
mainLine: variation,
result
};
}
function getTokens(moveString) {
const lexer = new Lexer(moveString);
const tokens = [];
let token;
do {
token = lexer.lex();
if (token.kind !== TokenKind_default.Whitespace)
tokens.push(token);
} while (token.kind !== TokenKind_default.EndOfFile);
return tokens;
}
function assertKind(token, expectedKind) {
if (token.kind !== expectedKind)
throw new UnexpectedTokenError(token, {
expected: TokenKind_default[expectedKind]
});
}
function handleMoveNumber(variation, moveNoToken, pointsToken, notationToken) {
assertKind(pointsToken, TokenKind_default.Points);
assertKind(notationToken, TokenKind_default.Notation);
variation.push({
moveNumber: +moveNoToken.value,
move: getMove(notationToken.value),
isWhiteMove: pointsToken.value === "."
});
}
function handleNotation(variation, token) {
const prevNode = variation.at(-1);
variation.push({
moveNumber: prevNode?.moveNumber ?? 1,
move: getMove(token.value),
isWhiteMove: prevNode ? !prevNode.isWhiteMove : true
});
}
function handleNAG(variation, token) {
const moveNode = variation.at(-1);
if (moveNode)
moveNode.NAG = token.value;
}
var resultRegex = /(\*|1\/2-1\/2|[01]-[01])\s*$/;
function* splitPGNs(input) {
const lines = input.split(/\r?\n/);
let PGN = "";
for (const line of lines) {
PGN += line;
if (resultRegex.test(line)) {
yield PGN;
PGN = "";
continue;
}
PGN += " ";
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
GameResults,
parse,
parseHeaders,
parseMoveString,
splitPGNs,
stringifyHeaders
});