@echecs/pgn 3.1.3 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +137 -0
- package/README.md +120 -93
- package/dist/grammar.cjs +2705 -131
- package/dist/grammar.cjs.map +1 -1
- package/dist/grammar.d.cts +11 -13
- package/dist/index.d.ts +14 -5
- package/dist/index.js +90 -28
- package/dist/index.js.map +1 -1
- package/package.json +32 -17
- package/dist/lexer.d.ts +0 -3
- package/dist/lexer.js +0 -94
- package/dist/lexer.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to
|
|
7
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [3.5.1] - 2026-03-14
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- README: document `stream()` API with signature and Node.js usage example
|
|
16
|
+
- README: update type names (`Moves` → `MoveList`) and clarify `Move.from`
|
|
17
|
+
disambiguation and move tuple slot semantics
|
|
18
|
+
- Updated benchmark results for v3.5.0 SAN rule restructure
|
|
19
|
+
|
|
20
|
+
## [3.5.0] - 2026-03-14
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `stream(input: AsyncIterable<string>): AsyncGenerator<PGN>` — new named export
|
|
25
|
+
for incremental, memory-efficient parsing of large PGN databases
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `Move.from` widened from `File | Rank` to `Disambiguation`
|
|
30
|
+
(`Square | File | Rank`) to correctly type fully-disambiguated moves (e.g.
|
|
31
|
+
`Qd1xe4` → `from: "d1"`)
|
|
32
|
+
- `type Moves` renamed to `MoveList`; new
|
|
33
|
+
`MovePair = [number, Move | undefined, Move?]` tuple
|
|
34
|
+
- `type Variation` simplified to `MoveList[]`
|
|
35
|
+
|
|
36
|
+
### Performance
|
|
37
|
+
|
|
38
|
+
- Restructured `SAN` grammar rule to eliminate post-match regex on every move;
|
|
39
|
+
closes remaining ~1.1–1.2x gap vs `pgn-parser` on move-heavy fixtures
|
|
40
|
+
|
|
41
|
+
## [3.4.0] - 2026-02-21
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Rewrote README following `@echecs/elo` library style with badges, Why, Quick
|
|
46
|
+
Start, Usage, and Contributing sections
|
|
47
|
+
- Updated AGENTS.md to reflect Peggy migration and remove stale nearley/moo
|
|
48
|
+
references
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- Features section in README highlighting RAV and NAG support with a parser
|
|
53
|
+
comparison table
|
|
54
|
+
- Performance section in README with benchmark results table
|
|
55
|
+
- Codecov badge to README
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
- `docs.yml` workflow (no hosted docs in this project)
|
|
60
|
+
|
|
61
|
+
## [3.3.0] - 2026-02-21
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- Peggy PEG parser replacing nearley/moo for O(n) linear-time parsing —
|
|
66
|
+
delivering up to 10× throughput improvement on large PGN files
|
|
67
|
+
- Comparative benchmark (`comparison.bench.ts`) measuring `@echecs/pgn` against
|
|
68
|
+
`@mliebelt/pgn-parser` and `chess.js`
|
|
69
|
+
|
|
70
|
+
### Performance
|
|
71
|
+
|
|
72
|
+
- Replaced `pickBy` with direct property assignment in SAN action block,
|
|
73
|
+
reducing allocations per move
|
|
74
|
+
- Added length-check guards before NAG and comment processing in MOVE action,
|
|
75
|
+
skipping unnecessary work for moves without annotations
|
|
76
|
+
- Removed `delete` mutations and reduced allocations in `pairMoves`, avoiding V8
|
|
77
|
+
hidden-class transitions
|
|
78
|
+
|
|
79
|
+
### Removed
|
|
80
|
+
|
|
81
|
+
- Stale `tokenizer.ts` debug script
|
|
82
|
+
|
|
83
|
+
## [3.2.1] - 2026-02-20
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- Sort `multiGameFixtures` keys in comparison benchmark to satisfy the
|
|
88
|
+
`sort-keys` lint rule
|
|
89
|
+
|
|
90
|
+
## [3.2.0] - 2026-02-20
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- Comparative PGN parser benchmark (`comparison.bench.ts`) for cross-library
|
|
95
|
+
performance tracking
|
|
96
|
+
|
|
97
|
+
### Performance
|
|
98
|
+
|
|
99
|
+
- Reduced Earley parser overhead via grammar and caching optimizations
|
|
100
|
+
|
|
101
|
+
## [3.1.3] - 2025-03-27
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- Removed accidental production dependency introduced in 3.1.2
|
|
106
|
+
|
|
107
|
+
## [3.1.2] - 2025-03-01
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
|
|
111
|
+
- Increased per-test timeout to accommodate `long.pgn` (~3 500 games) on slow CI
|
|
112
|
+
runners
|
|
113
|
+
|
|
114
|
+
## [3.1.1] - 2025-03-01
|
|
115
|
+
|
|
116
|
+
### Fixed
|
|
117
|
+
|
|
118
|
+
- Corrected `.js` extension on relative imports in test files (NodeNext
|
|
119
|
+
resolution)
|
|
120
|
+
|
|
121
|
+
## [3.1.0] - 2025-03-01
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
|
|
125
|
+
- moo tokenizer for faster lexing
|
|
126
|
+
- New grammar supporting the full PGN specification including RAV (recursive
|
|
127
|
+
annotated variations) and NAG (numeric annotation glyphs)
|
|
128
|
+
|
|
129
|
+
[unreleased]: https://github.com/mormubis/pgn/compare/v3.4.0...HEAD
|
|
130
|
+
[3.4.0]: https://github.com/mormubis/pgn/compare/v3.3.0...v3.4.0
|
|
131
|
+
[3.3.0]: https://github.com/mormubis/pgn/compare/v3.2.1...v3.3.0
|
|
132
|
+
[3.2.1]: https://github.com/mormubis/pgn/compare/v3.2.0...v3.2.1
|
|
133
|
+
[3.2.0]: https://github.com/mormubis/pgn/compare/v3.1.3...v3.2.0
|
|
134
|
+
[3.1.3]: https://github.com/mormubis/pgn/compare/v3.1.2...v3.1.3
|
|
135
|
+
[3.1.2]: https://github.com/mormubis/pgn/compare/v3.1.1...v3.1.2
|
|
136
|
+
[3.1.1]: https://github.com/mormubis/pgn/compare/v3.1.0...v3.1.1
|
|
137
|
+
[3.1.0]: https://github.com/mormubis/pgn/releases/tag/v3.1.0
|
package/README.md
CHANGED
|
@@ -1,140 +1,167 @@
|
|
|
1
1
|
# PGN
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[
|
|
3
|
+
[](https://www.npmjs.com/package/@echecs/pgn)
|
|
4
|
+
[](https://github.com/mormubis/pgn/actions/workflows/test.yml)
|
|
5
|
+
[](https://codecov.io/gh/mormubis/pgn)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**PGN** is a fast TypeScript parser for
|
|
9
|
+
[Portable Game Notation](http://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm)
|
|
10
|
+
— the standard format for recording chess games.
|
|
11
|
+
|
|
12
|
+
It parses PGN input into structured move objects with decomposed SAN, paired
|
|
13
|
+
white/black moves, and full support for annotations and variations. Zero runtime
|
|
14
|
+
dependencies.
|
|
15
|
+
|
|
16
|
+
## Why this library?
|
|
17
|
+
|
|
18
|
+
Most PGN parsers on npm either give you raw strings with no structure, or fail
|
|
19
|
+
on anything beyond a plain game record. If you're building a chess engine,
|
|
20
|
+
opening book, or game viewer, you need more:
|
|
21
|
+
|
|
22
|
+
- **Decomposed SAN** — every move is parsed into `piece`, `from`, `to`,
|
|
23
|
+
`capture`, `promotion`, `check`, and `checkmate` fields. No regex on your
|
|
24
|
+
side.
|
|
25
|
+
- **Paired move structure** — moves are returned as
|
|
26
|
+
`[moveNumber, whiteMove, blackMove]` tuples, ready to render or process
|
|
27
|
+
without further work.
|
|
28
|
+
- **RAV support** — recursive annotation variations (`(...)` sub-lines) are
|
|
29
|
+
parsed into a `variants` tree on each move. Essential for opening books and
|
|
30
|
+
annotated games.
|
|
31
|
+
- **NAG support** — symbolic (`!`, `?`, `!!`, `??`, `!?`, `?!`) and numeric
|
|
32
|
+
(`$1`–`$255`) annotations are surfaced as an `annotations` array. Essential
|
|
33
|
+
for Lichess and ChessBase exports.
|
|
34
|
+
- **Multi-game files** — parse entire PGN databases in one call, or stream them
|
|
35
|
+
game-by-game with `stream()` for memory-efficient processing of large files.
|
|
36
|
+
Tested on files with 3 500+ games.
|
|
37
|
+
- **Fast** — built on a [Peggy](https://peggyjs.org/) PEG parser. Throughput is
|
|
38
|
+
within 1.1–1.2x of the fastest parsers on npm, which do far less work per move
|
|
39
|
+
(see [BENCHMARK_RESULTS.md](./BENCHMARK_RESULTS.md)).
|
|
40
|
+
|
|
41
|
+
If you only need raw SAN strings and a flat move list, any PGN parser will do.
|
|
42
|
+
If you need structured, engine-ready output with annotations and variations,
|
|
43
|
+
this is the one.
|
|
6
44
|
|
|
7
45
|
## Installation
|
|
8
46
|
|
|
9
47
|
```bash
|
|
10
|
-
npm install
|
|
48
|
+
npm install @echecs/pgn
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import parse from '@echecs/pgn';
|
|
55
|
+
|
|
56
|
+
const games = parse(`
|
|
57
|
+
[Event "Example"]
|
|
58
|
+
[White "Player1"]
|
|
59
|
+
[Black "Player2"]
|
|
60
|
+
[Result "1-0"]
|
|
61
|
+
|
|
62
|
+
1. e4 e5 2. Nf3 Nc6 3. Bb5 1-0
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
console.log(games[0].moves[0]);
|
|
66
|
+
// [1, { piece: 'P', to: 'e4' }, { piece: 'P', to: 'e5' }]
|
|
11
67
|
```
|
|
12
68
|
|
|
13
69
|
## Usage
|
|
14
70
|
|
|
15
|
-
|
|
16
|
-
|
|
71
|
+
### `parse()`
|
|
72
|
+
|
|
73
|
+
Takes a PGN string and returns an array of game objects — one per game in the
|
|
74
|
+
file.
|
|
17
75
|
|
|
18
76
|
```typescript
|
|
19
77
|
parse(input: string): PGN[]
|
|
20
78
|
```
|
|
21
79
|
|
|
22
|
-
###
|
|
80
|
+
### `stream()`
|
|
23
81
|
|
|
24
|
-
|
|
82
|
+
Takes any `AsyncIterable<string>` and yields one `PGN` object per game. Memory
|
|
83
|
+
usage stays proportional to one game at a time, making it suitable for large
|
|
84
|
+
databases read from disk or a network stream.
|
|
25
85
|
|
|
26
|
-
|
|
86
|
+
```typescript
|
|
87
|
+
stream(input: AsyncIterable<string>): AsyncGenerator<PGN>
|
|
88
|
+
```
|
|
27
89
|
|
|
28
90
|
```typescript
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
91
|
+
import { createReadStream } from 'node:fs';
|
|
92
|
+
import { stream } from '@echecs/pgn';
|
|
93
|
+
|
|
94
|
+
const chunks = createReadStream('database.pgn', { encoding: 'utf8' });
|
|
95
|
+
for await (const game of stream(chunks)) {
|
|
96
|
+
console.log(game.meta.White, 'vs', game.meta.Black);
|
|
33
97
|
}
|
|
34
98
|
```
|
|
35
99
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
The `meta` object contains metadata about the chess game.
|
|
100
|
+
### PGN object
|
|
39
101
|
|
|
40
102
|
```typescript
|
|
41
103
|
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"Round": "playing round ordinal of the game",
|
|
46
|
-
"White": "player of the white pieces",
|
|
47
|
-
"Black": "player of the black pieces",
|
|
48
|
-
"Result": "result of the game",
|
|
49
|
-
// Any other additional tags
|
|
50
|
-
[key]: "string"
|
|
104
|
+
meta: Meta, // tag pairs (Event, Site, Date, White, Black, …)
|
|
105
|
+
moves: MoveList, // paired move list
|
|
106
|
+
result: 1 | 0 | 0.5 | '?'
|
|
51
107
|
}
|
|
52
108
|
```
|
|
53
109
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`Moves` is an array representing the sequence of moves in the game. Each element
|
|
57
|
-
is an array containing the move number, the white move, and the black move.
|
|
110
|
+
### Move object
|
|
58
111
|
|
|
59
112
|
```typescript
|
|
60
|
-
|
|
113
|
+
{
|
|
114
|
+
piece: 'P' | 'R' | 'N' | 'B' | 'Q' | 'K', // always present
|
|
115
|
+
to: string, // destination square, e.g. "e4"
|
|
116
|
+
from?: string, // disambiguation: file "e", rank "2", or square "e2"
|
|
117
|
+
capture?: true,
|
|
118
|
+
castling?: true,
|
|
119
|
+
check?: true,
|
|
120
|
+
checkmate?: true,
|
|
121
|
+
promotion?: 'R' | 'N' | 'B' | 'Q',
|
|
122
|
+
annotations?: string[], // e.g. ["!", "$14"]
|
|
123
|
+
comment?: string,
|
|
124
|
+
variants?: MoveList[], // recursive annotation variations
|
|
125
|
+
}
|
|
61
126
|
```
|
|
62
127
|
|
|
63
|
-
|
|
64
|
-
|
|
128
|
+
Moves are grouped into tuples: `[moveNumber, whiteMove, blackMove]`. Both move
|
|
129
|
+
slots can be `undefined` — `whiteMove` when a variation begins on black's turn,
|
|
130
|
+
`blackMove` when the game or variation ends on white's move.
|
|
65
131
|
|
|
66
|
-
|
|
132
|
+
### Annotations and comments
|
|
67
133
|
|
|
68
|
-
|
|
134
|
+
```pgn
|
|
135
|
+
12. Nf3! $14 { White has a slight advantage }
|
|
136
|
+
```
|
|
69
137
|
|
|
70
138
|
```typescript
|
|
71
139
|
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"check": false, // optional, indicates if the move put the rival king in check
|
|
76
|
-
"checkmate": false, // optional, indicates if it is a checkmate
|
|
77
|
-
"comment": "Some comment", // optional, comment about the move
|
|
78
|
-
"from": "e", // optional, disambiguation of the move
|
|
79
|
-
"piece": "K", // required, type of piece (P, R, N, B, Q, K)
|
|
80
|
-
"promotion": "Piece", // optional, promotion piece (R, N, B, Q)
|
|
81
|
-
"to": "g1", // required, ending square of the move
|
|
82
|
-
"variants": [...] // optional, array of moves for variations following Moves format
|
|
140
|
+
piece: 'N', to: 'f3',
|
|
141
|
+
annotations: ['!', '$14'],
|
|
142
|
+
comment: 'White has a slight advantage'
|
|
83
143
|
}
|
|
84
144
|
```
|
|
85
145
|
|
|
86
|
-
###
|
|
146
|
+
### Variations
|
|
87
147
|
|
|
88
|
-
|
|
148
|
+
```pgn
|
|
149
|
+
5... Ba5 (5... Be7 6. d4) 6. Qb3
|
|
150
|
+
```
|
|
89
151
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
import parse from '@echecs/pgn';
|
|
152
|
+
The alternative line appears as a `variants` array on the move where it
|
|
153
|
+
branches:
|
|
93
154
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
155
|
+
```typescript
|
|
156
|
+
{
|
|
157
|
+
piece: 'B', to: 'a5',
|
|
158
|
+
variants: [
|
|
159
|
+
[ [5, undefined, { piece: 'B', to: 'e7' }], [6, { piece: 'P', to: 'd4' }] ]
|
|
160
|
+
]
|
|
97
161
|
}
|
|
98
|
-
|
|
99
|
-
const pgn = parse(readFile('./games/file.pgn'));
|
|
100
|
-
|
|
101
|
-
// Output example of parsed `PGN`
|
|
102
|
-
console.log(pgn);
|
|
103
|
-
/*
|
|
104
|
-
[
|
|
105
|
-
{
|
|
106
|
-
"meta": {
|
|
107
|
-
"Event": "Some Tournament",
|
|
108
|
-
"Site": "Some Location",
|
|
109
|
-
"Date": "2023.10.04",
|
|
110
|
-
"Round": "1",
|
|
111
|
-
"White": "Player1",
|
|
112
|
-
"Black": "Player2",
|
|
113
|
-
"Result": "1-0",
|
|
114
|
-
// additional tags...
|
|
115
|
-
},
|
|
116
|
-
"moves": [
|
|
117
|
-
[
|
|
118
|
-
1,
|
|
119
|
-
{ "piece": "P", "to": "e4" },
|
|
120
|
-
{ "piece": "P", "to": "e5" }
|
|
121
|
-
],
|
|
122
|
-
[
|
|
123
|
-
2,
|
|
124
|
-
{ "piece": "N", "to": "f3" },
|
|
125
|
-
{ "piece": "N", "to": "c6" }
|
|
126
|
-
],
|
|
127
|
-
// more moves...
|
|
128
|
-
],
|
|
129
|
-
"result": "1-0"
|
|
130
|
-
}
|
|
131
|
-
];
|
|
132
|
-
*/
|
|
133
162
|
```
|
|
134
163
|
|
|
135
|
-
##
|
|
164
|
+
## Contributing
|
|
136
165
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
- For game validation, use **@echecs/game** as it is responsible for verifying
|
|
140
|
-
game correctness as part of the **ECHECS** project.
|
|
166
|
+
Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for
|
|
167
|
+
guidelines on how to submit issues and pull requests.
|