pgn-viewer-parser 1.0.0

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,340 @@
1
+ # PGN Viewer Parser
2
+
3
+ A production-ready TypeScript library for parsing and viewing chess PGN (Portable Game Notation) files. Designed for Next.js/React applications with full support for variations, comments, NAGs, and annotations.
4
+
5
+ ## Features
6
+
7
+ ✅ **Complete PGN Parsing** - Supports all official PGN features including variations, comments, NAGs, and annotations
8
+ ✅ **Tree Data Structure** - Represents games as a navigable tree with mainline and variations
9
+ ✅ **Pure TypeScript** - No chess.js dependency for parsing (optional for board state)
10
+ ✅ **React Components** - Optional viewer components with keyboard navigation
11
+ ✅ **Next.js Compatible** - Works with App Router (Server/Client components)
12
+ ✅ **Tree-Shakable** - Import only what you need
13
+ ✅ **Fully Typed** - Complete TypeScript type definitions
14
+ ✅ **Comprehensive Tests** - Extensive test coverage with Vitest
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install pgn-viewer-parser
20
+ ```
21
+
22
+ For React components:
23
+ ```bash
24
+ npm install pgn-viewer-parser react react-dom
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### Basic Parsing
30
+
31
+ ```typescript
32
+ import { parsePGN, GameCursor } from 'pgn-viewer-parser';
33
+
34
+ const pgnText = `
35
+ [Event "Live Chess"]
36
+ [White "Magnus Carlsen"]
37
+ [Black "Hikaru Nakamura"]
38
+
39
+ 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 1-0
40
+ `;
41
+
42
+ // Parse the PGN
43
+ const game = parsePGN(pgnText);
44
+
45
+ // Access headers
46
+ console.log(game.headers.Event); // "Live Chess"
47
+ console.log(game.headers.White); // "Magnus Carlsen"
48
+
49
+ // Navigate through moves
50
+ const cursor = new GameCursor(game.root);
51
+ cursor.next(); // e4
52
+ cursor.next(); // e5
53
+ console.log(cursor.current.san); // "e5"
54
+ ```
55
+
56
+ ### React Viewer
57
+
58
+ ```tsx
59
+ 'use client';
60
+
61
+ import { parsePGN, GameCursor } from 'pgn-viewer-parser';
62
+ import { PGNViewer, PGNControls } from 'pgn-viewer-parser/viewer';
63
+
64
+ export default function ChessGame() {
65
+ const game = parsePGN(pgnText);
66
+ const cursor = new GameCursor(game.root);
67
+
68
+ return (
69
+ <div>
70
+ <PGNViewer root={game.root} cursor={cursor} />
71
+ <PGNControls cursor={cursor} />
72
+ </div>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ## API Reference
78
+
79
+ ### Parsing
80
+
81
+ #### `parsePGN(pgnText: string): PGNGame`
82
+
83
+ Parses PGN text and returns a game object.
84
+
85
+ ```typescript
86
+ const game = parsePGN(pgnText);
87
+ ```
88
+
89
+ ### Data Structures
90
+
91
+ #### `PGNGame`
92
+
93
+ ```typescript
94
+ interface PGNGame {
95
+ headers: Record<string, string>; // PGN headers
96
+ root: MoveNode; // Root of the game tree
97
+ }
98
+ ```
99
+
100
+ #### `MoveNode`
101
+
102
+ ```typescript
103
+ interface MoveNode {
104
+ id: string; // Unique identifier
105
+ ply: number; // Half-move number (0-indexed)
106
+ moveNumber: number; // Full move number
107
+ color: 'w' | 'b'; // Side to move
108
+ san: string; // Move in SAN notation
109
+ nags: number[]; // Numeric Annotation Glyphs
110
+ commentBefore?: string; // Comment before move
111
+ commentAfter?: string; // Comment after move
112
+ clock?: number; // Clock time (seconds)
113
+ emt?: number; // Elapsed move time (seconds)
114
+ eval?: number; // Position evaluation
115
+ depth?: number; // Search depth
116
+ parent?: MoveNode; // Parent node
117
+ next?: MoveNode; // Next move in mainline
118
+ variations: MoveNode[]; // Alternative variations
119
+ }
120
+ ```
121
+
122
+ ### Navigation
123
+
124
+ #### `GameCursor`
125
+
126
+ ```typescript
127
+ class GameCursor {
128
+ current: MoveNode; // Current position
129
+ root: MoveNode; // Game root
130
+
131
+ next(): MoveNode | null; // Move forward
132
+ prev(): MoveNode | null; // Move backward
133
+ goTo(nodeId: string): void; // Jump to node
134
+ enterVariation(index: number): MoveNode | null; // Enter variation
135
+ exitVariation(): MoveNode | null; // Exit variation
136
+ toStart(): void; // Go to start
137
+ toEnd(): void; // Go to end
138
+ isAtStart(): boolean; // Check if at start
139
+ isAtEnd(): boolean; // Check if at end
140
+ getMainlinePath(): MoveNode[]; // Get path to current
141
+ }
142
+ ```
143
+
144
+ ### React Components
145
+
146
+ #### `<PGNViewer />`
147
+
148
+ Displays moves with variations and comments.
149
+
150
+ ```tsx
151
+ <PGNViewer
152
+ root={game.root}
153
+ cursor={cursor}
154
+ onMoveClick={(node) => console.log(node.san)}
155
+ className="custom-class"
156
+ />
157
+ ```
158
+
159
+ **Props:**
160
+ - `root: MoveNode` - Root of the game tree
161
+ - `cursor?: GameCursor` - Optional cursor for external control
162
+ - `onMoveClick?: (node: MoveNode) => void` - Callback when move is clicked
163
+ - `className?: string` - Custom CSS class
164
+
165
+ #### `<PGNControls />`
166
+
167
+ Navigation controls with keyboard support.
168
+
169
+ ```tsx
170
+ <PGNControls
171
+ cursor={cursor}
172
+ onPositionChange={(cursor) => forceUpdate()}
173
+ enableKeyboard={true}
174
+ />
175
+ ```
176
+
177
+ **Props:**
178
+ - `cursor: GameCursor` - The cursor to control
179
+ - `onPositionChange?: (cursor: GameCursor) => void` - Callback on position change
180
+ - `enableKeyboard?: boolean` - Enable keyboard controls (default: true)
181
+ - `className?: string` - Custom CSS class
182
+
183
+ **Keyboard Shortcuts:**
184
+ - `←` Previous move
185
+ - `→` Next move
186
+ - `↑` Exit variation
187
+ - `↓` Enter first variation
188
+ - `Ctrl+←` First move
189
+ - `Ctrl+→` Last move
190
+
191
+ ## Advanced Features
192
+
193
+ ### Parsing Variations
194
+
195
+ ```typescript
196
+ const pgn = `
197
+ 1. e4 e5 (1... c5 2. Nf3) 2. Nf3
198
+ `;
199
+
200
+ const game = parsePGN(pgn);
201
+ const e4 = game.root.next;
202
+ const e5 = e4.next;
203
+
204
+ // Access variation
205
+ const c5 = e5.variations[0];
206
+ console.log(c5.san); // "c5"
207
+ ```
208
+
209
+ ### Parsing Annotations
210
+
211
+ ```typescript
212
+ const pgn = `
213
+ 1. e4 e5 {[%clk 0:04:32][%eval +0.35][%depth 18]}
214
+ `;
215
+
216
+ const game = parsePGN(pgn);
217
+ const e5 = game.root.next.next;
218
+
219
+ console.log(e5.clock); // 272 (seconds)
220
+ console.log(e5.eval); // 0.35
221
+ console.log(e5.depth); // 18
222
+ ```
223
+
224
+ ### NAG Symbols
225
+
226
+ ```typescript
227
+ import { nagToSymbol } from 'pgn-viewer-parser';
228
+
229
+ console.log(nagToSymbol(1)); // "!" (good move)
230
+ console.log(nagToSymbol(2)); // "?" (poor move)
231
+ console.log(nagToSymbol(3)); // "!!" (brilliant move)
232
+ ```
233
+
234
+ ## Next.js Integration
235
+
236
+ ### App Router (Recommended)
237
+
238
+ ```tsx
239
+ // app/game/page.tsx
240
+ 'use client';
241
+
242
+ import { parsePGN, GameCursor } from 'pgn-viewer-parser';
243
+ import { PGNViewer, PGNControls } from 'pgn-viewer-parser/viewer';
244
+
245
+ export default function GamePage() {
246
+ // Your component code
247
+ }
248
+ ```
249
+
250
+ ### Server Component
251
+
252
+ ```tsx
253
+ // app/game/page.tsx
254
+ import { parsePGN } from 'pgn-viewer-parser';
255
+
256
+ export default function GamePage() {
257
+ const game = parsePGN(pgnText);
258
+
259
+ return (
260
+ <div>
261
+ <h1>{game.headers.Event}</h1>
262
+ {/* Render game info */}
263
+ </div>
264
+ );
265
+ }
266
+ ```
267
+
268
+ ## Comparison with chess.js
269
+
270
+ | Feature | pgn-viewer-parser | chess.js |
271
+ |---------|------------------|----------|
272
+ | PGN Parsing | ✅ Full support | ⚠️ Mainline only |
273
+ | Variations | ✅ Tree structure | ❌ Not supported |
274
+ | Comments | ✅ Before/after | ⚠️ Limited |
275
+ | NAGs | ✅ Full support | ❌ Not supported |
276
+ | Annotations | ✅ Clock/eval/depth | ❌ Not supported |
277
+ | Board State | ⚠️ Optional | ✅ Built-in |
278
+ | Move Validation | ⚠️ Optional | ✅ Built-in |
279
+ | React Components | ✅ Included | ❌ Not included |
280
+
281
+ **Use pgn-viewer-parser when:**
282
+ - You need to parse PGN with variations
283
+ - You want to display games with a tree structure
284
+ - You need annotations and comments
285
+ - You're building a PGN viewer/analyzer
286
+
287
+ **Use chess.js when:**
288
+ - You need move validation
289
+ - You're building a playable chess board
290
+ - You don't need variation support
291
+
292
+ **Use both together:**
293
+ ```typescript
294
+ import { parsePGN } from 'pgn-viewer-parser';
295
+ import { Chess } from 'chess.js';
296
+
297
+ const game = parsePGN(pgnText);
298
+ const chess = new Chess();
299
+
300
+ // Apply moves to chess.js for board state
301
+ let node = game.root.next;
302
+ while (node) {
303
+ chess.move(node.san);
304
+ node = node.next;
305
+ }
306
+ ```
307
+
308
+ ## Examples
309
+
310
+ See the `examples/` directory for complete examples:
311
+ - `basic-parsing.ts` - Basic parsing and navigation
312
+ - `react-viewer.tsx` - React component usage
313
+
314
+ ## Development
315
+
316
+ ```bash
317
+ # Install dependencies
318
+ npm install
319
+
320
+ # Run tests
321
+ npm test
322
+
323
+ # Build library
324
+ npm run build
325
+
326
+ # Type check
327
+ npm run type-check
328
+ ```
329
+
330
+ ## License
331
+
332
+ MIT
333
+
334
+ ## Contributing
335
+
336
+ Contributions are welcome! Please open an issue or PR.
337
+
338
+ ## Support
339
+
340
+ For issues and questions, please open an issue on GitHub.
@@ -0,0 +1,110 @@
1
+ class u {
2
+ constructor(t) {
3
+ this._root = t, this._current = t;
4
+ }
5
+ /**
6
+ * Gets the current node.
7
+ */
8
+ get current() {
9
+ return this._current;
10
+ }
11
+ /**
12
+ * Gets the root node.
13
+ */
14
+ get root() {
15
+ return this._root;
16
+ }
17
+ /**
18
+ * Moves to the next node in the current line.
19
+ * Returns the new current node, or null if at the end.
20
+ */
21
+ next() {
22
+ return this._current.next ? (this._current = this._current.next, this._current) : null;
23
+ }
24
+ /**
25
+ * Moves to the previous node.
26
+ * Returns the new current node, or null if at the start.
27
+ */
28
+ prev() {
29
+ return this._current.parent ? (this._current = this._current.parent, this._current) : null;
30
+ }
31
+ /**
32
+ * Jumps to a specific node by ID.
33
+ */
34
+ goTo(t) {
35
+ const r = this.findNodeById(this._root, t);
36
+ if (r)
37
+ this._current = r;
38
+ else
39
+ throw new Error(`Node with ID ${t} not found`);
40
+ }
41
+ /**
42
+ * Enters a variation at the given index.
43
+ * Returns the first node of the variation, or null if invalid index.
44
+ */
45
+ enterVariation(t) {
46
+ return t >= 0 && t < this._current.variations.length ? (this._current = this._current.variations[t], this._current) : null;
47
+ }
48
+ /**
49
+ * Exits the current variation and returns to the parent line.
50
+ * Returns the parent node, or null if already in the mainline.
51
+ */
52
+ exitVariation() {
53
+ return this._current.parent ? (this._current = this._current.parent, this._current) : null;
54
+ }
55
+ /**
56
+ * Goes to the start of the game (root node).
57
+ */
58
+ toStart() {
59
+ this._current = this._root;
60
+ }
61
+ /**
62
+ * Goes to the end of the current line.
63
+ */
64
+ toEnd() {
65
+ for (; this._current.next; )
66
+ this._current = this._current.next;
67
+ }
68
+ /**
69
+ * Checks if the cursor is at the start.
70
+ */
71
+ isAtStart() {
72
+ return this._current === this._root;
73
+ }
74
+ /**
75
+ * Checks if the cursor is at the end of the current line.
76
+ */
77
+ isAtEnd() {
78
+ return this._current.next === void 0;
79
+ }
80
+ /**
81
+ * Gets the mainline path from root to current position.
82
+ */
83
+ getMainlinePath() {
84
+ const t = [];
85
+ let r = this._current;
86
+ for (; r; )
87
+ t.unshift(r), r = r.parent;
88
+ return t;
89
+ }
90
+ /**
91
+ * Recursively finds a node by ID.
92
+ */
93
+ findNodeById(t, r) {
94
+ if (t.id === r)
95
+ return t;
96
+ if (t.next) {
97
+ const n = this.findNodeById(t.next, r);
98
+ if (n) return n;
99
+ }
100
+ for (const n of t.variations) {
101
+ const e = this.findNodeById(n, r);
102
+ if (e) return e;
103
+ }
104
+ return null;
105
+ }
106
+ }
107
+ export {
108
+ u as GameCursor
109
+ };
110
+ //# sourceMappingURL=game-cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-cursor.js","sources":["../../src/cursor/game-cursor.ts"],"sourcesContent":["import { MoveNode } from '../model/move-node.js';\n\n/**\n * Cursor for navigating through a chess game tree.\n */\nexport class GameCursor {\n private _current: MoveNode;\n private _root: MoveNode;\n\n constructor(root: MoveNode) {\n this._root = root;\n this._current = root;\n }\n\n /**\n * Gets the current node.\n */\n get current(): MoveNode {\n return this._current;\n }\n\n /**\n * Gets the root node.\n */\n get root(): MoveNode {\n return this._root;\n }\n\n /**\n * Moves to the next node in the current line.\n * Returns the new current node, or null if at the end.\n */\n next(): MoveNode | null {\n if (this._current.next) {\n this._current = this._current.next;\n return this._current;\n }\n return null;\n }\n\n /**\n * Moves to the previous node.\n * Returns the new current node, or null if at the start.\n */\n prev(): MoveNode | null {\n if (this._current.parent) {\n this._current = this._current.parent;\n return this._current;\n }\n return null;\n }\n\n /**\n * Jumps to a specific node by ID.\n */\n goTo(nodeId: string): void {\n const node = this.findNodeById(this._root, nodeId);\n if (node) {\n this._current = node;\n } else {\n throw new Error(`Node with ID ${nodeId} not found`);\n }\n }\n\n /**\n * Enters a variation at the given index.\n * Returns the first node of the variation, or null if invalid index.\n */\n enterVariation(index: number): MoveNode | null {\n if (index >= 0 && index < this._current.variations.length) {\n this._current = this._current.variations[index];\n return this._current;\n }\n return null;\n }\n\n /**\n * Exits the current variation and returns to the parent line.\n * Returns the parent node, or null if already in the mainline.\n */\n exitVariation(): MoveNode | null {\n if (this._current.parent) {\n this._current = this._current.parent;\n return this._current;\n }\n return null;\n }\n\n /**\n * Goes to the start of the game (root node).\n */\n toStart(): void {\n this._current = this._root;\n }\n\n /**\n * Goes to the end of the current line.\n */\n toEnd(): void {\n while (this._current.next) {\n this._current = this._current.next;\n }\n }\n\n /**\n * Checks if the cursor is at the start.\n */\n isAtStart(): boolean {\n return this._current === this._root;\n }\n\n /**\n * Checks if the cursor is at the end of the current line.\n */\n isAtEnd(): boolean {\n return this._current.next === undefined;\n }\n\n /**\n * Gets the mainline path from root to current position.\n */\n getMainlinePath(): MoveNode[] {\n const path: MoveNode[] = [];\n let node: MoveNode | undefined = this._current;\n\n while (node) {\n path.unshift(node);\n node = node.parent;\n }\n\n return path;\n }\n\n /**\n * Recursively finds a node by ID.\n */\n private findNodeById(node: MoveNode, id: string): MoveNode | null {\n if (node.id === id) {\n return node;\n }\n\n // Search in mainline\n if (node.next) {\n const found = this.findNodeById(node.next, id);\n if (found) return found;\n }\n\n // Search in variations\n for (const variation of node.variations) {\n const found = this.findNodeById(variation, id);\n if (found) return found;\n }\n\n return null;\n }\n}\n"],"names":["GameCursor","root","nodeId","node","index","path","id","found","variation"],"mappings":"AAKO,MAAMA,EAAW;AAAA,EAIpB,YAAYC,GAAgB;AACxB,SAAK,QAAQA,GACb,KAAK,WAAWA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,UAAoB;AACpB,WAAO,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,OAAiB;AACjB,WAAO,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAwB;AACpB,WAAI,KAAK,SAAS,QACd,KAAK,WAAW,KAAK,SAAS,MACvB,KAAK,YAET;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAwB;AACpB,WAAI,KAAK,SAAS,UACd,KAAK,WAAW,KAAK,SAAS,QACvB,KAAK,YAET;AAAA,EACX;AAAA;AAAA;AAAA;AAAA,EAKA,KAAKC,GAAsB;AACvB,UAAMC,IAAO,KAAK,aAAa,KAAK,OAAOD,CAAM;AACjD,QAAIC;AACA,WAAK,WAAWA;AAAA;AAEhB,YAAM,IAAI,MAAM,gBAAgBD,CAAM,YAAY;AAAA,EAE1D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAeE,GAAgC;AAC3C,WAAIA,KAAS,KAAKA,IAAQ,KAAK,SAAS,WAAW,UAC/C,KAAK,WAAW,KAAK,SAAS,WAAWA,CAAK,GACvC,KAAK,YAET;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAiC;AAC7B,WAAI,KAAK,SAAS,UACd,KAAK,WAAW,KAAK,SAAS,QACvB,KAAK,YAET;AAAA,EACX;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACZ,SAAK,WAAW,KAAK;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACV,WAAO,KAAK,SAAS;AACjB,WAAK,WAAW,KAAK,SAAS;AAAA,EAEtC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAqB;AACjB,WAAO,KAAK,aAAa,KAAK;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,UAAmB;AACf,WAAO,KAAK,SAAS,SAAS;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,kBAA8B;AAC1B,UAAMC,IAAmB,CAAA;AACzB,QAAIF,IAA6B,KAAK;AAEtC,WAAOA;AACH,MAAAE,EAAK,QAAQF,CAAI,GACjBA,IAAOA,EAAK;AAGhB,WAAOE;AAAA,EACX;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAaF,GAAgBG,GAA6B;AAC9D,QAAIH,EAAK,OAAOG;AACZ,aAAOH;AAIX,QAAIA,EAAK,MAAM;AACX,YAAMI,IAAQ,KAAK,aAAaJ,EAAK,MAAMG,CAAE;AAC7C,UAAIC,EAAO,QAAOA;AAAA,IACtB;AAGA,eAAWC,KAAaL,EAAK,YAAY;AACrC,YAAMI,IAAQ,KAAK,aAAaC,GAAWF,CAAE;AAC7C,UAAIC,EAAO,QAAOA;AAAA,IACtB;AAEA,WAAO;AAAA,EACX;AACJ;"}
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ import { createMoveNode as r, createRootNode as a } from "./model/move-node.js";
2
+ import { createPGNGame as p } from "./model/pgn-game.js";
3
+ import { PGNParser as n, parsePGN as s } from "./parser/pgn-parser.js";
4
+ import { PGNTokenizer as x, TokenType as G } from "./parser/tokenizer.js";
5
+ import { parseHeader as T, parseHeaders as i } from "./parser/header-parser.js";
6
+ import { GameCursor as d } from "./cursor/game-cursor.js";
7
+ import { NAG_SYMBOLS as S, nagToSymbol as c, nagsToSymbols as l } from "./utils/nag-symbols.js";
8
+ import { parseAnnotations as b, parseEvalAnnotation as g, parseTimeAnnotation as k } from "./utils/annotation-parser.js";
9
+ export {
10
+ d as GameCursor,
11
+ S as NAG_SYMBOLS,
12
+ n as PGNParser,
13
+ x as PGNTokenizer,
14
+ G as TokenType,
15
+ r as createMoveNode,
16
+ p as createPGNGame,
17
+ a as createRootNode,
18
+ c as nagToSymbol,
19
+ l as nagsToSymbols,
20
+ b as parseAnnotations,
21
+ g as parseEvalAnnotation,
22
+ T as parseHeader,
23
+ i as parseHeaders,
24
+ s as parsePGN,
25
+ k as parseTimeAnnotation
26
+ };
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,23 @@
1
+ function e(o) {
2
+ return {
3
+ ...o,
4
+ nags: o.nags || [],
5
+ variations: []
6
+ };
7
+ }
8
+ function n() {
9
+ return {
10
+ id: "root",
11
+ ply: 0,
12
+ moveNumber: 0,
13
+ color: "w",
14
+ san: "",
15
+ nags: [],
16
+ variations: []
17
+ };
18
+ }
19
+ export {
20
+ e as createMoveNode,
21
+ n as createRootNode
22
+ };
23
+ //# sourceMappingURL=move-node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"move-node.js","sources":["../../src/model/move-node.ts"],"sourcesContent":["/**\n * Represents a single move node in the game tree.\n * Each node can have a mainline continuation (next) and alternative variations.\n */\nexport interface MoveNode {\n /** Unique identifier for this node */\n id: string;\n\n /** Ply number (half-moves from start, 0-indexed) */\n ply: number;\n\n /** Move number in chess notation (1, 2, 3, etc.) */\n moveNumber: number;\n\n /** Color to move: 'w' for white, 'b' for black */\n color: 'w' | 'b';\n\n /** Standard Algebraic Notation of the move (e.g., \"e4\", \"Nf3\", \"O-O\") */\n san: string;\n\n /** Numeric Annotation Glyphs (e.g., $1 = \"!\", $2 = \"?\") */\n nags: number[];\n\n /** Comment appearing before this move */\n commentBefore?: string;\n\n /** Comment appearing after this move */\n commentAfter?: string;\n\n /** Clock time remaining in seconds */\n clock?: number;\n\n /** Elapsed move time in seconds */\n emt?: number;\n\n /** Position evaluation in centipawns (positive = white advantage) */\n eval?: number;\n\n /** Search depth for the evaluation */\n depth?: number;\n\n /** Parent node (undefined for root) */\n parent?: MoveNode;\n\n /** Next move in the mainline */\n next?: MoveNode;\n\n /** Alternative variations from this position */\n variations: MoveNode[];\n}\n\n/**\n * Creates a new MoveNode with the given properties.\n */\nexport function createMoveNode(props: {\n id: string;\n ply: number;\n moveNumber: number;\n color: 'w' | 'b';\n san: string;\n nags?: number[];\n commentBefore?: string;\n commentAfter?: string;\n clock?: number;\n emt?: number;\n eval?: number;\n depth?: number;\n parent?: MoveNode;\n}): MoveNode {\n return {\n ...props,\n nags: props.nags || [],\n variations: [],\n };\n}\n\n/**\n * Creates a root node (starting position with no move).\n */\nexport function createRootNode(): MoveNode {\n return {\n id: 'root',\n ply: 0,\n moveNumber: 0,\n color: 'w',\n san: '',\n nags: [],\n variations: [],\n };\n}\n"],"names":["createMoveNode","props","createRootNode"],"mappings":"AAsDO,SAASA,EAAeC,GAclB;AACT,SAAO;AAAA,IACH,GAAGA;AAAA,IACH,MAAMA,EAAM,QAAQ,CAAA;AAAA,IACpB,YAAY,CAAA;AAAA,EAAC;AAErB;AAKO,SAASC,IAA2B;AACvC,SAAO;AAAA,IACH,IAAI;AAAA,IACJ,KAAK;AAAA,IACL,YAAY;AAAA,IACZ,OAAO;AAAA,IACP,KAAK;AAAA,IACL,MAAM,CAAA;AAAA,IACN,YAAY,CAAA;AAAA,EAAC;AAErB;"}
@@ -0,0 +1,18 @@
1
+ function e(o = {}, r) {
2
+ return {
3
+ headers: o,
4
+ root: r || {
5
+ id: "root",
6
+ ply: 0,
7
+ moveNumber: 0,
8
+ color: "w",
9
+ san: "",
10
+ nags: [],
11
+ variations: []
12
+ }
13
+ };
14
+ }
15
+ export {
16
+ e as createPGNGame
17
+ };
18
+ //# sourceMappingURL=pgn-game.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pgn-game.js","sources":["../../src/model/pgn-game.ts"],"sourcesContent":["import { MoveNode } from './move-node.js';\n\n/**\n * Represents a complete chess game parsed from PGN.\n */\nexport interface PGNGame {\n /** PGN headers (Event, Site, Date, White, Black, Result, etc.) */\n headers: Record<string, string>;\n\n /** Root node of the game tree (starting position) */\n root: MoveNode;\n}\n\n/**\n * Creates a new PGNGame with the given headers and root node.\n */\nexport function createPGNGame(\n headers: Record<string, string> = {},\n root?: MoveNode\n): PGNGame {\n return {\n headers,\n root: root || {\n id: 'root',\n ply: 0,\n moveNumber: 0,\n color: 'w',\n san: '',\n nags: [],\n variations: [],\n },\n };\n}\n"],"names":["createPGNGame","headers","root"],"mappings":"AAgBO,SAASA,EACZC,IAAkC,CAAA,GAClCC,GACO;AACP,SAAO;AAAA,IACH,SAAAD;AAAA,IACA,MAAMC,KAAQ;AAAA,MACV,IAAI;AAAA,MACJ,KAAK;AAAA,MACL,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,KAAK;AAAA,MACL,MAAM,CAAA;AAAA,MACN,YAAY,CAAA;AAAA,IAAC;AAAA,EACjB;AAER;"}
@@ -0,0 +1,24 @@
1
+ function s(n) {
2
+ const e = {};
3
+ for (const r of n) {
4
+ const t = r.match(/\[(\w+)\s+"(.*)"\]/);
5
+ if (t) {
6
+ const [, c, o] = t;
7
+ e[c] = o;
8
+ }
9
+ }
10
+ return e;
11
+ }
12
+ function a(n) {
13
+ const e = n.match(/\[(\w+)\s+"(.*)"\]/);
14
+ if (e) {
15
+ const [, r, t] = e;
16
+ return { key: r, value: t };
17
+ }
18
+ return null;
19
+ }
20
+ export {
21
+ a as parseHeader,
22
+ s as parseHeaders
23
+ };
24
+ //# sourceMappingURL=header-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"header-parser.js","sources":["../../src/parser/header-parser.ts"],"sourcesContent":["/**\n * Parses PGN headers from header tokens.\n * Format: [Key \"Value\"]\n */\nexport function parseHeaders(headerTokens: string[]): Record<string, string> {\n const headers: Record<string, string> = {};\n\n for (const token of headerTokens) {\n const match = token.match(/\\[(\\w+)\\s+\"(.*)\"\\]/);\n if (match) {\n const [, key, value] = match;\n headers[key] = value;\n }\n }\n\n return headers;\n}\n\n/**\n * Parses a single header string.\n */\nexport function parseHeader(header: string): { key: string; value: string } | null {\n const match = header.match(/\\[(\\w+)\\s+\"(.*)\"\\]/);\n if (match) {\n const [, key, value] = match;\n return { key, value };\n }\n return null;\n}\n"],"names":["parseHeaders","headerTokens","headers","token","match","key","value","parseHeader","header"],"mappings":"AAIO,SAASA,EAAaC,GAAgD;AACzE,QAAMC,IAAkC,CAAA;AAExC,aAAWC,KAASF,GAAc;AAC9B,UAAMG,IAAQD,EAAM,MAAM,oBAAoB;AAC9C,QAAIC,GAAO;AACP,YAAM,CAAA,EAAGC,GAAKC,CAAK,IAAIF;AACvB,MAAAF,EAAQG,CAAG,IAAIC;AAAA,IACnB;AAAA,EACJ;AAEA,SAAOJ;AACX;AAKO,SAASK,EAAYC,GAAuD;AAC/E,QAAMJ,IAAQI,EAAO,MAAM,oBAAoB;AAC/C,MAAIJ,GAAO;AACP,UAAM,CAAA,EAAGC,GAAKC,CAAK,IAAIF;AACvB,WAAO,EAAE,KAAAC,GAAK,OAAAC,EAAA;AAAA,EAClB;AACA,SAAO;AACX;"}
@@ -0,0 +1,118 @@
1
+ import { PGNTokenizer as E, TokenType as e } from "./tokenizer.js";
2
+ import { parseHeader as k } from "./header-parser.js";
3
+ import { parseAnnotations as T } from "../utils/annotation-parser.js";
4
+ import { createRootNode as y } from "../model/move-node.js";
5
+ import { createPGNGame as A } from "../model/pgn-game.js";
6
+ class M {
7
+ constructor() {
8
+ this.tokens = [], this.position = 0, this.nodeIdCounter = 0;
9
+ }
10
+ /**
11
+ * Parses PGN text and returns a PGNGame object.
12
+ */
13
+ parse(o) {
14
+ const s = new E(o);
15
+ this.tokens = s.tokenize(), this.position = 0, this.nodeIdCounter = 0;
16
+ const t = this.parseHeaders(), r = y();
17
+ return this.parseMoveSequence(r, 0), A(t, r);
18
+ }
19
+ /**
20
+ * Parses all headers at the beginning of the PGN.
21
+ */
22
+ parseHeaders() {
23
+ const o = {};
24
+ for (; this.current().type === e.HEADER; ) {
25
+ const s = this.consume(e.HEADER), t = k(s.value);
26
+ t && (o[t.key] = t.value);
27
+ }
28
+ return o;
29
+ }
30
+ /**
31
+ * Parses a sequence of moves and variations, building the tree structure.
32
+ * Returns the FIRST node created in this sequence (for variations).
33
+ */
34
+ parseMoveSequence(o, s) {
35
+ let t = o, r = s, c, h;
36
+ for (; !this.isAtEnd(); ) {
37
+ const a = this.current();
38
+ if (a.type === e.VARIATION_END)
39
+ return h;
40
+ if (a.type === e.COMMENT) {
41
+ const i = this.consume(e.COMMENT).value;
42
+ c = c ? `${c} ${i}` : i;
43
+ continue;
44
+ }
45
+ if (a.type === e.MOVE_NUMBER) {
46
+ this.advance();
47
+ continue;
48
+ }
49
+ if (a.type === e.RESULT) {
50
+ this.advance();
51
+ break;
52
+ }
53
+ if (a.type === e.VARIATION_START) {
54
+ this.consume(e.VARIATION_START);
55
+ const i = t.parent || o, d = t.ply, u = this.parseMoveSequence(i, d);
56
+ u && u !== i && t.variations.push(u), this.consume(e.VARIATION_END);
57
+ continue;
58
+ }
59
+ if (a.type === e.MOVE) {
60
+ const i = this.consume(e.MOVE), d = r % 2 === 0 ? "w" : "b", u = Math.floor(r / 2) + 1, f = [];
61
+ for (; this.current().type === e.NAG; ) {
62
+ const l = this.consume(e.NAG), n = parseInt(l.value.slice(1));
63
+ f.push(n);
64
+ }
65
+ let N;
66
+ const p = {};
67
+ if (this.current().type === e.COMMENT) {
68
+ const l = this.consume(e.COMMENT), n = T(l.value);
69
+ n.clock !== void 0 && (p.clock = n.clock), n.emt !== void 0 && (p.emt = n.emt), n.eval !== void 0 && (p.eval = n.eval), n.depth !== void 0 && (p.depth = n.depth), n.cleanComment && (N = n.cleanComment);
70
+ }
71
+ const m = {
72
+ id: this.generateNodeId(),
73
+ ply: r,
74
+ moveNumber: u,
75
+ color: d,
76
+ san: i.value,
77
+ nags: f,
78
+ commentBefore: c,
79
+ commentAfter: N,
80
+ ...p,
81
+ parent: t,
82
+ variations: []
83
+ };
84
+ t.next === void 0 && (t.next = m), h || (h = m), t = m, r++, c = void 0;
85
+ } else
86
+ this.advance();
87
+ }
88
+ return h;
89
+ }
90
+ current() {
91
+ return this.tokens[this.position] || { type: e.EOF, value: "", line: 0, column: 0 };
92
+ }
93
+ advance() {
94
+ return this.tokens[this.position++];
95
+ }
96
+ consume(o) {
97
+ const s = this.current();
98
+ if (s.type !== o)
99
+ throw new Error(
100
+ `Expected token type ${o}, got ${s.type} at line ${s.line}`
101
+ );
102
+ return this.advance();
103
+ }
104
+ isAtEnd() {
105
+ return this.current().type === e.EOF;
106
+ }
107
+ generateNodeId() {
108
+ return `node_${this.nodeIdCounter++}`;
109
+ }
110
+ }
111
+ function V(v) {
112
+ return new M().parse(v);
113
+ }
114
+ export {
115
+ M as PGNParser,
116
+ V as parsePGN
117
+ };
118
+ //# sourceMappingURL=pgn-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pgn-parser.js","sources":["../../src/parser/pgn-parser.ts"],"sourcesContent":["import { Token, TokenType, PGNTokenizer } from './tokenizer.js';\nimport { parseHeader } from './header-parser.js';\nimport { parseAnnotations } from '../utils/annotation-parser.js';\nimport { MoveNode, createRootNode } from '../model/move-node.js';\nimport { PGNGame, createPGNGame } from '../model/pgn-game.js';\n\n/**\n * Main PGN parser that converts PGN text into a game tree structure.\n */\nexport class PGNParser {\n private tokens: Token[] = [];\n private position: number = 0;\n private nodeIdCounter: number = 0;\n\n /**\n * Parses PGN text and returns a PGNGame object.\n */\n parse(pgnText: string): PGNGame {\n const tokenizer = new PGNTokenizer(pgnText);\n this.tokens = tokenizer.tokenize();\n this.position = 0;\n this.nodeIdCounter = 0;\n\n // Parse headers\n const headers = this.parseHeaders();\n\n // Create root node\n const root = createRootNode();\n\n // Parse moves starting from root\n this.parseMoveSequence(root, 0);\n\n return createPGNGame(headers, root);\n }\n\n /**\n * Parses all headers at the beginning of the PGN.\n */\n private parseHeaders(): Record<string, string> {\n const headers: Record<string, string> = {};\n\n while (this.current().type === TokenType.HEADER) {\n const headerToken = this.consume(TokenType.HEADER);\n const parsed = parseHeader(headerToken.value);\n if (parsed) {\n headers[parsed.key] = parsed.value;\n }\n }\n\n return headers;\n }\n\n /**\n * Parses a sequence of moves and variations, building the tree structure.\n * Returns the FIRST node created in this sequence (for variations).\n */\n private parseMoveSequence(\n parentNode: MoveNode,\n startPly: number\n ): MoveNode | undefined {\n let currentNode = parentNode;\n let ply = startPly;\n let pendingComment: string | undefined;\n let firstNode: MoveNode | undefined; // Track the first node we create\n\n while (!this.isAtEnd()) {\n const token = this.current();\n\n // End of variation\n if (token.type === TokenType.VARIATION_END) {\n return firstNode; // Return first node, not current\n }\n\n // Comment before move\n if (token.type === TokenType.COMMENT) {\n const comment = this.consume(TokenType.COMMENT).value;\n pendingComment = pendingComment\n ? `${pendingComment} ${comment}`\n : comment;\n continue;\n }\n\n // Skip move numbers\n if (token.type === TokenType.MOVE_NUMBER) {\n this.advance();\n continue;\n }\n\n // Result marker (end of game)\n if (token.type === TokenType.RESULT) {\n this.advance();\n break;\n }\n\n // Variation start\n if (token.type === TokenType.VARIATION_START) {\n this.consume(TokenType.VARIATION_START);\n\n // Variations are stored on the move they replace (currentNode)\n // but they branch from the parent's position\n // For example: \"1. e4 e5 (1... c5)\" - variation stored on e5, but branches from e4's position\n const variationParent = currentNode.parent || parentNode;\n const variationPly = currentNode.ply; // Same ply as the move being replaced\n\n // Parse the variation\n const variationStart = this.parseMoveSequence(variationParent, variationPly);\n\n if (variationStart && variationStart !== variationParent) {\n currentNode.variations.push(variationStart);\n }\n\n this.consume(TokenType.VARIATION_END);\n continue;\n }\n\n // Move\n if (token.type === TokenType.MOVE) {\n const moveToken = this.consume(TokenType.MOVE);\n const color = ply % 2 === 0 ? 'w' : 'b';\n const moveNumber = Math.floor(ply / 2) + 1;\n\n // Collect NAGs\n const nags: number[] = [];\n while (this.current().type === TokenType.NAG) {\n const nagToken = this.consume(TokenType.NAG);\n const nagValue = parseInt(nagToken.value.slice(1)); // Remove '$'\n nags.push(nagValue);\n }\n\n // Collect comment after move\n let commentAfter: string | undefined;\n const annotations: {\n clock?: number;\n emt?: number;\n eval?: number;\n depth?: number;\n } = {};\n\n if (this.current().type === TokenType.COMMENT) {\n const commentToken = this.consume(TokenType.COMMENT);\n const parsed = parseAnnotations(commentToken.value);\n\n if (parsed.clock !== undefined) annotations.clock = parsed.clock;\n if (parsed.emt !== undefined) annotations.emt = parsed.emt;\n if (parsed.eval !== undefined) annotations.eval = parsed.eval;\n if (parsed.depth !== undefined) annotations.depth = parsed.depth;\n\n if (parsed.cleanComment) {\n commentAfter = parsed.cleanComment;\n }\n }\n\n // Create new move node\n const newNode: MoveNode = {\n id: this.generateNodeId(),\n ply,\n moveNumber,\n color,\n san: moveToken.value,\n nags,\n commentBefore: pendingComment,\n commentAfter,\n ...annotations,\n parent: currentNode,\n variations: [],\n };\n\n // Link to parent\n if (currentNode.next === undefined) {\n currentNode.next = newNode;\n }\n\n // Track first node created\n if (!firstNode) {\n firstNode = newNode;\n }\n\n currentNode = newNode;\n ply++;\n pendingComment = undefined;\n } else {\n // Unknown token, skip\n this.advance();\n }\n }\n\n return firstNode; // Return first node for variations\n }\n\n private current(): Token {\n return this.tokens[this.position] || { type: TokenType.EOF, value: '', line: 0, column: 0 };\n }\n\n private advance(): Token {\n return this.tokens[this.position++];\n }\n\n private consume(expectedType: TokenType): Token {\n const token = this.current();\n if (token.type !== expectedType) {\n throw new Error(\n `Expected token type ${expectedType}, got ${token.type} at line ${token.line}`\n );\n }\n return this.advance();\n }\n\n private isAtEnd(): boolean {\n return this.current().type === TokenType.EOF;\n }\n\n private generateNodeId(): string {\n return `node_${this.nodeIdCounter++}`;\n }\n}\n\n/**\n * Convenience function to parse PGN text.\n */\nexport function parsePGN(pgnText: string): PGNGame {\n const parser = new PGNParser();\n return parser.parse(pgnText);\n}\n"],"names":["PGNParser","pgnText","tokenizer","PGNTokenizer","headers","root","createRootNode","createPGNGame","TokenType","headerToken","parsed","parseHeader","parentNode","startPly","currentNode","ply","pendingComment","firstNode","token","comment","variationParent","variationPly","variationStart","moveToken","color","moveNumber","nags","nagToken","nagValue","commentAfter","annotations","commentToken","parseAnnotations","newNode","expectedType","parsePGN"],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ var n = /* @__PURE__ */ ((i) => (i.HEADER = "HEADER", i.MOVE_NUMBER = "MOVE_NUMBER", i.MOVE = "MOVE", i.NAG = "NAG", i.COMMENT = "COMMENT", i.VARIATION_START = "VARIATION_START", i.VARIATION_END = "VARIATION_END", i.RESULT = "RESULT", i.EOF = "EOF", i))(n || {});
2
+ class r {
3
+ constructor(t) {
4
+ this.position = 0, this.line = 1, this.column = 1, this.input = t;
5
+ }
6
+ /**
7
+ * Returns all tokens from the input.
8
+ */
9
+ tokenize() {
10
+ const t = [];
11
+ let e;
12
+ for (; (e = this.nextToken()).type !== "EOF"; )
13
+ t.push(e);
14
+ return t.push(e), t;
15
+ }
16
+ /**
17
+ * Gets the next token from the input.
18
+ */
19
+ nextToken() {
20
+ if (this.skipWhitespace(), this.position >= this.input.length)
21
+ return this.createToken("EOF", "");
22
+ const t = this.input[this.position];
23
+ return t === "[" ? this.readHeader() : t === "{" ? this.readBraceComment() : t === ";" ? this.readLineComment() : t === "(" ? this.createToken("VARIATION_START", this.advance()) : t === ")" ? this.createToken("VARIATION_END", this.advance()) : t === "$" ? this.readNAG() : this.isResultStart() ? this.readResult() : this.isDigit(t) ? this.readMoveNumber() : this.isMoveStart(t) ? this.readMove() : (this.advance(), this.nextToken());
24
+ }
25
+ readHeader() {
26
+ this.advance();
27
+ let t = "[", e = !1;
28
+ for (; this.position < this.input.length; ) {
29
+ const s = this.current();
30
+ if (s === '"' && (e = !e), s === "]" && !e) {
31
+ t += this.advance();
32
+ break;
33
+ }
34
+ t += this.advance();
35
+ }
36
+ return this.createToken("HEADER", t);
37
+ }
38
+ readBraceComment() {
39
+ this.advance();
40
+ let t = "";
41
+ for (; this.position < this.input.length && this.current() !== "}"; )
42
+ t += this.advance();
43
+ return this.current() === "}" && this.advance(), this.createToken("COMMENT", t.trim());
44
+ }
45
+ readLineComment() {
46
+ this.advance();
47
+ let t = "";
48
+ for (; this.position < this.input.length && this.current() !== `
49
+ `; )
50
+ t += this.advance();
51
+ return this.createToken("COMMENT", t.trim());
52
+ }
53
+ readNAG() {
54
+ this.advance();
55
+ let t = "$";
56
+ for (; this.position < this.input.length && this.isDigit(this.current()); )
57
+ t += this.advance();
58
+ return this.createToken("NAG", t);
59
+ }
60
+ readResult() {
61
+ let t = "";
62
+ if (this.current() === "*")
63
+ t = this.advance();
64
+ else
65
+ for (; this.position < this.input.length && /[01\-\/]/.test(this.current()); )
66
+ t += this.advance();
67
+ return this.createToken("RESULT", t);
68
+ }
69
+ readMoveNumber() {
70
+ let t = "";
71
+ for (; this.position < this.input.length && this.isDigit(this.current()); )
72
+ t += this.advance();
73
+ for (; this.current() === "."; )
74
+ t += this.advance();
75
+ return this.createToken("MOVE_NUMBER", t);
76
+ }
77
+ readMove() {
78
+ let t = "";
79
+ for (; this.position < this.input.length && this.isMoveChar(this.current()); )
80
+ t += this.advance();
81
+ return this.createToken("MOVE", t);
82
+ }
83
+ isMoveStart(t) {
84
+ return /[NBRQK]/.test(t) || /[a-h]/.test(t) || t === "O";
85
+ }
86
+ isMoveChar(t) {
87
+ return /[a-h1-8NBRQKO\-+=x#]/.test(t) || t === "+" || t === "#" || t === "!";
88
+ }
89
+ isResultStart() {
90
+ const t = this.input.slice(this.position);
91
+ return t.startsWith("1-0") || t.startsWith("0-1") || t.startsWith("1/2-1/2") || t.startsWith("*");
92
+ }
93
+ isDigit(t) {
94
+ return /[0-9]/.test(t);
95
+ }
96
+ skipWhitespace() {
97
+ for (; this.position < this.input.length && /\s/.test(this.current()); )
98
+ this.advance();
99
+ }
100
+ current() {
101
+ return this.input[this.position];
102
+ }
103
+ advance() {
104
+ const t = this.input[this.position];
105
+ return this.position++, t === `
106
+ ` ? (this.line++, this.column = 1) : this.column++, t;
107
+ }
108
+ createToken(t, e) {
109
+ return {
110
+ type: t,
111
+ value: e,
112
+ line: this.line,
113
+ column: this.column
114
+ };
115
+ }
116
+ }
117
+ export {
118
+ r as PGNTokenizer,
119
+ n as TokenType
120
+ };
121
+ //# sourceMappingURL=tokenizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenizer.js","sources":["../../src/parser/tokenizer.ts"],"sourcesContent":["/**\n * Token types for PGN lexical analysis\n */\nexport enum TokenType {\n HEADER = 'HEADER',\n MOVE_NUMBER = 'MOVE_NUMBER',\n MOVE = 'MOVE',\n NAG = 'NAG',\n COMMENT = 'COMMENT',\n VARIATION_START = 'VARIATION_START',\n VARIATION_END = 'VARIATION_END',\n RESULT = 'RESULT',\n EOF = 'EOF',\n}\n\n/**\n * Represents a single token from PGN text\n */\nexport interface Token {\n type: TokenType;\n value: string;\n line: number;\n column: number;\n}\n\n/**\n * Tokenizes PGN text into a stream of tokens.\n */\nexport class PGNTokenizer {\n private input: string;\n private position: number = 0;\n private line: number = 1;\n private column: number = 1;\n\n constructor(input: string) {\n this.input = input;\n }\n\n /**\n * Returns all tokens from the input.\n */\n tokenize(): Token[] {\n const tokens: Token[] = [];\n let token: Token;\n\n while ((token = this.nextToken()).type !== TokenType.EOF) {\n tokens.push(token);\n }\n\n tokens.push(token); // Add EOF token\n return tokens;\n }\n\n /**\n * Gets the next token from the input.\n */\n private nextToken(): Token {\n this.skipWhitespace();\n\n if (this.position >= this.input.length) {\n return this.createToken(TokenType.EOF, '');\n }\n\n const char = this.input[this.position];\n\n // Header: [Key \"Value\"]\n if (char === '[') {\n return this.readHeader();\n }\n\n // Comment: {text} or ; line comment\n if (char === '{') {\n return this.readBraceComment();\n }\n\n if (char === ';') {\n return this.readLineComment();\n }\n\n // Variation markers\n if (char === '(') {\n return this.createToken(TokenType.VARIATION_START, this.advance());\n }\n\n if (char === ')') {\n return this.createToken(TokenType.VARIATION_END, this.advance());\n }\n\n // NAG: $1, $2, etc.\n if (char === '$') {\n return this.readNAG();\n }\n\n // Result: 1-0, 0-1, 1/2-1/2, *\n if (this.isResultStart()) {\n return this.readResult();\n }\n\n // Move number: 1. or 1...\n if (this.isDigit(char)) {\n return this.readMoveNumber();\n }\n\n // Move in SAN notation\n if (this.isMoveStart(char)) {\n return this.readMove();\n }\n\n // Unknown character, skip it\n this.advance();\n return this.nextToken();\n }\n\n private readHeader(): Token {\n this.advance(); // skip '['\n\n let value = '[';\n let inQuotes = false;\n\n while (this.position < this.input.length) {\n const char = this.current();\n\n if (char === '\"') {\n inQuotes = !inQuotes;\n }\n\n if (char === ']' && !inQuotes) {\n value += this.advance();\n break;\n }\n\n value += this.advance();\n }\n\n return this.createToken(TokenType.HEADER, value);\n }\n\n private readBraceComment(): Token {\n this.advance(); // skip '{'\n let value = '';\n\n while (this.position < this.input.length && this.current() !== '}') {\n value += this.advance();\n }\n\n if (this.current() === '}') {\n this.advance(); // skip '}'\n }\n\n return this.createToken(TokenType.COMMENT, value.trim());\n }\n\n private readLineComment(): Token {\n this.advance(); // skip ';'\n let value = '';\n\n while (this.position < this.input.length && this.current() !== '\\n') {\n value += this.advance();\n }\n\n return this.createToken(TokenType.COMMENT, value.trim());\n }\n\n private readNAG(): Token {\n this.advance(); // skip '$'\n let value = '$';\n\n while (this.position < this.input.length && this.isDigit(this.current())) {\n value += this.advance();\n }\n\n return this.createToken(TokenType.NAG, value);\n }\n\n private readResult(): Token {\n let value = '';\n\n // Match: 1-0, 0-1, 1/2-1/2, or *\n if (this.current() === '*') {\n value = this.advance();\n } else {\n while (\n this.position < this.input.length &&\n /[01\\-\\/]/.test(this.current())\n ) {\n value += this.advance();\n }\n }\n\n return this.createToken(TokenType.RESULT, value);\n }\n\n private readMoveNumber(): Token {\n let value = '';\n\n while (this.position < this.input.length && this.isDigit(this.current())) {\n value += this.advance();\n }\n\n // Skip dots: 1. or 1...\n while (this.current() === '.') {\n value += this.advance();\n }\n\n return this.createToken(TokenType.MOVE_NUMBER, value);\n }\n\n private readMove(): Token {\n let value = '';\n\n // Read SAN move: e4, Nf3, O-O, exd5, etc.\n while (\n this.position < this.input.length &&\n this.isMoveChar(this.current())\n ) {\n value += this.advance();\n }\n\n return this.createToken(TokenType.MOVE, value);\n }\n\n private isMoveStart(char: string): boolean {\n return /[NBRQK]/.test(char) || /[a-h]/.test(char) || char === 'O';\n }\n\n private isMoveChar(char: string): boolean {\n return (\n /[a-h1-8NBRQKO\\-+=x#]/.test(char) ||\n char === '+' ||\n char === '#' ||\n char === '!'\n );\n }\n\n private isResultStart(): boolean {\n const remaining = this.input.slice(this.position);\n return (\n remaining.startsWith('1-0') ||\n remaining.startsWith('0-1') ||\n remaining.startsWith('1/2-1/2') ||\n remaining.startsWith('*')\n );\n }\n\n private isDigit(char: string): boolean {\n return /[0-9]/.test(char);\n }\n\n private skipWhitespace(): void {\n while (\n this.position < this.input.length &&\n /\\s/.test(this.current())\n ) {\n this.advance();\n }\n }\n\n private current(): string {\n return this.input[this.position];\n }\n\n private advance(): string {\n const char = this.input[this.position];\n this.position++;\n\n if (char === '\\n') {\n this.line++;\n this.column = 1;\n } else {\n this.column++;\n }\n\n return char;\n }\n\n private createToken(type: TokenType, value: string): Token {\n return {\n type,\n value,\n line: this.line,\n column: this.column,\n };\n }\n}\n"],"names":["TokenType","PGNTokenizer","input","tokens","token","char","value","inQuotes","remaining","type"],"mappings":"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"}
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .pgn-viewer{font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:14px;line-height:1.8;padding:16px;background-color:#f9f9f9;border:1px solid #ddd;border-radius:4px;max-width:100%;overflow-x:auto}.pgn-move-number{color:#666;margin-right:4px;font-weight:600}.pgn-move{display:inline-block;padding:2px 6px;margin:0 2px;border-radius:3px;transition:background-color .2s}.pgn-move:hover{background-color:#e0e0e0}.pgn-move-current{background-color:#4a90e2;color:#fff;font-weight:600}.pgn-move-current:hover{background-color:#357abd}.pgn-nag{color:#d9534f;margin-left:2px;font-weight:700}.pgn-comment{color:#5a5a5a;font-style:italic;margin:0 4px}.pgn-annotation{color:#888;font-size:12px;margin-left:4px}.pgn-variation{margin-top:4px;margin-bottom:4px;color:#555}.pgn-controls{display:flex;gap:8px;padding:12px;background-color:#f0f0f0;border-radius:4px;justify-content:center}.pgn-control-button{background-color:#4a90e2;color:#fff;border:none;border-radius:4px;padding:8px 16px;font-size:16px;cursor:pointer;transition:background-color .2s;min-width:44px}.pgn-control-button:hover:not(:disabled){background-color:#357abd}.pgn-control-button:disabled{background-color:#ccc;cursor:not-allowed;opacity:.6}.pgn-control-button:active:not(:disabled){transform:scale(.95)}
@@ -0,0 +1,40 @@
1
+ function o(e) {
2
+ const t = {}, s = e.match(/\[%clk\s+(\d+):(\d+):(\d+)\]/);
3
+ if (s) {
4
+ const [, n, c, r] = s;
5
+ t.clock = parseInt(n) * 3600 + parseInt(c) * 60 + parseInt(r);
6
+ }
7
+ const a = e.match(/\[%emt\s+(\d+):(\d+):(\d+)\]/);
8
+ if (a) {
9
+ const [, n, c, r] = a;
10
+ t.emt = parseInt(n) * 3600 + parseInt(c) * 60 + parseInt(r);
11
+ }
12
+ return t;
13
+ }
14
+ function d(e) {
15
+ const t = {}, s = e.match(/\[%eval\s+([\+\-]?\d+\.?\d*|#[\+\-]?\d+)\]/);
16
+ if (s) {
17
+ const n = s[1];
18
+ if (n.startsWith("#")) {
19
+ const c = parseInt(n.slice(1));
20
+ t.eval = c > 0 ? 1e4 : -1e4;
21
+ } else
22
+ t.eval = parseFloat(n);
23
+ }
24
+ const a = e.match(/\[%depth\s+(\d+)\]/);
25
+ return a && (t.depth = parseInt(a[1])), t;
26
+ }
27
+ function l(e) {
28
+ const t = o(e), s = d(e), a = e.replace(/\[%clk\s+\d+:\d+:\d+\]/g, "").replace(/\[%emt\s+\d+:\d+:\d+\]/g, "").replace(/\[%eval\s+[\+\-]?\d+\.?\d*\]/g, "").replace(/\[%eval\s+#[\+\-]?\d+\]/g, "").replace(/\[%depth\s+\d+\]/g, "").trim();
29
+ return {
30
+ ...t,
31
+ ...s,
32
+ cleanComment: a
33
+ };
34
+ }
35
+ export {
36
+ l as parseAnnotations,
37
+ d as parseEvalAnnotation,
38
+ o as parseTimeAnnotation
39
+ };
40
+ //# sourceMappingURL=annotation-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"annotation-parser.js","sources":["../../src/utils/annotation-parser.ts"],"sourcesContent":["/**\n * Parses time annotations from comments.\n * Supports: [%clk 0:04:32], [%emt 0:00:03]\n */\nexport function parseTimeAnnotation(comment: string): {\n clock?: number;\n emt?: number;\n} {\n const result: { clock?: number; emt?: number } = {};\n\n // Parse clock: [%clk 0:04:32]\n const clockMatch = comment.match(/\\[%clk\\s+(\\d+):(\\d+):(\\d+)\\]/);\n if (clockMatch) {\n const [, hours, minutes, seconds] = clockMatch;\n result.clock =\n parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);\n }\n\n // Parse EMT: [%emt 0:00:03]\n const emtMatch = comment.match(/\\[%emt\\s+(\\d+):(\\d+):(\\d+)\\]/);\n if (emtMatch) {\n const [, hours, minutes, seconds] = emtMatch;\n result.emt =\n parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);\n }\n\n return result;\n}\n\n/**\n * Parses evaluation annotation from comments.\n * Supports: [%eval +0.35], [%eval -1.2], [%eval #3]\n */\nexport function parseEvalAnnotation(comment: string): {\n eval?: number;\n depth?: number;\n} {\n const result: { eval?: number; depth?: number } = {};\n\n // Parse eval: [%eval +0.35] or [%eval #3]\n const evalMatch = comment.match(/\\[%eval\\s+([\\+\\-]?\\d+\\.?\\d*|#[\\+\\-]?\\d+)\\]/);\n if (evalMatch) {\n const evalStr = evalMatch[1];\n if (evalStr.startsWith('#')) {\n // Mate score: convert to large number\n const mateIn = parseInt(evalStr.slice(1));\n result.eval = mateIn > 0 ? 10000 : -10000;\n } else {\n result.eval = parseFloat(evalStr);\n }\n }\n\n // Parse depth: [%depth 18]\n const depthMatch = comment.match(/\\[%depth\\s+(\\d+)\\]/);\n if (depthMatch) {\n result.depth = parseInt(depthMatch[1]);\n }\n\n return result;\n}\n\n/**\n * Parses all annotations from a comment string.\n */\nexport function parseAnnotations(comment: string): {\n clock?: number;\n emt?: number;\n eval?: number;\n depth?: number;\n cleanComment: string;\n} {\n const timeData = parseTimeAnnotation(comment);\n const evalData = parseEvalAnnotation(comment);\n\n // Remove annotations from comment\n const cleanComment = comment\n .replace(/\\[%clk\\s+\\d+:\\d+:\\d+\\]/g, '')\n .replace(/\\[%emt\\s+\\d+:\\d+:\\d+\\]/g, '')\n .replace(/\\[%eval\\s+[\\+\\-]?\\d+\\.?\\d*\\]/g, '')\n .replace(/\\[%eval\\s+#[\\+\\-]?\\d+\\]/g, '')\n .replace(/\\[%depth\\s+\\d+\\]/g, '')\n .trim();\n\n return {\n ...timeData,\n ...evalData,\n cleanComment,\n };\n}\n"],"names":["parseTimeAnnotation","comment","result","clockMatch","hours","minutes","seconds","emtMatch","parseEvalAnnotation","evalMatch","evalStr","mateIn","depthMatch","parseAnnotations","timeData","evalData","cleanComment"],"mappings":"AAIO,SAASA,EAAoBC,GAGlC;AACE,QAAMC,IAA2C,CAAA,GAG3CC,IAAaF,EAAQ,MAAM,8BAA8B;AAC/D,MAAIE,GAAY;AACZ,UAAM,GAAGC,GAAOC,GAASC,CAAO,IAAIH;AACpC,IAAAD,EAAO,QACH,SAASE,CAAK,IAAI,OAAO,SAASC,CAAO,IAAI,KAAK,SAASC,CAAO;AAAA,EAC1E;AAGA,QAAMC,IAAWN,EAAQ,MAAM,8BAA8B;AAC7D,MAAIM,GAAU;AACV,UAAM,GAAGH,GAAOC,GAASC,CAAO,IAAIC;AACpC,IAAAL,EAAO,MACH,SAASE,CAAK,IAAI,OAAO,SAASC,CAAO,IAAI,KAAK,SAASC,CAAO;AAAA,EAC1E;AAEA,SAAOJ;AACX;AAMO,SAASM,EAAoBP,GAGlC;AACE,QAAMC,IAA4C,CAAA,GAG5CO,IAAYR,EAAQ,MAAM,4CAA4C;AAC5E,MAAIQ,GAAW;AACX,UAAMC,IAAUD,EAAU,CAAC;AAC3B,QAAIC,EAAQ,WAAW,GAAG,GAAG;AAEzB,YAAMC,IAAS,SAASD,EAAQ,MAAM,CAAC,CAAC;AACxC,MAAAR,EAAO,OAAOS,IAAS,IAAI,MAAQ;AAAA,IACvC;AACI,MAAAT,EAAO,OAAO,WAAWQ,CAAO;AAAA,EAExC;AAGA,QAAME,IAAaX,EAAQ,MAAM,oBAAoB;AACrD,SAAIW,MACAV,EAAO,QAAQ,SAASU,EAAW,CAAC,CAAC,IAGlCV;AACX;AAKO,SAASW,EAAiBZ,GAM/B;AACE,QAAMa,IAAWd,EAAoBC,CAAO,GACtCc,IAAWP,EAAoBP,CAAO,GAGtCe,IAAef,EAChB,QAAQ,2BAA2B,EAAE,EACrC,QAAQ,2BAA2B,EAAE,EACrC,QAAQ,iCAAiC,EAAE,EAC3C,QAAQ,4BAA4B,EAAE,EACtC,QAAQ,qBAAqB,EAAE,EAC/B,KAAA;AAEL,SAAO;AAAA,IACH,GAAGa;AAAA,IACH,GAAGC;AAAA,IACH,cAAAC;AAAA,EAAA;AAER;"}
@@ -0,0 +1,56 @@
1
+ const o = {
2
+ 1: "!",
3
+ // Good move
4
+ 2: "?",
5
+ // Poor move
6
+ 3: "!!",
7
+ // Very good move
8
+ 4: "??",
9
+ // Very poor move
10
+ 5: "!?",
11
+ // Interesting move
12
+ 6: "?!",
13
+ // Questionable move
14
+ 7: "□",
15
+ // Forced move
16
+ 10: "=",
17
+ // Equal position
18
+ 13: "∞",
19
+ // Unclear position
20
+ 14: "⩲",
21
+ // White has slight advantage
22
+ 15: "⩱",
23
+ // Black has slight advantage
24
+ 16: "±",
25
+ // White has moderate advantage
26
+ 17: "∓",
27
+ // Black has moderate advantage
28
+ 18: "+−",
29
+ // White has decisive advantage
30
+ 19: "−+",
31
+ // Black has decisive advantage
32
+ 22: "⨀",
33
+ // Zugzwang
34
+ 32: "⟳",
35
+ // Development advantage
36
+ 36: "↑",
37
+ // Initiative
38
+ 40: "→",
39
+ // Attack
40
+ 132: "⇆",
41
+ // Counterplay
42
+ 138: "⊕"
43
+ // Time pressure
44
+ };
45
+ function t(n) {
46
+ return o[n] || `$${n}`;
47
+ }
48
+ function r(n) {
49
+ return n.map(t);
50
+ }
51
+ export {
52
+ o as NAG_SYMBOLS,
53
+ t as nagToSymbol,
54
+ r as nagsToSymbols
55
+ };
56
+ //# sourceMappingURL=nag-symbols.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nag-symbols.js","sources":["../../src/utils/nag-symbols.ts"],"sourcesContent":["/**\n * Maps NAG (Numeric Annotation Glyph) codes to their symbolic representations.\n */\nexport const NAG_SYMBOLS: Record<number, string> = {\n 1: '!', // Good move\n 2: '?', // Poor move\n 3: '!!', // Very good move\n 4: '??', // Very poor move\n 5: '!?', // Interesting move\n 6: '?!', // Questionable move\n 7: '□', // Forced move\n 10: '=', // Equal position\n 13: '∞', // Unclear position\n 14: '⩲', // White has slight advantage\n 15: '⩱', // Black has slight advantage\n 16: '±', // White has moderate advantage\n 17: '∓', // Black has moderate advantage\n 18: '+−', // White has decisive advantage\n 19: '−+', // Black has decisive advantage\n 22: '⨀', // Zugzwang\n 32: '⟳', // Development advantage\n 36: '↑', // Initiative\n 40: '→', // Attack\n 132: '⇆', // Counterplay\n 138: '⊕', // Time pressure\n};\n\n/**\n * Converts a NAG code to its symbolic representation.\n */\nexport function nagToSymbol(nag: number): string {\n return NAG_SYMBOLS[nag] || `$${nag}`;\n}\n\n/**\n * Converts an array of NAG codes to their symbolic representations.\n */\nexport function nagsToSymbols(nags: number[]): string[] {\n return nags.map(nagToSymbol);\n}\n"],"names":["NAG_SYMBOLS","nagToSymbol","nag","nagsToSymbols","nags"],"mappings":"AAGO,MAAMA,IAAsC;AAAA,EAC/C,GAAG;AAAA;AAAA,EACH,GAAG;AAAA;AAAA,EACH,GAAG;AAAA;AAAA,EACH,GAAG;AAAA;AAAA,EACH,GAAG;AAAA;AAAA,EACH,GAAG;AAAA;AAAA,EACH,GAAG;AAAA;AAAA,EACH,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,KAAK;AAAA;AAAA,EACL,KAAK;AAAA;AACT;AAKO,SAASC,EAAYC,GAAqB;AAC7C,SAAOF,EAAYE,CAAG,KAAK,IAAIA,CAAG;AACtC;AAKO,SAASC,EAAcC,GAA0B;AACpD,SAAOA,EAAK,IAAIH,CAAW;AAC/B;"}
@@ -0,0 +1,71 @@
1
+ import { jsxs as y, jsx as d } from "react/jsx-runtime";
2
+ import { useCallback as f, useEffect as A } from "react";
3
+ /* empty css */
4
+ function x({
5
+ cursor: l,
6
+ onPositionChange: t,
7
+ enableKeyboard: w = !0,
8
+ className: b = ""
9
+ }) {
10
+ const r = f(() => {
11
+ l.toStart(), t == null || t(l);
12
+ }, [l, t]), p = f(() => {
13
+ l.prev(), t == null || t(l);
14
+ }, [l, t]), v = f(() => {
15
+ l.next(), t == null || t(l);
16
+ }, [l, t]), m = f(() => {
17
+ l.toEnd(), t == null || t(l);
18
+ }, [l, t]);
19
+ return A(() => {
20
+ if (!w) return;
21
+ const k = (e) => {
22
+ e.ctrlKey || e.metaKey ? e.key === "ArrowLeft" ? (e.preventDefault(), r()) : e.key === "ArrowRight" && (e.preventDefault(), m()) : e.key === "ArrowLeft" ? (e.preventDefault(), p()) : e.key === "ArrowRight" ? (e.preventDefault(), v()) : e.key === "ArrowUp" ? (e.preventDefault(), l.exitVariation(), t == null || t(l)) : e.key === "ArrowDown" && (e.preventDefault(), l.enterVariation(0), t == null || t(l));
23
+ };
24
+ return window.addEventListener("keydown", k), () => window.removeEventListener("keydown", k);
25
+ }, [w, r, p, v, m, l, t]), /* @__PURE__ */ y("div", { className: `pgn-controls ${b}`, children: [
26
+ /* @__PURE__ */ d(
27
+ "button",
28
+ {
29
+ className: "pgn-control-button",
30
+ onClick: r,
31
+ disabled: l.isAtStart(),
32
+ title: "First move (Ctrl+←)",
33
+ children: "⏮"
34
+ }
35
+ ),
36
+ /* @__PURE__ */ d(
37
+ "button",
38
+ {
39
+ className: "pgn-control-button",
40
+ onClick: p,
41
+ disabled: l.isAtStart(),
42
+ title: "Previous move (←)",
43
+ children: "◀"
44
+ }
45
+ ),
46
+ /* @__PURE__ */ d(
47
+ "button",
48
+ {
49
+ className: "pgn-control-button",
50
+ onClick: v,
51
+ disabled: l.isAtEnd(),
52
+ title: "Next move (→)",
53
+ children: "▶"
54
+ }
55
+ ),
56
+ /* @__PURE__ */ d(
57
+ "button",
58
+ {
59
+ className: "pgn-control-button",
60
+ onClick: m,
61
+ disabled: l.isAtEnd(),
62
+ title: "Last move (Ctrl+→)",
63
+ children: "⏭"
64
+ }
65
+ )
66
+ ] });
67
+ }
68
+ export {
69
+ x as PGNControls
70
+ };
71
+ //# sourceMappingURL=PGNControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PGNControls.js","sources":["../../src/viewer/PGNControls.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useCallback } from 'react';\nimport { GameCursor } from '../cursor/game-cursor.js';\nimport './styles.css';\n\nexport interface PGNControlsProps {\n /** The game cursor to control */\n cursor: GameCursor;\n /** Callback when position changes */\n onPositionChange?: (cursor: GameCursor) => void;\n /** Enable keyboard controls (default: true) */\n enableKeyboard?: boolean;\n /** Custom class name */\n className?: string;\n}\n\n/**\n * Navigation controls for a chess game.\n */\nexport function PGNControls({\n cursor,\n onPositionChange,\n enableKeyboard = true,\n className = '',\n}: PGNControlsProps) {\n const handleFirst = useCallback(() => {\n cursor.toStart();\n onPositionChange?.(cursor);\n }, [cursor, onPositionChange]);\n\n const handlePrev = useCallback(() => {\n cursor.prev();\n onPositionChange?.(cursor);\n }, [cursor, onPositionChange]);\n\n const handleNext = useCallback(() => {\n cursor.next();\n onPositionChange?.(cursor);\n }, [cursor, onPositionChange]);\n\n const handleLast = useCallback(() => {\n cursor.toEnd();\n onPositionChange?.(cursor);\n }, [cursor, onPositionChange]);\n\n // Keyboard controls\n useEffect(() => {\n if (!enableKeyboard) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.ctrlKey || e.metaKey) {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n handleFirst();\n } else if (e.key === 'ArrowRight') {\n e.preventDefault();\n handleLast();\n }\n } else {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n handlePrev();\n } else if (e.key === 'ArrowRight') {\n e.preventDefault();\n handleNext();\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n cursor.exitVariation();\n onPositionChange?.(cursor);\n } else if (e.key === 'ArrowDown') {\n e.preventDefault();\n cursor.enterVariation(0);\n onPositionChange?.(cursor);\n }\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [enableKeyboard, handleFirst, handlePrev, handleNext, handleLast, cursor, onPositionChange]);\n\n return (\n <div className={`pgn-controls ${className}`}>\n <button\n className=\"pgn-control-button\"\n onClick={handleFirst}\n disabled={cursor.isAtStart()}\n title=\"First move (Ctrl+←)\"\n >\n ⏮\n </button>\n <button\n className=\"pgn-control-button\"\n onClick={handlePrev}\n disabled={cursor.isAtStart()}\n title=\"Previous move (←)\"\n >\n ◀\n </button>\n <button\n className=\"pgn-control-button\"\n onClick={handleNext}\n disabled={cursor.isAtEnd()}\n title=\"Next move (→)\"\n >\n ▶\n </button>\n <button\n className=\"pgn-control-button\"\n onClick={handleLast}\n disabled={cursor.isAtEnd()}\n title=\"Last move (Ctrl+→)\"\n >\n ⏭\n </button>\n </div>\n );\n}\n"],"names":["PGNControls","cursor","onPositionChange","enableKeyboard","className","handleFirst","useCallback","handlePrev","handleNext","handleLast","useEffect","handleKeyDown","jsxs","jsx"],"mappings":";;;AAoBO,SAASA,EAAY;AAAA,EACxB,QAAAC;AAAA,EACA,kBAAAC;AAAA,EACA,gBAAAC,IAAiB;AAAA,EACjB,WAAAC,IAAY;AAChB,GAAqB;AACjB,QAAMC,IAAcC,EAAY,MAAM;AAClC,IAAAL,EAAO,QAAA,GACPC,KAAA,QAAAA,EAAmBD;AAAA,EACvB,GAAG,CAACA,GAAQC,CAAgB,CAAC,GAEvBK,IAAaD,EAAY,MAAM;AACjC,IAAAL,EAAO,KAAA,GACPC,KAAA,QAAAA,EAAmBD;AAAA,EACvB,GAAG,CAACA,GAAQC,CAAgB,CAAC,GAEvBM,IAAaF,EAAY,MAAM;AACjC,IAAAL,EAAO,KAAA,GACPC,KAAA,QAAAA,EAAmBD;AAAA,EACvB,GAAG,CAACA,GAAQC,CAAgB,CAAC,GAEvBO,IAAaH,EAAY,MAAM;AACjC,IAAAL,EAAO,MAAA,GACPC,KAAA,QAAAA,EAAmBD;AAAA,EACvB,GAAG,CAACA,GAAQC,CAAgB,CAAC;AAG7B,SAAAQ,EAAU,MAAM;AACZ,QAAI,CAACP,EAAgB;AAErB,UAAMQ,IAAgB,CAAC,MAAqB;AACxC,MAAI,EAAE,WAAW,EAAE,UACX,EAAE,QAAQ,eACV,EAAE,eAAA,GACFN,EAAA,KACO,EAAE,QAAQ,iBACjB,EAAE,eAAA,GACFI,EAAA,KAGA,EAAE,QAAQ,eACV,EAAE,eAAA,GACFF,EAAA,KACO,EAAE,QAAQ,gBACjB,EAAE,eAAA,GACFC,EAAA,KACO,EAAE,QAAQ,aACjB,EAAE,eAAA,GACFP,EAAO,cAAA,GACPC,KAAA,QAAAA,EAAmBD,MACZ,EAAE,QAAQ,gBACjB,EAAE,eAAA,GACFA,EAAO,eAAe,CAAC,GACvBC,KAAA,QAAAA,EAAmBD;AAAA,IAG/B;AAEA,kBAAO,iBAAiB,WAAWU,CAAa,GACzC,MAAM,OAAO,oBAAoB,WAAWA,CAAa;AAAA,EACpE,GAAG,CAACR,GAAgBE,GAAaE,GAAYC,GAAYC,GAAYR,GAAQC,CAAgB,CAAC,GAG1F,gBAAAU,EAAC,OAAA,EAAI,WAAW,gBAAgBR,CAAS,IACrC,UAAA;AAAA,IAAA,gBAAAS;AAAA,MAAC;AAAA,MAAA;AAAA,QACG,WAAU;AAAA,QACV,SAASR;AAAA,QACT,UAAUJ,EAAO,UAAA;AAAA,QACjB,OAAM;AAAA,QACT,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAGD,gBAAAY;AAAA,MAAC;AAAA,MAAA;AAAA,QACG,WAAU;AAAA,QACV,SAASN;AAAA,QACT,UAAUN,EAAO,UAAA;AAAA,QACjB,OAAM;AAAA,QACT,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAGD,gBAAAY;AAAA,MAAC;AAAA,MAAA;AAAA,QACG,WAAU;AAAA,QACV,SAASL;AAAA,QACT,UAAUP,EAAO,QAAA;AAAA,QACjB,OAAM;AAAA,QACT,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAGD,gBAAAY;AAAA,MAAC;AAAA,MAAA;AAAA,QACG,WAAU;AAAA,QACV,SAASJ;AAAA,QACT,UAAUR,EAAO,QAAA;AAAA,QACjB,OAAM;AAAA,QACT,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAED,GACJ;AAER;"}
@@ -0,0 +1,94 @@
1
+ import { jsx as o, jsxs as l, Fragment as v } from "react/jsx-runtime";
2
+ import { useState as f, useEffect as $, useCallback as N } from "react";
3
+ import { GameCursor as x } from "../cursor/game-cursor.js";
4
+ import { nagToSymbol as b } from "../utils/nag-symbols.js";
5
+ /* empty css */
6
+ function k({
7
+ root: s,
8
+ cursor: a,
9
+ onMoveClick: t,
10
+ className: i = ""
11
+ }) {
12
+ const [e] = f(() => new x(s)), n = a || e, [p, c] = f(n.current.id);
13
+ $(() => {
14
+ c(n.current.id);
15
+ }, [n.current.id]);
16
+ const m = N(
17
+ (r) => {
18
+ n.goTo(r.id), c(r.id), t == null || t(r);
19
+ },
20
+ [n, t]
21
+ );
22
+ return /* @__PURE__ */ o("div", { className: `pgn-viewer ${i}`, children: h(s, p, m, 0) });
23
+ }
24
+ function h(s, a, t, i) {
25
+ const e = [];
26
+ let n = s.next;
27
+ for (; n; ) {
28
+ const p = n.id === a, c = n.color === "w";
29
+ n.commentBefore && e.push(
30
+ /* @__PURE__ */ o("span", { className: "pgn-comment", children: `{${n.commentBefore}}` }, `comment-before-${n.id}`)
31
+ ), c && e.push(
32
+ /* @__PURE__ */ l("span", { className: "pgn-move-number", children: [
33
+ n.moveNumber,
34
+ "."
35
+ ] }, `move-num-${n.id}`)
36
+ );
37
+ const m = n.nags.map(b).join("");
38
+ e.push(
39
+ /* @__PURE__ */ l(
40
+ "span",
41
+ {
42
+ className: `pgn-move ${p ? "pgn-move-current" : ""}`,
43
+ onClick: () => t(n),
44
+ style: { cursor: "pointer" },
45
+ children: [
46
+ n.san,
47
+ m && /* @__PURE__ */ o("span", { className: "pgn-nag", children: m })
48
+ ]
49
+ },
50
+ n.id
51
+ )
52
+ ), n.commentAfter && e.push(
53
+ /* @__PURE__ */ o("span", { className: "pgn-comment", children: `{${n.commentAfter}}` }, `comment-after-${n.id}`)
54
+ );
55
+ const r = [];
56
+ if (n.clock !== void 0) {
57
+ const u = Math.floor(n.clock / 3600), d = Math.floor(n.clock % 3600 / 60), g = n.clock % 60;
58
+ r.push(`clk ${u}:${d.toString().padStart(2, "0")}:${g.toString().padStart(2, "0")}`);
59
+ }
60
+ n.eval !== void 0 && r.push(`eval ${n.eval > 0 ? "+" : ""}${n.eval.toFixed(2)}`), r.length > 0 && e.push(
61
+ /* @__PURE__ */ l("span", { className: "pgn-annotation", children: [
62
+ "[",
63
+ r.join(", "),
64
+ "]"
65
+ ] }, `annotations-${n.id}`)
66
+ ), n.variations.length > 0 && n.variations.forEach((u, d) => {
67
+ e.push(
68
+ /* @__PURE__ */ l(
69
+ "div",
70
+ {
71
+ className: "pgn-variation",
72
+ style: { marginLeft: `${(i + 1) * 20}px` },
73
+ children: [
74
+ "(",
75
+ h(
76
+ { ...n, next: u },
77
+ a,
78
+ t,
79
+ i + 1
80
+ ),
81
+ ")"
82
+ ]
83
+ },
84
+ `variation-${n.id}-${d}`
85
+ )
86
+ );
87
+ }), n = n.next;
88
+ }
89
+ return /* @__PURE__ */ o(v, { children: e });
90
+ }
91
+ export {
92
+ k as PGNViewer
93
+ };
94
+ //# sourceMappingURL=PGNViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PGNViewer.js","sources":["../../src/viewer/PGNViewer.tsx"],"sourcesContent":["'use client';\n\nimport React, { useState, useEffect, useCallback } from 'react';\nimport { MoveNode } from '../model/move-node.js';\nimport { GameCursor } from '../cursor/game-cursor.js';\nimport { nagToSymbol } from '../utils/nag-symbols.js';\nimport './styles.css';\n\nexport interface PGNViewerProps {\n /** The root node of the game tree */\n root: MoveNode;\n /** Optional cursor for external control */\n cursor?: GameCursor;\n /** Callback when a move is clicked */\n onMoveClick?: (node: MoveNode) => void;\n /** Custom class name */\n className?: string;\n}\n\n/**\n * Displays a chess game in PGN notation with support for variations.\n */\nexport function PGNViewer({\n root,\n cursor: externalCursor,\n onMoveClick,\n className = '',\n}: PGNViewerProps) {\n const [internalCursor] = useState(() => new GameCursor(root));\n const cursor = externalCursor || internalCursor;\n const [currentNodeId, setCurrentNodeId] = useState(cursor.current.id);\n\n // Update when cursor changes\n useEffect(() => {\n setCurrentNodeId(cursor.current.id);\n }, [cursor.current.id]);\n\n const handleMoveClick = useCallback(\n (node: MoveNode) => {\n cursor.goTo(node.id);\n setCurrentNodeId(node.id);\n onMoveClick?.(node);\n },\n [cursor, onMoveClick]\n );\n\n return (\n <div className={`pgn-viewer ${className}`}>\n {renderMoveTree(root, currentNodeId, handleMoveClick, 0)}\n </div>\n );\n}\n\n/**\n * Recursively renders the move tree with proper indentation for variations.\n */\nfunction renderMoveTree(\n node: MoveNode,\n currentNodeId: string,\n onMoveClick: (node: MoveNode) => void,\n depth: number\n): React.ReactNode {\n const elements: React.ReactNode[] = [];\n let currentNode: MoveNode | undefined = node.next;\n let moveIndex = 0;\n\n while (currentNode) {\n const isCurrentMove = currentNode.id === currentNodeId;\n const showMoveNumber = currentNode.color === 'w';\n\n // Comment before move\n if (currentNode.commentBefore) {\n elements.push(\n <span key={`comment-before-${currentNode.id}`} className=\"pgn-comment\">\n {`{${currentNode.commentBefore}}`}\n </span>\n );\n }\n\n // Move number for white moves\n if (showMoveNumber) {\n elements.push(\n <span key={`move-num-${currentNode.id}`} className=\"pgn-move-number\">\n {currentNode.moveNumber}.\n </span>\n );\n }\n\n // The move itself\n const nags = currentNode.nags.map(nagToSymbol).join('');\n elements.push(\n <span\n key={currentNode.id}\n className={`pgn-move ${isCurrentMove ? 'pgn-move-current' : ''}`}\n onClick={() => onMoveClick(currentNode!)}\n style={{ cursor: 'pointer' }}\n >\n {currentNode.san}\n {nags && <span className=\"pgn-nag\">{nags}</span>}\n </span>\n );\n\n // Comment after move\n if (currentNode.commentAfter) {\n elements.push(\n <span key={`comment-after-${currentNode.id}`} className=\"pgn-comment\">\n {`{${currentNode.commentAfter}}`}\n </span>\n );\n }\n\n // Annotations (clock, eval)\n const annotations: string[] = [];\n if (currentNode.clock !== undefined) {\n const hours = Math.floor(currentNode.clock / 3600);\n const minutes = Math.floor((currentNode.clock % 3600) / 60);\n const seconds = currentNode.clock % 60;\n annotations.push(`clk ${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);\n }\n if (currentNode.eval !== undefined) {\n annotations.push(`eval ${currentNode.eval > 0 ? '+' : ''}${currentNode.eval.toFixed(2)}`);\n }\n\n if (annotations.length > 0) {\n elements.push(\n <span key={`annotations-${currentNode.id}`} className=\"pgn-annotation\">\n [{annotations.join(', ')}]\n </span>\n );\n }\n\n // Variations\n if (currentNode.variations.length > 0) {\n currentNode.variations.forEach((variation, index) => {\n elements.push(\n <div\n key={`variation-${currentNode!.id}-${index}`}\n className=\"pgn-variation\"\n style={{ marginLeft: `${(depth + 1) * 20}px` }}\n >\n (\n {renderMoveTree(\n { ...currentNode!, next: variation },\n currentNodeId,\n onMoveClick,\n depth + 1\n )}\n )\n </div>\n );\n });\n }\n\n currentNode = currentNode.next;\n moveIndex++;\n }\n\n return <>{elements}</>;\n}\n"],"names":["PGNViewer","root","externalCursor","onMoveClick","className","internalCursor","useState","GameCursor","cursor","currentNodeId","setCurrentNodeId","useEffect","handleMoveClick","useCallback","node","jsx","renderMoveTree","depth","elements","currentNode","isCurrentMove","showMoveNumber","jsxs","nags","nagToSymbol","annotations","hours","minutes","seconds","variation","index"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { PGNViewer as e } from "./PGNViewer.js";
2
+ import { PGNControls as f } from "./PGNControls.js";
3
+ export {
4
+ f as PGNControls,
5
+ e as PGNViewer
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "pgn-viewer-parser",
3
+ "version": "1.0.0",
4
+ "description": "A production-ready PGN parser and viewer library for chess, with full support for variations, comments, NAGs, and annotations",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./viewer": {
15
+ "types": "./dist/viewer/index.d.ts",
16
+ "import": "./dist/viewer/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc && vite build",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "type-check": "tsc --noEmit",
28
+ "prepublishOnly": "npm run type-check && npm run test && npm run build"
29
+ },
30
+ "keywords": [
31
+ "chess",
32
+ "pgn",
33
+ "parser",
34
+ "viewer",
35
+ "react",
36
+ "nextjs",
37
+ "typescript",
38
+ "variations",
39
+ "tree"
40
+ ],
41
+ "author": "",
42
+ "license": "MIT",
43
+ "peerDependencies": {
44
+ "react": "^18.0.0",
45
+ "react-dom": "^18.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "react": {
49
+ "optional": true
50
+ },
51
+ "react-dom": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^20.11.0",
57
+ "@types/react": "^18.2.48",
58
+ "@types/react-dom": "^18.2.18",
59
+ "react": "^18.2.0",
60
+ "react-dom": "^18.2.0",
61
+ "typescript": "^5.3.3",
62
+ "vite": "^5.0.11",
63
+ "vitest": "^1.2.0"
64
+ },
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "https://github.com/yourusername/pgn-viewer-parser.git"
68
+ }
69
+ }