https://github.com/echecsjs/pgn
PGN (Portable Game Notation) parser. Supports comments, variations, NAGs, and annotations.
https://github.com/echecsjs/pgn
chess parser pgn typescript
Last synced: about 2 hours ago
JSON representation
PGN (Portable Game Notation) parser. Supports comments, variations, NAGs, and annotations.
- Host: GitHub
- URL: https://github.com/echecsjs/pgn
- Owner: echecsjs
- License: mit
- Created: 2020-04-10T04:57:06.000Z (about 6 years ago)
- Default Branch: main
- Last Pushed: 2026-06-30T13:04:16.000Z (1 day ago)
- Last Synced: 2026-06-30T15:08:32.954Z (about 22 hours ago)
- Topics: chess, parser, pgn, typescript
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/@echecs/pgn
- Size: 9.99 MB
- Stars: 4
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# PGN
[](https://www.npmjs.com/package/@echecs/pgn)
[](https://codecov.io/gh/echecsjs/pgn)
[](LICENSE)
[](SPEC.md)
**PGN** is a fast, lightweight TypeScript parser for
[Portable Game Notation](http://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm)
— 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 |
## Why this library?
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](https://peggyjs.org/) 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](./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.
## Installation
```bash
npm install @echecs/pgn
```
## Quick Start
```typescript
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, ... }]
```
## Usage
### `parse()`
Takes a PGN string and returns an array of game objects — one per game in the
file.
```typescript
parse(input: string, options?: ParseOptions): PGN[]
```
### ~~`stream()`~~ (deprecated)
> **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.
### `stringify()`
Converts one or more parsed `PGN` objects back into a valid PGN string,
providing semantic round-trip fidelity.
```typescript
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
`ParseOptions` — `onError` is not accepted since `stringify` never fails hard.
```typescript
import parse, { stringify } from '@echecs/pgn';
const games = parse(pgnString);
const output = stringify(games); // valid PGN string
```
### Error handling
By default, `parse()` silently returns `[]` on parse failure. Pass an `onError`
callback to observe failures:
```typescript
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 |
### Warnings
Pass `onWarning` to observe spec-compliance issues that do not prevent parsing:
```typescript
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()`.
### PGN object
```typescript
{
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 object
`Notation` extends `SAN` from [`@echecs/san`](https://github.com/echecsjs/san) —
all SAN fields are always present.
```typescript
{
// 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 `undefined` — `whiteMove` when a variation begins on black's turn,
`blackMove` when the game or variation ends on white's move.
### Annotations and comments
```pgn
12. Nf3! $14 { White has a slight advantage }
```
```typescript
{
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'
}
```
### Comment annotations
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.
#### Types
```typescript
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
```
#### Example
```pgn
1. e4 { [%cal Ge2e4,Re4e5] [%clk 0:05:00] } e5
```
```typescript
{
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
}
```
### Variations
```pgn
5... Ba5 (5... Be7 6. d4) 6. Qb3
```
The alternative line appears as a `variants` array on the move where it
branches:
```typescript
{
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', ... }] ]
]
}
```
## Exported types
All public types are exported as named type exports:
```typescript
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';
```
## Contributing
Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for
guidelines on how to submit issues and pull requests.