242 lines
4.0 KiB
Go
242 lines
4.0 KiB
Go
|
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
|
||
|
}
|
||
|
}
|
||
|
}
|