@echecs/pgn - v5.0.0
    Preparing search index...

    @echecs/pgn - v5.0.0

    PGN

    npm Coverage License: MIT Spec

    PGN is a fast, lightweight TypeScript parser for Portable Game Notation — the standard format for recording chess games.

    It parses PGN input into structured move objects with decomposed SAN, paired white/black moves, and full support for annotations and variations. Zero runtime dependencies. The smallest PGN parser on npm.

    Package Pack size Unpacked
    @echecs/pgn 42 KB 195 KB
    pgn-parser 99 KB 606 KB
    @mliebelt/pgn-parser 148 KB 595 KB
    chess.js 150 KB 724 KB

    Most PGN parsers on npm either give you raw strings with no structure, or fail on anything beyond a plain game record. If you're building a chess engine, opening book, or game viewer, you need more:

    • Decomposed SAN — every move is parsed into piece, from, to, capture, promotion, check, and checkmate fields. No regex on your side.
    • Paired move structure — moves are returned as [moveNumber, whiteMove, blackMove] tuples, ready to render or process without further work.
    • RAV support — recursive annotation variations ((...) sub-lines) are parsed into a variants tree on each move. Essential for opening books and annotated games.
    • NAG support — symbolic (!, ?, !!, ??, !?, ?!) and numeric ($1$255) annotations are surfaced as an annotations array. Essential for Lichess and ChessBase exports.
    • Multi-game files — parse entire PGN databases in one call. Tested on files with 3 500+ games.
    • Fast — built on a Peggy PEG parser. Throughput is within 1.1–1.2x of the fastest parsers on npm, which do far less work per move (see BENCHMARK_RESULTS.md).

    If you only need raw SAN strings and a flat move list, any PGN parser will do. If you need structured, engine-ready output with annotations and variations, this is the one.

    npm install @echecs/pgn
    
    import parse from '@echecs/pgn';

    const games = parse(`
    [Event "Example"]
    [White "Player1"]
    [Black "Player2"]
    [Result "1-0"]

    1. e4 e5 2. Nf3 Nc6 3. Bb5 1-0
    `);

    console.log(games[0].moves[0]);
    // [1, { piece: 'pawn', to: 'e4', capture: false, ... }, { piece: 'pawn', to: 'e5', capture: false, ... }]

    Takes a PGN string and returns an array of game objects — one per game in the file.

    parse(input: string, options?: ParseOptions): PGN[]
    

    Deprecated. Use parse() instead — it already handles multi-game input. stream() will be removed in the next major version. It emits a console.warn on first call.

    Converts one or more parsed PGN objects back into a valid PGN string, providing semantic round-trip fidelity.

    stringify(input: PGN | PGN[], options?: StringifyOptions): string
    

    Reconstructs SAN from Notation fields, re-serializes annotation commands ([%cal], [%csl], [%clk], [%eval]) back into comment blocks, and preserves RAVs and NAGs. Pass onWarning to observe recoverable issues (e.g. invalid castling destination, negative clock). StringifyOptions is a subset of ParseOptionsonError is not accepted since stringify never fails hard.

    import parse, { stringify } from '@echecs/pgn';

    const games = parse(pgnString);
    const output = stringify(games); // valid PGN string

    By default, parse() silently returns [] on parse failure. Pass an onError callback to observe failures:

    import parse, { type ParseError } from '@echecs/pgn';

    const games = parse(input, {
    onError(err: ParseError) {
    console.error(
    `Parse failed at line ${err.line}:${err.column}${err.message}`,
    );
    },
    });

    onError receives a ParseError with:

    Field Type Description
    message string Human-readable description from the parser
    offset number Character offset in the input (0-based)
    line number 1-based line number
    column number 1-based column number

    Pass onWarning to observe spec-compliance issues that do not prevent parsing:

    import parse, { type ParseWarning } from '@echecs/pgn';

    const games = parse(input, {
    onWarning(warn: ParseWarning) {
    console.warn(warn.message);
    },
    });

    onWarning receives a ParseWarning with the same fields as ParseError: message, offset, line, column.

    Currently fires for:

    • Missing STR tags (Black, Date, Event, Result, Round, Site, White) — emitted in alphabetical key order; position fields are nominal placeholders
    • Move number mismatch (declared move number in the PGN text doesn't match the move's actual position) — position fields are nominal placeholders
    • Result tag mismatch ([Result "..."] tag value differs from the game termination marker) — position fields are nominal placeholders
    • Duplicate tag names — line and column point to the opening [ of the duplicate tag

    The same option is accepted by stringify().

    {
    meta: Meta, // tag pairs (Event, Site, Date, White, Black, …)
    moves: NotationList, // paired notation list
    result: 1 | 0 | 0.5 | '?'
    }

    meta is an index of all tag pairs from the PGN header. The Result key is optional — games with no tag pairs return meta: {}. Use game.result (always present) as the authoritative game outcome.

    Notation extends SAN from @echecs/san — all SAN fields are always present.

    {
    // SAN fields (always present)
    piece: Piece, // 'pawn' | 'knight' | 'bishop' | 'rook' | 'queen' | 'king'
    to: Square | undefined, // destination square, e.g. "e4"; undefined for castling
    from: Disambiguation | undefined, // file "e", rank "2", or square "e2"
    capture: boolean,
    castling: boolean,
    check: boolean,
    checkmate: boolean,
    long: boolean, // queenside castling
    promotion: PromotionPiece | undefined, // 'queen' | 'rook' | 'bishop' | 'knight'

    // PGN-specific fields (optional)
    annotations?: string[], // e.g. ["!", "$14"]
    comment?: string,
    arrows?: Arrow[], // from [%cal ...] command
    squares?: SquareAnnotation[], // from [%csl ...] command
    clock?: number, // from [%clk ...] — seconds remaining
    eval?: Eval, // from [%eval ...] — engine evaluation
    variants?: NotationList[], // recursive annotation variations
    }

    Moves are grouped into tuples: [moveNumber, whiteMove, blackMove]. Both move slots can be undefinedwhiteMove when a variation begins on black's turn, blackMove when the game or variation ends on white's move.

    12. Nf3! $14 { White has a slight advantage }
    
    {
    piece: 'knight', to: 'f3', capture: false, castling: false,
    check: false, checkmate: false, long: false,
    from: undefined, promotion: undefined,
    annotations: ['!', '14'], // numeric NAGs stored without '$' prefix
    comment: 'White has a slight advantage'
    }

    PGN files produced by GUIs and engines embed structured commands inside move comments using the [%cmd ...] syntax. This library parses the four most common commands and exposes them as dedicated fields on Notation:

    Field Type PGN command Description
    arrows Arrow[] [%cal ...] Coloured arrows drawn on the board
    squares SquareAnnotation[] [%csl ...] Coloured square highlights
    clock number [%clk ...] Remaining time in seconds (sub-second preserved)
    eval Eval [%eval ...] Engine evaluation (centipawns or mate-in-N)

    Command strings are stripped from move.comment. Unknown [%...] commands are left in the comment string unchanged.

    type AnnotationColor = 'B' | 'C' | 'G' | 'O' | 'R' | 'Y'; // Blue, Cyan, Green, Orange, Red, Yellow

    interface Arrow {
    color: AnnotationColor;
    from: Square; // e.g. "e2"
    to: Square; // e.g. "e4"
    }

    interface SquareAnnotation {
    color: AnnotationColor;
    square: Square; // e.g. "e4"
    }

    type Eval =
    | { type: 'cp'; value: number; depth?: number } // centipawn score
    | { type: 'mate'; value: number; depth?: number }; // mate in N
    1. e4 { [%cal Ge2e4,Re4e5] [%clk 0:05:00] } e5
    
    {
    piece: 'pawn', to: 'e4', capture: false, castling: false,
    check: false, checkmate: false, long: false,
    from: undefined, promotion: undefined,
    // comment is absent — no free text remains after stripping commands
    arrows: [
    { color: 'G', from: 'e2', to: 'e4' },
    { color: 'R', from: 'e4', to: 'e5' },
    ],
    clock: 300, // 5 minutes in seconds
    }
    5... Ba5 (5... Be7 6. d4) 6. Qb3
    

    The alternative line appears as a variants array on the move where it branches:

    {
    piece: 'bishop', to: 'a5', capture: false, castling: false,
    check: false, checkmate: false, long: false,
    from: undefined, promotion: undefined,
    variants: [
    [ [5, undefined, { piece: 'bishop', to: 'e7', ... }], [6, { piece: 'pawn', to: 'd4', ... }] ]
    ]
    }

    All public types are exported as named type exports:

    import type {
    AnnotationColor, // 'B' | 'C' | 'G' | 'O' | 'R' | 'Y'
    Arrow, // { color, from, to }
    Disambiguation, // Square | File | Rank
    Eval, // { type: 'cp' | 'mate', value, depth? }
    File, // 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h'
    Meta, // { [key: string]: string | undefined }
    Notation, // single parsed notation object (extends SAN)
    NotationList, // NotationPair[]
    NotationPair, // [number, Notation | undefined, Notation?]
    ParseError, // { message, offset, line, column }
    ParseOptions, // { onError?, onWarning? }
    ParseWarning, // { message, offset, line, column }
    PGN, // { meta, moves, result }
    Piece, // 'pawn' | 'knight' | 'bishop' | 'rook' | 'queen' | 'king'
    PromotionPiece, // 'knight' | 'bishop' | 'rook' | 'queen'
    Rank, // '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8'
    Result, // '1-0' | '0-1' | '1/2-1/2' | '?'
    SAN, // base SAN interface from @echecs/san
    Square, // `${File}${Rank}`, e.g. "e4"
    SquareAnnotation, // { color, square }
    StringifyOptions, // { onWarning? }
    Variation, // NotationList[]
    } from '@echecs/pgn';

    Contributions are welcome. Please read CONTRIBUTING.md for guidelines on how to submit issues and pull requests.