package parsekit import ( "fmt" "io" "strings" ) // ParseAPI holds the internal state of a parse run and provides an API to // ParseHandler methods to communicate with the parser. type ParseAPI struct { tokenAPI *TokenAPI // the input reader loopCheck map[string]bool // used for parser loop detection expecting string // a description of what the current state expects to find (see Expects()) result *TokenResult // Last TokenHandler result as retrieved by On(...).Accept() err *Error // error during parsing, retrieved by Error(), further ParseAPI calls are ignored stopped bool // a boolean set to true by Stop(), further ParseAPI calls are ignored } // panicWhenStoppedOrInError will panic when the parser has produced an error // or when it has been stopped. It is used from the ParseAPI methods, to // prevent further calls to the ParseAPI on these occasions. // // Basically, this guard ensures proper coding of parsers, making sure // that clean routes are followed. You can consider this check a runtime // unit test. func (p *ParseAPI) panicWhenStoppedOrInError() { if !p.isStoppedOrInError() { return } called, _ := getCaller(1) parts := strings.Split(called, ".") calledShort := parts[len(parts)-1] _, filepos := getCaller(2) after := "Error()" if p.stopped { after = "Stop()" } panic(fmt.Sprintf( "parsekit.ParseAPI.%s(): Illegal call to %s() at %s: "+ "no calls allowed after ParseAPI.%s", calledShort, calledShort, filepos, after)) } func (p *ParseAPI) isStoppedOrInError() bool { return p.stopped || p.err != nil } func (p *ParseAPI) initLoopCheck() { p.loopCheck = map[string]bool{} } func (p *ParseAPI) checkForLoops() { _, filepos := getCaller(2) if _, ok := p.loopCheck[filepos]; ok { panic(fmt.Sprintf("parsekit.ParseAPI: Loop detected in parser at %s", filepos)) } p.loopCheck[filepos] = true } // On checks if the input at the current cursor position matches the provided // TokenHandler. On must be chained with another method that tells the parser // what action to perform when a match was found: // // 1) On(...).Skip() - Only move cursor forward, ignore the matched runes. // // 2) On(...).Accept() - Move cursor forward, add runes to parsers's string buffer. // // 3) On(...).Stay() - Do nothing, the cursor stays at the same position. // // So an example chain could look like this: // // p.On(parsekit.A.Whitespace).Skip() // // The chain as a whole returns a boolean that indicates whether or not at match // was found. When no match was found, false is returned and Skip() and Accept() // will have no effect. Because of this, typical use of an On() chain is as // expression for a conditional statement (if, switch/case, for). E.g.: // // // Skip multiple exclamation marks. // for p.On(parsekit.A.Excl).Skip() { } // // // Fork a route based on the input. // switch { // case p.On(parsekit.A.Excl).Stay() // p.RouteTo(stateHandlerA) // case p.On(parsekit.A.Colon).Stay(): // p.RouteTo(stateHandlerB) // default: // p.RouteTo(stateHandlerC) // } // // // When there's a "hi" on input, then say hello. // if p.On(parsekit.C.Str("hi")).Accept() { // fmt.Println("Hello!") // } func (p *ParseAPI) On(tokenHandler TokenHandler) *ParseAPIOnAction { p.panicWhenStoppedOrInError() p.checkForLoops() if tokenHandler == nil { _, filepos := getCaller(1) panic(fmt.Sprintf( "parsekit.ParseAPI.On(): On() called with nil "+ "tokenHandler argument at %s", filepos)) } p.result = nil p.tokenAPI.result = newTokenResult() fork := p.tokenAPI.Fork() ok := tokenHandler(fork) return &ParseAPIOnAction{ parseAPI: p, tokenAPI: fork, ok: ok, } } // ParseAPIOnAction is a struct that is used for building the On()-method chain. // The On() method will return an initialized struct of this type. type ParseAPIOnAction struct { parseAPI *ParseAPI tokenAPI *TokenAPI ok bool } // Accept tells the parser to move the cursor past a match that was found, // and to make the TokenResult from the TokenAPI available in the ParseAPI // through the Result() method. // // Returns true in case a match was found. // When no match was found, then no action is taken and false is returned. func (a *ParseAPIOnAction) Accept() bool { if a.ok { a.tokenAPI.Merge() a.parseAPI.result = a.tokenAPI.root.result a.flushTokenAPI() a.flushReader() //a.flush() } return a.ok } // Skip tells the parser to move the cursor past a match that was found, // without making the results available through the ParseAPI. // // Note that functionally, you could call Accept() just as well, simply // ignoring the results. However, the Skip() call is a bit more efficient // than the Accept() call and (more important if you ask me) the code // expresses more clearly that your intent is to skip the match. // // Returns true in case a match was found. // When no match was found, then no action is taken and false is returned. func (a *ParseAPIOnAction) Skip() bool { if a.ok { a.tokenAPI.root.cursor = a.tokenAPI.cursor a.parseAPI.result = nil a.flushTokenAPI() a.flushReader() } return a.ok } // Stay tells the parser to not move the cursor after finding a match. // // A typical use of Stay() is to let one ParseHandler detect the start // of some kind of token, but without moving the read cursor forward. // When a match is found, it hands off control to another ParseHandler // to take care of the actual token parsing. // // Returns true in case a match was found, false otherwise. func (a *ParseAPIOnAction) Stay() bool { if a.ok { a.parseAPI.result = nil a.flushTokenAPI() } return a.ok } func (a *ParseAPIOnAction) flushTokenAPI() { a.tokenAPI.root.result = newTokenResult() a.tokenAPI.root.detachChilds() } func (a *ParseAPIOnAction) flushReader() { if a.tokenAPI.offset > 0 { a.tokenAPI.root.reader.flush(a.tokenAPI.offset) a.tokenAPI.root.offset = 0 a.parseAPI.initLoopCheck() } } // Result returns a TokenResult struct, containing results as produced by the // last ParseAPI.On().Accept() call. func (p *ParseAPI) Result() *TokenResult { result := p.result if p.result == nil { _, filepos := getCaller(1) panic(fmt.Sprintf( "parsekit.ParseAPI.TokenResult(): TokenResult() called at %s without "+ "calling ParseAPI.Accept() on beforehand", filepos)) } return result } // Handle is used to execute other ParseHandler functions from within your // ParseHandler function. // // The boolean return value is true when the parser can still continue. // It will be false when either an error was set (using ParseAPI.Error()), // or the parser was stopped (using ParseAPI.Stop()). func (p *ParseAPI) Handle(parseHandler ParseHandler) bool { p.panicWhenStoppedOrInError() p.panicWhenParseHandlerNil(parseHandler) parseHandler(p) return !p.isStoppedOrInError() } func (p *ParseAPI) panicWhenParseHandlerNil(parseHandler ParseHandler) { if parseHandler == nil { _, filepos := getCaller(2) panic(fmt.Sprintf("parsekit.ParseAPI.Handle(): Handle() called with nil input at %s", filepos)) } } // Expects is used to let a ParseHandler function describe what input it is // expecting. This expectation is used in error messages to provide some // context to them. // // When defining an expectation inside a ParseHandler, you do not need to // handle unexpected input yourself. When the end of the parser is reached // without stopping it using ParseAPI.Stop() or ParseAPI.ExpectEndOfFile(), // an automatic error will be emitted using ParseAPI.UnexpectedInput(). func (p *ParseAPI) Expects(description string) { p.panicWhenStoppedOrInError() p.expecting = description } // Stop is used by the parser impementation to tell the ParseAPI that it has // completed the parsing process successfully. // // When the parser implementation returns without stopping first (and // without running into an error), the Parser.Execute() will call // ParserAPI.ExpectEndOfFile() to check if the end of the file was reached. // If not, then things will end in an UnexpectedError(). // Even though this fallback mechanism will work in a lot of cases, try to make // your parser explicit about things and call Stop() actively yourself. // // After stopping, no more calls to ParseAPI methods are allowed. // Calling a method in this state will result in a panic. func (p *ParseAPI) Stop() { p.stopped = true } // Error sets the error message in the ParseAPI. // // After setting an error, no more calls to ParseAPI methods are allowed. // Calling a method in this state will result in a panic. func (p *ParseAPI) Error(format string, args ...interface{}) { // No call to p.panicWhenStoppedOrInError(), to allow a parser to // set a different error message when needed. message := fmt.Sprintf(format, args...) p.err = &Error{message, p.tokenAPI.Cursor()} } // ExpectEndOfFile can be used to check if the input is at end of file. // // When it finds that the end of the file was indeed reached, then the // parser will be stopped through ParseAPI.Stop(). Otherwise unexpected // input is reported through ParseAPI.UnexpectedInput() with "end of file" // as the expectation. func (p *ParseAPI) ExpectEndOfFile() { p.panicWhenStoppedOrInError() if p.On(A.EndOfFile).Stay() { p.Stop() } else { p.Expects("end of file") p.UnexpectedInput() } } // UnexpectedInput is used to set an error that tells the user that some // unexpected input was encountered. // // It can automatically produce an error message for a couple of situations: // 1) input simply didn't match the expectation // 2) the end of the input was reached // 3) there was an invalid UTF8 character on the input. // // The parser implementation can provide some feedback for this error by // calling ParseAPI.Expects() to set the expectation. When set, the // expectation is included in the error message. func (p *ParseAPI) UnexpectedInput() { p.panicWhenStoppedOrInError() r, err := p.tokenAPI.NextRune() switch { case err == nil: p.Error("unexpected character %q%s", r, fmtExpects(p)) case err == io.EOF: p.Error("unexpected end of file%s", fmtExpects(p)) default: p.Error("unexpected error '%s'%s", err, fmtExpects(p)) } } func fmtExpects(p *ParseAPI) string { if p.expecting == "" { return "" } return fmt.Sprintf(" (expected %s)", p.expecting) }