package pgn import ( "bufio" "fmt" "strings" "code.c-base.org/gochess/libchess/pkg/game" ) // PoolParsers defines how may parsers are prenitialized. const PoolParsers = 8 // ParseFn defines the signature of a parser function. type ParseFn func(*Parser) ParseFn // Parser implements a PGN parser. type Parser struct { lexer *Lexer errors chan error games chan *Game game *Game tokenBuf *Token useBuf bool } var parserFactory = make(chan *Parser, PoolParsers) func init() { go func() { for { parserFactory <- &Parser{ errors: make(chan error), games: make(chan *Game), game: newGame(), } } }() } // NewParser returns an initialized parser func NewParser(input *bufio.Reader) *Parser { p := <-parserFactory p.lexer = NewLexer(input) go p.run() return p } func (p *Parser) run() { defer close(p.errors) defer close(p.games) for fn := parseTagSection; fn != nil; { fn = fn(p) } } // Next returns the next parsed game from the input stream or an error. func (p *Parser) Next() (*Game, error) { select { case err := <-p.errors: return nil, err case g := <-p.games: return g, nil } } func (p *Parser) next() (*Token, error) { if p.useBuf { p.useBuf = false return p.tokenBuf, nil } t, err := p.lexer.Next() if err != nil { return nil, err } p.tokenBuf = t return t, nil } func (p *Parser) undo() { p.useBuf = true } func (p *Parser) throwUnexpected(t *Token) { p.errors <- fmt.Errorf( "parsing error: unexpected token in line %d at %d: %q", t.Line, t.Col, t.Value, ) } func throwUnexpectedEOF(p *Parser) ParseFn { p.errors <- fmt.Errorf( "parsing error: unexpected EOF", ) return nil } func (p *Parser) emit() { p.games <- p.game p.game = newGame() } func parseTagSection(p *Parser) ParseFn { for { // grab next token t, err := p.next() // bail out on error if err != nil { p.errors <- err return nil } // handle for EOF if t == nil || t.Type == TokenEOF { p.emit() return nil } switch t.Type { case TokenNewline, TokenWhitespace: // noop case TokenBracketLeft: return parseTag case TokenSymbol: p.undo() return parseMovetext default: p.throwUnexpected(t) return nil } } } func parseTag(p *Parser) ParseFn { tag := game.Tag{} findSymbol: for { t, err := p.next() if err != nil { p.errors <- err return nil } if t == nil || t.Type == TokenEOF { return throwUnexpectedEOF } switch t.Type { case TokenNewline, TokenWhitespace, TokenComment: // noop case TokenSymbol: tag.Key = t.Value break findSymbol default: p.throwUnexpected(t) return nil } } findValue: for { t, err := p.next() if err != nil { p.errors <- err return nil } if t == nil || t.Type == TokenEOF { return throwUnexpectedEOF } switch t.Type { case TokenNewline, TokenWhitespace, TokenComment: // noop case TokenString: tag.Value = t.Value break findValue default: p.throwUnexpected(t) return nil } } for { t, err := p.next() if err != nil { p.errors <- err return nil } if t == nil || t.Type == TokenEOF { return throwUnexpectedEOF } switch t.Type { case TokenNewline, TokenWhitespace, TokenComment: // noop case TokenBracketRight: p.game.Tags = append(p.game.Tags, tag) return parseTagSection default: p.throwUnexpected(t) return nil } } } func parseMovetext(p *Parser) ParseFn { isTermination := func(s string) bool { switch s { case "0-1", "1-0", "1/2", "1/2-1/2", "*": return true default: return false } } for { t, err := p.next() if err != nil { p.errors <- err return nil } if t == nil || t.Type == TokenEOF { p.undo() return parseTagSection } switch t.Type { case TokenNewline, TokenWhitespace, TokenComment: // noop case TokenSymbol: if strings.Contains(t.Value, ".") { continue } if !isTermination(t.Value) { p.game.Moves = append(p.game.Moves, t.Value) } case TokenBracketLeft: p.emit() p.undo() return parseTagSection default: p.throwUnexpected(t) return nil } } }