pgn2 2.0.2 → 2.0.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/lib/pgn/game.rb +11 -5
- data/lib/pgn/pgn_parser.rb +77 -78
- data/lib/pgn/pgn_parser.y +37 -26
- data/lib/pgn/version.rb +1 -1
- data/lib/pgn.rb +4 -0
- data/spec/parser_explicit_spec.rb +39 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26cc37d3385d1f5516a2cff916f3219198914295b2ea83b587f1a8a6ee267480
|
|
4
|
+
data.tar.gz: 7efabc9471ace989a11a48879fb496f3999d35dbb42b3c8c138bdc392beeb597
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3622fadf7c7918121f2c3e6700d1bb4284d9a258d76f56a2b47d20813f7f837ad357ff45892e252da96b6c52694909241809f203b97872b5bdb3dbe5a2b33023
|
|
7
|
+
data.tar.gz: e3205c5033d63601f1a64e175e7e1799ab9c698e736d216e15408c4eb358851ba8bf415139bf66780dbc5bd559fde6468ca06cf81a29df056d50c6ba52f9b6b9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.3 (2026-08-20)
|
|
4
|
+
|
|
5
|
+
### Summary
|
|
6
|
+
|
|
7
|
+
Parser fix: a move may now be followed by any number of comments, NAGs,
|
|
8
|
+
and variations, in any order (PGN spec §3.2.5). Previously the grammar
|
|
9
|
+
accepted only a single trailing comment before a variation, so common
|
|
10
|
+
real-world annotations like `1... Kxh7 {a} {[%cal ...]} (20... Kf8)` raised
|
|
11
|
+
`parse error on value "("` and whole files were skipped. No public API
|
|
12
|
+
changes; `MoveText#comment` and `#variations` keep their existing shapes.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`PGN::PgnParser` (grammar)** — replace the single-comment
|
|
17
|
+
`move_trailer` + separate `variation_list` with a `move_suffix` list of
|
|
18
|
+
`[:comment, c] | [:nags, a] | [:variation, v]` items folded onto the
|
|
19
|
+
move by the new `build_move` helper. Multiple comments on one move are
|
|
20
|
+
concatenated with a space (each cleaned individually); variations keep
|
|
21
|
+
the legacy reversed storage order so round-trip and serializer behavior
|
|
22
|
+
are unchanged.
|
|
23
|
+
- **`PGN::MoveText.clean_text`** — extracted as a class method so the
|
|
24
|
+
parser can clean each trailing comment before merging; the instance
|
|
25
|
+
method delegates to it (behavior unchanged).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Two (or more) comments before a variation no longer abort parsing.
|
|
30
|
+
- A comment appearing after a variation now attaches to the move it
|
|
31
|
+
follows instead of becoming a stray game comment.
|
|
32
|
+
- `PGN.parse` no longer mutates the caller's input string (it dups before
|
|
33
|
+
`force_encoding`), so frozen-string-literal callers no longer raise
|
|
34
|
+
FrozenError on the target Ruby.
|
|
35
|
+
|
|
36
|
+
### Known issues
|
|
37
|
+
|
|
38
|
+
- A lone backslash inside a comment that is not part of `\\`, `\{`, or
|
|
39
|
+
`\}` (e.g. `{/\Ba3#}`) still raises an "Unmatched input" lexer error;
|
|
40
|
+
the comment escape grammar remains stricter than the PGN spec.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
3
44
|
## 2.0.2 (2026-08-18)
|
|
4
45
|
|
|
5
46
|
### Summary
|
data/lib/pgn/game.rb
CHANGED
|
@@ -9,7 +9,7 @@ module PGN
|
|
|
9
9
|
def initialize(notation, annotation = nil, comment = nil, variations = [])
|
|
10
10
|
@notation = notation
|
|
11
11
|
@annotation = annotation
|
|
12
|
-
@comment = clean_text(comment)
|
|
12
|
+
@comment = MoveText.clean_text(comment)
|
|
13
13
|
@variations = variations
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -34,16 +34,22 @@ module PGN
|
|
|
34
34
|
san.include?('0') ? san.gsub('0', 'O') : san
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
# Strip the single outermost brace pair, then unescape \{ \} \\ so
|
|
38
|
+
# the internal representation is the raw comment body. {Serializer}
|
|
39
|
+
# re-escapes on output, so parse -> serialize -> parse is byte-stable.
|
|
40
|
+
# Exposed as a class method so the parser can clean each trailing comment
|
|
41
|
+
# individually before concatenating multiple comments on one move.
|
|
42
|
+
def self.clean_text(text)
|
|
38
43
|
return unless text
|
|
39
44
|
|
|
40
|
-
# Strip the single outermost brace pair, then unescape \{ \} \\ so
|
|
41
|
-
# the internal representation is the raw comment body. {Serializer}
|
|
42
|
-
# re-escapes on output, so parse -> serialize -> parse is byte-stable.
|
|
43
45
|
text = text[1..-2] if text.start_with?('{') && text.end_with?('}')
|
|
44
46
|
text = text.gsub(/\\([\\{}])/, '\1')
|
|
45
47
|
text.gsub(/\s+/, ' ').strip
|
|
46
48
|
end
|
|
49
|
+
|
|
50
|
+
def clean_text(text)
|
|
51
|
+
MoveText.clean_text(text)
|
|
52
|
+
end
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
# {PGN::Game} holds all of the information about a game. It is either
|
data/lib/pgn/pgn_parser.rb
CHANGED
|
@@ -8,7 +8,7 @@ require 'racc/parser.rb'
|
|
|
8
8
|
module PGN
|
|
9
9
|
class PgnParser < Racc::Parser
|
|
10
10
|
|
|
11
|
-
module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y',
|
|
11
|
+
module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 87)
|
|
12
12
|
|
|
13
13
|
def parse(input)
|
|
14
14
|
@lexer = PGN::Lexer.new(input)
|
|
@@ -29,6 +29,26 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 96)
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
+
# Fold a move's trailing suffix (comments, NAGs, variations, in any
|
|
33
|
+
# order) onto a MoveText. Multiple comments are concatenated with a
|
|
34
|
+
# space after each is individually cleaned; NAGs are merged in order;
|
|
35
|
+
# variations are collected in source order and then reversed, preserving
|
|
36
|
+
# the legacy order the serializer and round-trip specs depend on.
|
|
37
|
+
def build_move(notation, suffix)
|
|
38
|
+
comment_parts = []
|
|
39
|
+
nags = nil
|
|
40
|
+
variations = []
|
|
41
|
+
suffix.each do |kind, payload|
|
|
42
|
+
case kind
|
|
43
|
+
when :comment then comment_parts << MoveText.clean_text(payload)
|
|
44
|
+
when :nags then nags = nags ? nags + payload : payload
|
|
45
|
+
when :variation then variations << payload
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
comment = comment_parts.empty? ? nil : comment_parts.join(' ')
|
|
49
|
+
MoveText.new(notation, nags, comment, variations.reverse)
|
|
50
|
+
end
|
|
51
|
+
|
|
32
52
|
# Punctuation tokens are racc'd by their literal character (taken from
|
|
33
53
|
# PGN::Lexer::LITERAL_BYTES, the single source of truth for those
|
|
34
54
|
# characters); everything else is racc'd by the upcased lexer symbol.
|
|
@@ -66,42 +86,44 @@ module_eval(<<'...end pgn_parser.y/module_eval...', 'pgn_parser.y', 96)
|
|
|
66
86
|
##### State transition tables begin ###
|
|
67
87
|
|
|
68
88
|
racc_action_table = [
|
|
69
|
-
2, 16, 7, 17,
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
2, 16, 7, 17, 21, 14, 6, 24, 21, 6,
|
|
90
|
+
30, 24, 25, 16, 12, 17, 25, 14, 11, 18,
|
|
91
|
+
26, 28 ]
|
|
72
92
|
|
|
73
93
|
racc_action_check = [
|
|
74
|
-
1,
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
1, 29, 2, 29, 17, 29, 4, 17, 19, 1,
|
|
95
|
+
29, 19, 17, 10, 10, 10, 19, 10, 6, 11,
|
|
96
|
+
18, 22 ]
|
|
77
97
|
|
|
78
98
|
racc_action_pointer = [
|
|
79
|
-
nil, 0, 2, nil, -3, nil,
|
|
80
|
-
|
|
81
|
-
nil, nil, nil,
|
|
82
|
-
nil
|
|
99
|
+
nil, 0, 2, nil, -3, nil, 10, nil, nil, nil,
|
|
100
|
+
10, 17, nil, nil, nil, nil, nil, 1, 10, 5,
|
|
101
|
+
nil, nil, 15, nil, nil, nil, nil, nil, nil, -2,
|
|
102
|
+
nil ]
|
|
83
103
|
|
|
84
104
|
racc_action_default = [
|
|
85
|
-
-1, -
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
105
|
+
-1, -23, -23, -2, -8, -4, -23, 31, -3, -5,
|
|
106
|
+
-23, -23, -7, -9, -10, -11, -12, -13, -23, -14,
|
|
107
|
+
-15, -17, -18, -19, -20, -8, -6, -16, -21, -23,
|
|
108
|
+
-22 ]
|
|
89
109
|
|
|
90
110
|
racc_goto_table = [
|
|
91
|
-
10,
|
|
92
|
-
|
|
111
|
+
10, 5, 1, 20, 9, 27, 3, 4, 8, 19,
|
|
112
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
113
|
+
nil, 29 ]
|
|
93
114
|
|
|
94
115
|
racc_goto_check = [
|
|
95
|
-
6,
|
|
96
|
-
|
|
116
|
+
6, 5, 1, 10, 5, 10, 2, 3, 4, 9,
|
|
117
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
118
|
+
nil, 6 ]
|
|
97
119
|
|
|
98
120
|
racc_goto_pointer = [
|
|
99
|
-
nil,
|
|
100
|
-
|
|
121
|
+
nil, 2, 5, 6, 4, 0, -4, nil, nil, -8,
|
|
122
|
+
-14, nil, nil ]
|
|
101
123
|
|
|
102
124
|
racc_goto_default = [
|
|
103
125
|
nil, nil, nil, nil, nil, nil, nil, 13, 15, nil,
|
|
104
|
-
nil,
|
|
126
|
+
nil, 22, 23 ]
|
|
105
127
|
|
|
106
128
|
racc_reduce_table = [
|
|
107
129
|
0, 0, :racc_error,
|
|
@@ -116,23 +138,21 @@ racc_reduce_table = [
|
|
|
116
138
|
2, 19, :_reduce_9,
|
|
117
139
|
1, 20, :_reduce_10,
|
|
118
140
|
1, 20, :_reduce_none,
|
|
119
|
-
|
|
120
|
-
1,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
1, 20, :_reduce_12,
|
|
142
|
+
1, 21, :_reduce_13,
|
|
143
|
+
2, 21, :_reduce_14,
|
|
144
|
+
1, 22, :_reduce_15,
|
|
145
|
+
2, 22, :_reduce_16,
|
|
124
146
|
1, 23, :_reduce_17,
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
1, 23, :_reduce_18,
|
|
148
|
+
1, 23, :_reduce_19,
|
|
127
149
|
1, 24, :_reduce_20,
|
|
128
150
|
2, 24, :_reduce_21,
|
|
129
|
-
|
|
130
|
-
2, 22, :_reduce_23,
|
|
131
|
-
3, 25, :_reduce_24 ]
|
|
151
|
+
3, 25, :_reduce_22 ]
|
|
132
152
|
|
|
133
|
-
racc_reduce_n =
|
|
153
|
+
racc_reduce_n = 23
|
|
134
154
|
|
|
135
|
-
racc_shift_n =
|
|
155
|
+
racc_shift_n = 31
|
|
136
156
|
|
|
137
157
|
racc_token_table = {
|
|
138
158
|
false => 0,
|
|
@@ -193,8 +213,8 @@ Racc_token_to_s_table = [
|
|
|
193
213
|
"element_sequence",
|
|
194
214
|
"element",
|
|
195
215
|
"san_move_annotated",
|
|
196
|
-
"
|
|
197
|
-
"
|
|
216
|
+
"move_suffix",
|
|
217
|
+
"suffix_item",
|
|
198
218
|
"annotation_list",
|
|
199
219
|
"variation" ]
|
|
200
220
|
Ractor.make_shareable(Racc_token_to_s_table) if defined?(Ractor)
|
|
@@ -293,101 +313,80 @@ module_eval(<<'.,.,', 'pgn_parser.y', 50)
|
|
|
293
313
|
|
|
294
314
|
module_eval(<<'.,.,', 'pgn_parser.y', 54)
|
|
295
315
|
def _reduce_12(val, _values, result)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
316
|
+
# A standalone comment (not attached to a move) becomes the game
|
|
317
|
+
# comment; the last such comment in the game wins.
|
|
318
|
+
@game_comment = val[0]
|
|
319
|
+
result = nil
|
|
299
320
|
|
|
300
321
|
result
|
|
301
322
|
end
|
|
302
323
|
.,.,
|
|
303
324
|
|
|
304
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
325
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 61)
|
|
305
326
|
def _reduce_13(val, _values, result)
|
|
306
|
-
|
|
307
|
-
# comment; the last such comment in the game wins.
|
|
308
|
-
@game_comment = val[0]
|
|
309
|
-
result = nil
|
|
310
|
-
|
|
327
|
+
result = MoveText.new(val[0])
|
|
311
328
|
result
|
|
312
329
|
end
|
|
313
330
|
.,.,
|
|
314
331
|
|
|
315
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
332
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 62)
|
|
316
333
|
def _reduce_14(val, _values, result)
|
|
317
|
-
result =
|
|
334
|
+
result = build_move(val[0], val[1])
|
|
318
335
|
result
|
|
319
336
|
end
|
|
320
337
|
.,.,
|
|
321
338
|
|
|
322
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
339
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 70)
|
|
323
340
|
def _reduce_15(val, _values, result)
|
|
324
|
-
result =
|
|
341
|
+
result = [val[0]]
|
|
325
342
|
result
|
|
326
343
|
end
|
|
327
344
|
.,.,
|
|
328
345
|
|
|
329
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
346
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 71)
|
|
330
347
|
def _reduce_16(val, _values, result)
|
|
331
|
-
result = [
|
|
348
|
+
result = val[0] << val[1]
|
|
332
349
|
result
|
|
333
350
|
end
|
|
334
351
|
.,.,
|
|
335
352
|
|
|
336
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
353
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 74)
|
|
337
354
|
def _reduce_17(val, _values, result)
|
|
338
|
-
result = [val[0]
|
|
355
|
+
result = [:comment, val[0]]
|
|
339
356
|
result
|
|
340
357
|
end
|
|
341
358
|
.,.,
|
|
342
359
|
|
|
343
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
360
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 75)
|
|
344
361
|
def _reduce_18(val, _values, result)
|
|
345
|
-
result = [
|
|
362
|
+
result = [:nags, val[0]]
|
|
346
363
|
result
|
|
347
364
|
end
|
|
348
365
|
.,.,
|
|
349
366
|
|
|
350
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
367
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 76)
|
|
351
368
|
def _reduce_19(val, _values, result)
|
|
352
|
-
result = [
|
|
369
|
+
result = [:variation, val[0]]
|
|
353
370
|
result
|
|
354
371
|
end
|
|
355
372
|
.,.,
|
|
356
373
|
|
|
357
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
374
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 79)
|
|
358
375
|
def _reduce_20(val, _values, result)
|
|
359
376
|
result = [val[0]]
|
|
360
377
|
result
|
|
361
378
|
end
|
|
362
379
|
.,.,
|
|
363
380
|
|
|
364
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
381
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 80)
|
|
365
382
|
def _reduce_21(val, _values, result)
|
|
366
383
|
result = val[0] << val[1]
|
|
367
384
|
result
|
|
368
385
|
end
|
|
369
386
|
.,.,
|
|
370
387
|
|
|
371
|
-
module_eval(<<'.,.,', 'pgn_parser.y',
|
|
388
|
+
module_eval(<<'.,.,', 'pgn_parser.y', 83)
|
|
372
389
|
def _reduce_22(val, _values, result)
|
|
373
|
-
result = [val[0]]
|
|
374
|
-
result
|
|
375
|
-
end
|
|
376
|
-
.,.,
|
|
377
|
-
|
|
378
|
-
module_eval(<<'.,.,', 'pgn_parser.y', 85)
|
|
379
|
-
def _reduce_23(val, _values, result)
|
|
380
|
-
# Left-recursive; the legacy variation-order reversal is applied as
|
|
381
|
-
# a single explicit `.reverse` where the list is consumed in
|
|
382
|
-
# {element}, instead of being implicit in recursion direction.
|
|
383
|
-
result = val[0] << val[1]
|
|
384
|
-
|
|
385
|
-
result
|
|
386
|
-
end
|
|
387
|
-
.,.,
|
|
388
|
-
|
|
389
|
-
module_eval(<<'.,.,', 'pgn_parser.y', 92)
|
|
390
|
-
def _reduce_24(val, _values, result)
|
|
391
390
|
result = val[1]
|
|
392
391
|
result
|
|
393
392
|
end
|
data/lib/pgn/pgn_parser.y
CHANGED
|
@@ -50,12 +50,6 @@ rule
|
|
|
50
50
|
element:
|
|
51
51
|
MOVE_NUMBER { result = nil }
|
|
52
52
|
| san_move_annotated
|
|
53
|
-
| san_move_annotated variation_list
|
|
54
|
-
{
|
|
55
|
-
result = val[0]
|
|
56
|
-
result.variations = val[1].reverse
|
|
57
|
-
result
|
|
58
|
-
}
|
|
59
53
|
| COMMENT
|
|
60
54
|
{
|
|
61
55
|
# A standalone comment (not attached to a move) becomes the game
|
|
@@ -66,28 +60,25 @@ rule
|
|
|
66
60
|
|
|
67
61
|
san_move_annotated:
|
|
68
62
|
SAN_MOVE { result = MoveText.new(val[0]) }
|
|
69
|
-
| SAN_MOVE
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
| SAN_MOVE move_suffix { result = build_move(val[0], val[1]) }
|
|
64
|
+
|
|
65
|
+
# The trailing material after a move: any number of comments, NAGs, and
|
|
66
|
+
# variations, in any order (PGN spec §3.2.5). Each suffix item is tagged
|
|
67
|
+
# [:comment, c] | [:nags, a] | [:variation, v]; {build_move} folds them
|
|
68
|
+
# onto the MoveText. This generalizes the old single-comment move_trailer,
|
|
69
|
+
# which rejected two comments before a variation (`{a} {b} (...)`).
|
|
70
|
+
move_suffix:
|
|
71
|
+
suffix_item { result = [val[0]] }
|
|
72
|
+
| move_suffix suffix_item { result = val[0] << val[1] }
|
|
73
|
+
|
|
74
|
+
suffix_item:
|
|
75
|
+
COMMENT { result = [:comment, val[0]] }
|
|
76
|
+
| annotation_list { result = [:nags, val[0]] }
|
|
77
|
+
| variation { result = [:variation, val[0]] }
|
|
77
78
|
|
|
78
79
|
annotation_list:
|
|
79
|
-
NAG
|
|
80
|
-
| annotation_list NAG
|
|
81
|
-
|
|
82
|
-
variation_list:
|
|
83
|
-
variation { result = [val[0]] }
|
|
84
|
-
| variation_list variation
|
|
85
|
-
{
|
|
86
|
-
# Left-recursive; the legacy variation-order reversal is applied as
|
|
87
|
-
# a single explicit `.reverse` where the list is consumed in
|
|
88
|
-
# {element}, instead of being implicit in recursion direction.
|
|
89
|
-
result = val[0] << val[1]
|
|
90
|
-
}
|
|
80
|
+
NAG { result = [val[0]] }
|
|
81
|
+
| annotation_list NAG { result = val[0] << val[1] }
|
|
91
82
|
|
|
92
83
|
variation:
|
|
93
84
|
'(' element_sequence ')' { result = val[1] }
|
|
@@ -113,6 +104,26 @@ rule
|
|
|
113
104
|
|
|
114
105
|
private
|
|
115
106
|
|
|
107
|
+
# Fold a move's trailing suffix (comments, NAGs, variations, in any
|
|
108
|
+
# order) onto a MoveText. Multiple comments are concatenated with a
|
|
109
|
+
# space after each is individually cleaned; NAGs are merged in order;
|
|
110
|
+
# variations are collected in source order and then reversed, preserving
|
|
111
|
+
# the legacy order the serializer and round-trip specs depend on.
|
|
112
|
+
def build_move(notation, suffix)
|
|
113
|
+
comment_parts = []
|
|
114
|
+
nags = nil
|
|
115
|
+
variations = []
|
|
116
|
+
suffix.each do |kind, payload|
|
|
117
|
+
case kind
|
|
118
|
+
when :comment then comment_parts << MoveText.clean_text(payload)
|
|
119
|
+
when :nags then nags = nags ? nags + payload : payload
|
|
120
|
+
when :variation then variations << payload
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
comment = comment_parts.empty? ? nil : comment_parts.join(' ')
|
|
124
|
+
MoveText.new(notation, nags, comment, variations.reverse)
|
|
125
|
+
end
|
|
126
|
+
|
|
116
127
|
# Punctuation tokens are racc'd by their literal character (taken from
|
|
117
128
|
# PGN::Lexer::LITERAL_BYTES, the single source of truth for those
|
|
118
129
|
# characters); everything else is racc'd by the upcased lexer symbol.
|
data/lib/pgn/version.rb
CHANGED
data/lib/pgn.rb
CHANGED
|
@@ -26,6 +26,10 @@ module PGN
|
|
|
26
26
|
# @see http://www.chessclub.com/help/PGN-spec PGN Specification
|
|
27
27
|
#
|
|
28
28
|
def self.parse(pgn, encoding = Encoding::ISO_8859_1)
|
|
29
|
+
# Operate on a private copy so a frozen caller string is never mutated
|
|
30
|
+
# (force_encoding mutates its receiver; under frozen-string literals
|
|
31
|
+
# this would otherwise raise FrozenError on the target Ruby).
|
|
32
|
+
pgn = pgn.dup
|
|
29
33
|
pgn.force_encoding(encoding) if encoding
|
|
30
34
|
|
|
31
35
|
PGN::Parser.new.parse(pgn).map do |game|
|
|
@@ -176,12 +176,51 @@ RSpec.describe PGN, '.parse (racc parser)' do
|
|
|
176
176
|
end
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
+
describe 'move suffix (comments, NAGs, variations in any order)' do
|
|
180
|
+
it 'attaches two comments before a variation to the move' do
|
|
181
|
+
game = PGN.parse('[White "A"] 1. Bxh7+ Kxh7 {a} {b} (20... Kf8) 2. Ng5+ *').first
|
|
182
|
+
move = game.moves[1]
|
|
183
|
+
expect(move.notation).to eq('Kxh7')
|
|
184
|
+
expect(move.comment).to eq('a b')
|
|
185
|
+
expect(variations_as(move)).to eq([%w[Kf8]])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'attaches a comment after a variation to the same move' do
|
|
189
|
+
game = PGN.parse('[White "A"] 1. e4 (1... c5) {a} (2... f5) 2. Nf3 *').first
|
|
190
|
+
move = game.moves.first
|
|
191
|
+
expect(move.comment).to eq('a')
|
|
192
|
+
expect(variations_as(move).sort).to eq([%w[c5], %w[f5]].sort)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'merges an arbitrary mix of comments, NAGs, and variations' do
|
|
196
|
+
game = PGN.parse('[White "A"] 1. e4 {c1} $1 (1... d5) {c2} $2 *').first
|
|
197
|
+
move = game.moves.first
|
|
198
|
+
expect(move.comment).to eq('c1 c2')
|
|
199
|
+
expect(move.annotation).to eq(%w[$1 $2])
|
|
200
|
+
expect(variations_as(move)).to eq([%w[d5]])
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'round-trips a multi-comment move through parse -> serialize -> parse' do
|
|
204
|
+
input = +"[White \"A\"]\n\n1. e4 {a} {b} (1... d5) *\n"
|
|
205
|
+
once = PGN.parse(input, Encoding::UTF_8).first.to_pgn
|
|
206
|
+
twice = PGN.parse(once, Encoding::UTF_8).first.to_pgn
|
|
207
|
+
expect(twice).to eq(once)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
179
211
|
describe 'pgn (verbatim raw text)' do
|
|
180
212
|
it 'returns the verbatim text for a single game' do
|
|
181
213
|
input = "[White \"A\"] 1. e4 1-0\n"
|
|
182
214
|
expect(PGN.parse(input).first.pgn).to eq(input)
|
|
183
215
|
end
|
|
184
216
|
|
|
217
|
+
it 'does not mutate a frozen input string' do
|
|
218
|
+
input = "[White \"A\"] 1. e4 1-0\n".freeze
|
|
219
|
+
expect { PGN.parse(input, Encoding::UTF_8) }.not_to raise_error
|
|
220
|
+
expect(input.encoding).to eq(Encoding::UTF_8)
|
|
221
|
+
expect(PGN.parse(input, Encoding::UTF_8).first.pgn).to eq(input)
|
|
222
|
+
end
|
|
223
|
+
|
|
185
224
|
it 'partitions consecutive games contiguously' do
|
|
186
225
|
input = "[White \"A\"] 1. e4 1-0\n\n[White \"B\"] 1. d4 0-1\n"
|
|
187
226
|
games = PGN.parse(input)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgn2
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stacey Touset
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-08-
|
|
12
|
+
date: 2026-08-20 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rb_sys
|