UNPKG

pgnify

Version:

A lightning fast [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) parser.

466 lines (456 loc) 12.9 kB
"use strict"; 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 });