From f245c6b231825ecc996e193a0888de32aa098365 Mon Sep 17 00:00:00 2001 From: NSStudent Date: Fri, 15 May 2026 19:05:56 +0200 Subject: [PATCH] Fix mixed PGN comment annotations --- .../MultipleCommentParser.swift | 47 ++++++--- .../PGN/CommentListParserTests.swift | 97 +++++++++++++++++++ 2 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 Tests/FischerCoreTests/PGN/CommentListParserTests.swift diff --git a/Sources/FischerCore/PGN/Parsers/CommentParsers/MultipleCommentParser.swift b/Sources/FischerCore/PGN/Parsers/CommentParsers/MultipleCommentParser.swift index f882c14f..e3705314 100644 --- a/Sources/FischerCore/PGN/Parsers/CommentParsers/MultipleCommentParser.swift +++ b/Sources/FischerCore/PGN/Parsers/CommentParsers/MultipleCommentParser.swift @@ -7,24 +7,43 @@ import Parsing +private struct SpecialCommentParser: Parser { + var body: some Parser { + OneOf { + CALParser() + CSLParser() + EMTParser() + EvalParser() + CLKParser() + } + } +} + struct MultipleCommentParser: Parser { var body: some Parser { - "{" - Whitespace() - Many { - OneOf{ - CALParser() - CSLParser() - EMTParser() - EvalParser() - CLKParser() - } - } separator: { - OneOf { + Parse { + "{" + Whitespace() + SpecialCommentParser() + Many { + SpecialCommentParser() + } separator: { Whitespace() } + Optionally { + Whitespace(1...) + Prefix { $0 != "}" } + .map(String.init) + } + Whitespace() + "}" + }.map { firstComment, remainingComments, text in + var comments = [firstComment] + comments.append(contentsOf: remainingComments) + if let text, !text.isEmpty { + comments.append(.text(text)) + } + return comments } - Whitespace() - "}" } } diff --git a/Tests/FischerCoreTests/PGN/CommentListParserTests.swift b/Tests/FischerCoreTests/PGN/CommentListParserTests.swift new file mode 100644 index 00000000..ff0da8e3 --- /dev/null +++ b/Tests/FischerCoreTests/PGN/CommentListParserTests.swift @@ -0,0 +1,97 @@ +// +// CommentListParserTests.swift +// FischerCore +// +// Created by Codex on 15/5/26. +// + +import Parsing +import Testing +@testable import FischerCore + +struct CommentListParserTests { + @Test("Parses adjacent CSL and CAL annotations followed by text") + func parsesAdjacentSquareAndArrowAnnotationsWithText() throws { + let comments = try CommentListParser().parse( + "{[%csl Rh7][%cal Gd3h7,Gh2h7] amenazando mate!}" + ) + + #expect(comments == [ + .squareList([ + PGNSquare(color: .red, square: .h7), + ]), + .arrowList([ + PGNArrow(color: .green, fromSquare: .d3, toSquare: .h7), + PGNArrow(color: .green, fromSquare: .h2, toSquare: .h7), + ]), + .text("amenazando mate!"), + ]) + } + + @Test("Parses adjacent annotations with multiple arrows and long text") + func parsesAdjacentAnnotationsWithMultipleArrowsAndLongText() throws { + let comments = try CommentListParser().parse( + "{[%csl Gg3][%cal Gd1h1,Gg4g5,Rf4h6,Gc3e2,Ge2g3] con una ventaja clara. Plan: Th1-Ce2 y Cg3-g5.}" + ) + + #expect(comments == [ + .squareList([ + PGNSquare(color: .green, square: .g3), + ]), + .arrowList([ + PGNArrow(color: .green, fromSquare: .d1, toSquare: .h1), + PGNArrow(color: .green, fromSquare: .g4, toSquare: .g5), + PGNArrow(color: .red, fromSquare: .f4, toSquare: .h6), + PGNArrow(color: .green, fromSquare: .c3, toSquare: .e2), + PGNArrow(color: .green, fromSquare: .e2, toSquare: .g3), + ]), + .text("con una ventaja clara. Plan: Th1-Ce2 y Cg3-g5."), + ]) + } + + @Test("Preserves adjacent CSL and CAL annotations without text") + func parsesAdjacentSquareAndArrowAnnotationsWithoutText() throws { + let comments = try CommentListParser().parse( + "{ [%csl Gf6][%cal Bg8f6,Yd7f6] }" + ) + + #expect(comments == [ + .squareList([ + PGNSquare(color: .green, square: .f6), + ]), + .arrowList([ + PGNArrow(color: .blue, fromSquare: .g8, toSquare: .f6), + PGNArrow(color: .yellow, fromSquare: .d7, toSquare: .f6), + ]), + ]) + } + + @Test("PGN game parser keeps adjacent annotations and text in move comments") + func pgnGameParserKeepsAdjacentAnnotationsAndTextInMoveComments() throws { + let input = + """ + [Event "Mixed comment annotations"] + [Site "Local"] + [Date "2026.05.15"] + [Round "-"] + [White "-"] + [Black "-"] + [Result "*"] + + 1. e4 {[%csl Rh7][%cal Gd3h7,Gh2h7] amenazando mate!} * + """ + + let game = try PGNGameParser().parse(input) + + #expect(game.elements[0].postWhiteCommentList == [ + .squareList([ + PGNSquare(color: .red, square: .h7), + ]), + .arrowList([ + PGNArrow(color: .green, fromSquare: .d3, toSquare: .h7), + PGNArrow(color: .green, fromSquare: .h2, toSquare: .h7), + ]), + .text("amenazando mate!"), + ]) + } +}