fast-pgn-parser 0.0.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/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # fast-pgn-parser
2
+
3
+ Basic but fast PGN (Portable Game Notation) parser for Node.js, backed by C bindings to [libpgn](https://github.com/fwttnnn/libpgn).
4
+
5
+ The core parsing is implemented in C via libpgn and exposed to Node.js through native bindings.
6
+
7
+ ## Status
8
+
9
+ In development. N-API bindings to libpgn are in place: libpgn is built and **statically linked** into the native addon. If the addon fails to build (no git, no C++ toolchain), the module throws at load time.
10
+
11
+ ## API
12
+
13
+ - **`parse(pgnText)`** — Parses PGN text and returns an **array of game objects**. Each game has:
14
+ - **`tags`** — Object of key/value PGN headers (e.g. `Event`, `White`, `Black`, `Result`).
15
+ - **`moves`** — Array of move strings (SAN notation).
16
+ - **`pgntext`** — the raw PGN text for that game only.
17
+
18
+ Example:
19
+
20
+ ```js
21
+ import { parse } from 'fast-pgn-parser';
22
+
23
+ const games = parse(pgnText);
24
+ for (const game of games) {
25
+ console.log(game.tags.Event, game.tags.White, 'vs', game.tags.Black);
26
+ console.log(game.moves); // ['e4', 'e5', 'Nf3', ...]
27
+ console.log(game.pgntext); // is the unparsed PGN for this game
28
+ }
29
+ ```
30
+
31
+ ## Project layout
32
+
33
+ - **`src/`** — Node.js API and N-API addon (`binding.cc`)
34
+ - **`test/`** — Tests (`node --test`)
35
+ - **`vendor/`** — [libpgn](https://github.com/fwttnnn/libpgn) is cloned into `vendor/libpgn` on install (see `vendor/README.md`)
36
+
37
+ ## Scripts
38
+
39
+ - `npm install` — install deps, clone libpgn if needed, build native addon (fails if addon cannot be built)
40
+ - `npm run rebuild` — rebuild the native addon (node-gyp)
41
+ - `npm test` — run tests
42
+ - `npm run test:coverage` — run tests with coverage (c8)
43
+ - `npm run benchmark` — benchmark `parse()` vs [pgn-parser](https://www.npmjs.com/package/pgn-parser) using `scripts/games.pgn` (default 100 runs; `node scripts/benchmark-parse.mjs [path] [N]`)
44
+
45
+ ## Building the native addon
46
+
47
+ - **Node.js** ≥ 18
48
+ - **Git** — to clone libpgn (or add `vendor/libpgn` manually)
49
+ - **C/C++ toolchain** — [node-gyp](https://github.com/nodejs/node-gyp#installation) (e.g. Visual Studio Build Tools on Windows, Xcode CLI on macOS, build-essential on Linux)
50
+
51
+ On Windows, **Visual Studio 18** (version 18 in the install path) is supported via a postinstall script (`scripts/fix-node-gyp-vs18.cjs`) that applies the same approach as projects like bitboard-chess: node-gyp is adjusted so version 18 is treated as 2022 and the v145 toolset is used when the path contains `\18\`. No patch file is used.
52
+
53
+ After `npm install`, the addon is in `build/Release/pgn_parser.node`. If the addon did not build, importing the module throws.
package/binding.gyp ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "targets": [{
3
+ "target_name": "pgn_parser",
4
+ "sources": [
5
+ "src/binding.cc",
6
+ "vendor/libpgn/annotation.c",
7
+ "vendor/libpgn/check.c",
8
+ "vendor/libpgn/comments.c",
9
+ "vendor/libpgn/coordinate.c",
10
+ "vendor/libpgn/metadata.c",
11
+ "vendor/libpgn/moves.c",
12
+ "vendor/libpgn/pgn.c",
13
+ "vendor/libpgn/piece.c",
14
+ "vendor/libpgn/score.c",
15
+ "vendor/libpgn/utils/buffer.c",
16
+ "vendor/libpgn/utils/cursor.c",
17
+ "vendor/libpgn/utils/export.c"
18
+ ],
19
+ "include_dirs": [
20
+ "<!@(node -p \"require('node-addon-api').include\")",
21
+ "vendor/libpgn",
22
+ "vendor/libpgn/utils"
23
+ ],
24
+ "dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
25
+ "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"],
26
+ "cflags!": ["-fno-exceptions"],
27
+ "cflags_cc!": ["-fno-exceptions"],
28
+ "conditions": [
29
+ ["OS=='win'", {
30
+ "defines": ["PGN_STATIC_BUILD"],
31
+ "msvs_settings": {
32
+ "VCCLCompilerTool": {
33
+ "ExceptionHandling": 1
34
+ }
35
+ }
36
+ }],
37
+ ["OS!='win'", {
38
+ "cflags": ["-std=c99"],
39
+ "cflags_cc": ["-std=c++17"]
40
+ }]
41
+ ]
42
+ }]
43
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "fast-pgn-parser",
3
+ "version": "0.0.1",
4
+ "description": "Basic but fast PGN parser",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "scripts": {
8
+ "install": "node scripts/install-libpgn.cjs",
9
+ "postinstall": "node scripts/fix-node-gyp-vs18.cjs",
10
+ "rebuild": "node-gyp rebuild",
11
+ "test": "node --test",
12
+ "test:coverage": "c8 node --test",
13
+ "coverage": "c8 node --test",
14
+ "benchmark": "node scripts/benchmark-parse.mjs"
15
+ },
16
+ "dependencies": {
17
+ "node-addon-api": "^8.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "c8": "^10.1.2",
21
+ "node-gyp": "^10.0.0",
22
+ "pgn-parser": "^2.2.1"
23
+ },
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "gypfile": true
28
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Benchmark: fast-pgn-parser parse() vs pgn-parser parse()
4
+ * Usage: node scripts/benchmark-parse.mjs [path-to.pgn] [N]
5
+ * Defaults: scripts/games.pgn, N=100
6
+ */
7
+ import { readFileSync } from 'fs';
8
+ import { join, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { parse as ourParse, isNative } from '../src/index.js';
11
+ import { createRequire } from 'module';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const pgnParser = require('pgn-parser');
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const rootDir = join(__dirname, '..');
18
+
19
+ const pgnPath = process.argv[2] || join(rootDir, 'scripts', 'games.pgn');
20
+ const N = parseInt(process.argv[3] || '100', 10);
21
+
22
+ let pgnText;
23
+ try {
24
+ pgnText = readFileSync(pgnPath, 'utf8');
25
+ pgnText = pgnText.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
26
+ } catch (err) {
27
+ console.error('Failed to read:', pgnPath, err.message);
28
+ process.exit(1);
29
+ }
30
+
31
+ const bytes = Buffer.byteLength(pgnText, 'utf8');
32
+ const gameCount = ourParse(pgnText).length;
33
+ console.log('Input:', pgnPath);
34
+ console.log('Size:', (bytes / 1024).toFixed(2), 'KB, games:', gameCount);
35
+ console.log('Runs:', N);
36
+ console.log('fast-pgn-parser (native):', isNative());
37
+ console.log('');
38
+
39
+ function timeIt(name, fn) {
40
+ const start = performance.now();
41
+ for (let i = 0; i < N; i++) fn();
42
+ const elapsed = performance.now() - start;
43
+ const perRun = elapsed / N;
44
+ const opsPerSec = (N / elapsed) * 1000;
45
+ console.log(name);
46
+ console.log(' total:', elapsed.toFixed(2), 'ms');
47
+ console.log(' per parse:', perRun.toFixed(4), 'ms');
48
+ console.log(' ops/sec:', opsPerSec.toFixed(1));
49
+ console.log('');
50
+ return elapsed;
51
+ }
52
+
53
+ const ourMs = timeIt('fast-pgn-parser', () => ourParse(pgnText));
54
+ const theirMs = timeIt('pgn-parser', () => pgnParser.parse(pgnText));
55
+
56
+ const ratio = theirMs / ourMs;
57
+ console.log('Ratio (pgn-parser / fast-pgn-parser):', ratio.toFixed(2) + 'x');
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Apply the same VS 18 fix as bitboard-chess (no patch file):
4
+ * - Map version 18 to versionYear 2022 so node-gyp accepts it via [2019, 2022]
5
+ * - In getToolset, use v145 when install path contains \18\, else v143 for 2022
6
+ * Idempotent: safe to run on vanilla or already-patched node-gyp.
7
+ */
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const findVs = path.join(
12
+ __dirname,
13
+ '..',
14
+ 'node_modules',
15
+ 'node-gyp',
16
+ 'lib',
17
+ 'find-visualstudio.js'
18
+ );
19
+
20
+ if (!fs.existsSync(findVs)) {
21
+ process.exit(0);
22
+ }
23
+
24
+ let code = fs.readFileSync(findVs, 'utf8');
25
+
26
+ // 1) getVersionInfo: versionMajor 18 -> versionYear 2022 (so [2019, 2022] accepts it)
27
+ code = code.replace(
28
+ /(if \(ret\.versionMajor === 18\) \{\s*)ret\.versionYear = 2026/,
29
+ '$1ret.versionYear = 2022'
30
+ );
31
+ if (!code.includes('if (ret.versionMajor === 18)')) {
32
+ code = code.replace(
33
+ /(if \(ret\.versionMajor === 17\) \{\s*ret\.versionYear = 2022\s*return ret\s*\})\s*(this\.log\.silly\('- unsupported version:', ret\.versionMajor\))/,
34
+ "$1\n if (ret.versionMajor === 18) {\n ret.versionYear = 2022\n return ret\n }\n $2"
35
+ );
36
+ }
37
+
38
+ // 2) supportedYears: use [2019, 2022] only
39
+ code = code.replace(/\[2019, 2022, 2026\]/g, '[2019, 2022]');
40
+
41
+ // 3) getToolset: for 2022 use v145 when path has \18\, else v143; remove 2026 branch if present
42
+ if (!code.includes("includes('\\\\18\\\\')")) {
43
+ code = code.replace(
44
+ /(versionYear === 2022\) \{\s*)return 'v143'/,
45
+ "$1return (info.path && info.path.includes('\\\\18\\\\')) ? 'v145' : 'v143'"
46
+ );
47
+ code = code.replace(
48
+ /\s*\} else if \(versionYear === 2026\) \{\s*return 'v145'\s*\}/m,
49
+ '\n }'
50
+ );
51
+ }
52
+
53
+ fs.writeFileSync(findVs, code, 'utf8');