@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 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
- `PGN` is a parser that is part of the **ECHECS** project, designed to interpret
4
- the
5
- [PGN (Portable Game Notation) specification](http://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm).
3
+ [![npm](https://img.shields.io/npm/v/@echecs/pgn)](https://www.npmjs.com/package/@echecs/pgn)
4
+ [![Test](https://github.com/mormubis/pgn/actions/workflows/test.yml/badge.svg)](https://github.com/mormubis/pgn/actions/workflows/test.yml)
5
+ [![Coverage](https://codecov.io/gh/mormubis/pgn/branch/main/graph/badge.svg)](https://codecov.io/gh/mormubis/pgn)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 --save-dev @echecs/pgn
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
- The `parse` function takes a PGN formatted string as input and returns an array
16
- of parsed PGN objects.
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
- ### PGN Object Format
80
+ ### `stream()`
23
81
 
24
- Here’s the structure of the `PGN` object:
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
- #### PGN Object
86
+ ```typescript
87
+ stream(input: AsyncIterable<string>): AsyncGenerator<PGN>
88
+ ```
27
89
 
28
90
  ```typescript
29
- {
30
- "meta": Meta,
31
- "moves": Moves,
32
- "result": "1-0" // possible values: "1-0", "0-1", "1/2-1/2", "?"
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
- #### Meta Object
37
-
38
- The `meta` object contains metadata about the chess game.
100
+ ### PGN object
39
101
 
40
102
  ```typescript
41
103
  {
42
- "Event": "name of the tournament or match event",
43
- "Site": "location of the event",
44
- "Date": "starting date of the game",
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
- #### Moves Array
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
- [moveNumber, Move, Move];
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
- Note: Half moves are included for variations or in cases where the last move was
64
- made by white.
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
- #### Move Object
132
+ ### Annotations and comments
67
133
 
68
- Each move is represented by the following structure:
134
+ ```pgn
135
+ 12. Nf3! $14 { White has a slight advantage }
136
+ ```
69
137
 
70
138
  ```typescript
71
139
  {
72
- "annotations": ["!", "$126"], // optional, annotations for the move
73
- "capture": false, // optional, indicates if any piece was captured
74
- "castling": true, // optional, indicates if the move was castling
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
- ### Example
146
+ ### Variations
87
147
 
88
- Here's a sample usage of the `PGN` parser:
148
+ ```pgn
149
+ 5... Ba5 (5... Be7 6. d4) 6. Qb3
150
+ ```
89
151
 
90
- ```typescript
91
- import { readFileSync } from 'fs';
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
- function readFile(path) {
95
- const filename = require.resolve(path);
96
- return readFileSync(filename, 'utf8');
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
- ## Important Notes
164
+ ## Contributing
136
165
 
137
- - `PGN` is a parser and does not verify the validity of the PGN games. It only
138
- parses the provided content.
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.