cm-pgn
Version:
Module for parsing and rendering of PGNs (Portable Game Notation)
237 lines (173 loc) • 7.47 kB
Markdown
# cm-pgn
## Parser for PGNs (Portable Game Notation)
This is as **ES6 Module for parsing and rendering of PGNs** ([Portable Game Notation](https://de.wikipedia.org/wiki/Portable_Game_Notation)).
The API is similar to `history()` of [chess.js](https://github.com/jhlywa/chess.js), but this module **supports variations, nags and comments** in the pgn.
I used the grammar file from [PgnViewerJS](https://github.com/mliebelt/PgnViewerJS) of [mliebelt](https://github.com/mliebelt) to create the parser.
## Install
`npm install cm-pgn`
## Usage
Use the `Pgn` class as JS Module:
```html
<script type="module">
import {Pgn} from "./PATH/TO/cm-pgn/src/Pgn.js"
// parse pgn
const pgn = new Pgn(`[Site "Berlin"]
[Date "1989.07.02"]
[White "Haack, Stefan"]
[Black "Maier, Karsten"]
1. e4 e5 (e6) 2. Nf3 $1 {Great move!} Nc6 *`)
</script>
```
## Pgn constructor
`constructor(pgnString = "", props = {})`
Supported `props`:
- `sloppy` (default `false`) — accept non-standard move notations like `e2e4`
or `e2-e4`. See [.move(move, options)](https://github.com/jhlywa/chess.js/blob/master/README.md#movemove--options-) from chess.js.
- `chess960` (default `false`) — parse Chess960 / Fischer Random games,
including non-standard castling notation. Also auto-enabled when the
header contains `[Variant "Chess960"]`, `"Fischerandom"` or `"Freestyle"`.
If the header contains `[SetUp "1"]` together with `[FEN "…"]`, the game
is replayed from that FEN instead of the standard starting position.
## Rendering a PGN
```js
pgn.render(renderHeader = true, renderComments = true, renderNags = true)
```
Re-serializes `pgn.header` and `pgn.history` back to a PGN string,
word-wrapped at 80 columns. `pgn.header.render()` and
`pgn.history.render(renderComments, renderNags)` are available if you
only need one of the two blocks.
## Header tags
`pgn.header.tags` is a plain object mapping tag names to string values.
A `TAGS` constant with the well-known PGN tag names is exported from
`src/Header.js` for safer lookups:
```js
import {TAGS} from "./PATH/TO/cm-pgn/src/Header.js"
pgn.header.tags[TAGS.White] // "Haack, Stefan"
```
## Data structure
The `pgn` has a `pgn.header` and a `pgn.history`.
### pgn.header
The header holds the PGN header elements in the key value object `tags`.
```js
pgn.header.tags = {
Site: "Berlin",
Date: "1989.07.02",
White: "Haack, Stefan",
Black: "Maier, Karsten"
}
```
### pgn.history
The moves are stored in an array. Every element of that array has the following structure
```js
pgn.history.moves[i] = {
color: "w", // the moving color
fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", // the fen after that move
flags: "b", // the flags, like described below
from: "e2", // the square from
next: {color: "b", from: "e7", to: "e6", flags: "n", piece: "p", /*…*/}, // a pointer to the next move
piece: "p", // the piece type
ply: 1, // the ply number
previous: undefined, // a pointer to the previous move
san: "e4", // the move in SAN notation
to: "e4", // the square to
uci: "e2e4", // the move in UCI notation
variation: (4) [{/*…*/}, {/*…*/}, {/*…*/}, {/*…*/}], // a pointer to the begin of the current variation
variations: [] // all variations starting with that move
}
```
#### pgn.history.moves[i].flags
- 'n' - a non-capture
- 'b' - a pawn push of two squares
- 'e' - an en passant capture
- 'c' - a standard capture
- 'p' - a promotion
- 'k' - kingside castling
- 'q' - queenside castling
#### pgn.history.moves[i].piece
- 'p' - pawn
- 'n' - knight
- 'b' - bishop
- 'r' - rook
- 'q' - queen
- 'k' - king
#### Optional fields on `pgn.history.moves[i]`
- `nag` — the NAG as string, e.g. `"$1"`
- `commentMove`, `commentBefore`, `commentAfter` — PGN `{ ... }` comments
around the move; newlines are preserved
- `gameOver`, `inCheck`, `inCheckmate`, `inDraw`, `inStalemate`,
`insufficientMaterial`, `inThreefoldRepetition` — set to `true` when
the corresponding chess.js predicate holds after the move
#### Examples
```js
const history = pgn.history
assert.equal(4, history.moves.length)
assert.equal(history.moves[0].san, "e4")
assert.equal(history.moves[1].variations.length, 1)
assert.equal(history.moves[1].variations[0][0].san, "e6")
assert.equal(history.moves[2].nag, "$1")
assert.equal(history.moves[2].commentAfter, "Great move!")
assert.equal(history.moves[2].fen, "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2")
assert.equal(history.moves[3].from, "b8")
assert.equal(history.moves[3].to, "c6")
assert.equal(history.moves[3].uci, "b8c6")
assert.equal(history.moves[3].san, "Nc6")
assert.equal(history.moves[3].previous.san, "Nf3")
assert.equal(history.moves[3].previous.next.san, "Nc6")
```
## Building a history programmatically
`pgn.history` also exposes a small mutation API:
```js
// validate without appending — returns a move object or null
pgn.history.validateMove("Nf3")
// append a move to the main line (or to a variation, via `previous`)
const move = pgn.history.addMove("Nf3")
// walk the linked list back to the starting position
pgn.history.historyToMove(move) // => move[]
```
`addMove(notation, previous = null, sloppy = true)` appends to the end of
the main line by default. Passing an existing move as `previous` appends
after that move; if `previous` already has a `next`, the new move is
attached as a variation instead.
To add a move at the very start of the game — either as the first
main-line move or as an alternative to an existing first move (e.g.
`1. e4 (1. d4) 1... e5`) — use `addMoveAtStart`:
```js
history.addMove("e4")
history.addMoveAtStart("d4") // => 1. e4 (1. d4)
// equivalent shorthand through addMove:
history.addMove("d4", "start")
```
The move is validated from the starting position (or `setUpFen`). If the
main line is empty, it simply becomes `moves[0]`; otherwise it is pushed
onto `moves[0].variations`. The returned move can be extended with
further `addMove(..., previousVariationMove)` calls.
## Parsing multi-game PGN databases
Use `PgnList` to parse a string (or file) containing several games:
```js
import {PgnList} from "./PATH/TO/cm-pgn/src/PgnList.js"
// from a string
const pgns = PgnList.parse(multiGamePgnString) // => Pgn[]
pgns[0].history.moves
// or keep the raw game strings
const list = new PgnList(multiGamePgnString)
list.pgns // => string[]
// or load from a URL (browser)
const list2 = new PgnList()
await list2.fetch("./games.pgn")
list2.pgns // => string[]
```
## Notes on comments
Line breaks inside `{ ... }` comments are preserved as-is in
`commentMove` / `commentBefore` / `commentAfter`. Multiple consecutive
comments at the same position (e.g. `{ a } { b }`) are supported and
their texts are joined with a single space.
## Development
This module uses [PEG.js](https://pegjs.org/) for parser generation. The parser (`pgnParser.js`)
in `src/parser/` is generated from the grammar file `src/grammar/pgn.pegjs`.
To recreate the parser after modification of `src/grammar/pgn.pegjs`, run `./generate-parser.sh`
in the repository root.
## Testing
[Run the unit tests](https://shaack.com/projekte/cm-pgn/test)
## External Links
- [Wikipedia Portable_Game_Notation](https://en.wikipedia.org/wiki/Portable_Game_Notation)
- [Portable Game Notation Specification and Implementation Guide](http://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm)