279 lines
9.1 KiB
Go
279 lines
9.1 KiB
Go
package parsekit
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// 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
|
|
result *TokenHandlerResult // Last TokenHandler result as produced 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 := callerFunc(1)
|
|
|
|
after := "Error()"
|
|
if p.stopped {
|
|
after = "Stop()"
|
|
}
|
|
|
|
callerPanic(2, "parsekit.ParseAPI.%s(): Illegal call to %s() at {caller}: "+
|
|
"no calls allowed after ParseAPI.%s", called, called, 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 := callerFilepos(2)
|
|
if _, ok := p.loopCheck[filepos]; ok {
|
|
callerPanic(2, "parsekit.ParseAPI: Loop detected in parser at {caller}")
|
|
}
|
|
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() - Move read cursor forward, ignoring the match results.
|
|
//
|
|
// 2) On(...).Accept() - Move cursor, making results available through Result()
|
|
//
|
|
// 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)
|
|
// }
|
|
//
|
|
// // Echo back a sequence of digits on the input.
|
|
// if p.On(parsekit.A.Digits).Accept() {
|
|
// fmt.Println(p.Result().String())
|
|
// }
|
|
func (p *ParseAPI) On(tokenHandler TokenHandler) *ParseAPIOnAction {
|
|
p.panicWhenStoppedOrInError()
|
|
p.checkForLoops()
|
|
if tokenHandler == nil {
|
|
callerPanic(1, "parsekit.ParseAPI.On(): On() called with nil tokenHandler argument at {caller}")
|
|
}
|
|
|
|
p.result = nil
|
|
p.tokenAPI.clearResults()
|
|
child := p.tokenAPI.Fork()
|
|
ok := tokenHandler(child)
|
|
|
|
return &ParseAPIOnAction{
|
|
parseAPI: p,
|
|
tokenAPI: child,
|
|
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 read cursor past a match that was
|
|
// found, and to make the TokenHandlerResult from the TokenAPI available in the
|
|
// ParseAPI through the ParseAPI.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.flushReader()
|
|
a.parseAPI.result = a.tokenAPI.root.result
|
|
}
|
|
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.parseAPI.result = nil
|
|
a.tokenAPI.clearResults()
|
|
a.tokenAPI.syncCursorTo(a.tokenAPI.root)
|
|
a.tokenAPI.detachChilds()
|
|
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.tokenAPI.clearResults()
|
|
a.tokenAPI.detachChilds()
|
|
}
|
|
return a.ok
|
|
}
|
|
|
|
func (a *ParseAPIOnAction) flushReader() {
|
|
if a.tokenAPI.result.offset > 0 {
|
|
a.tokenAPI.root.reader.flush(a.tokenAPI.root.result.offset)
|
|
a.tokenAPI.root.result.offset = 0
|
|
a.parseAPI.initLoopCheck()
|
|
}
|
|
}
|
|
|
|
// Result returns a TokenHandlerResult struct, containing results as produced by the
|
|
// last ParseAPI.On().Accept() call.
|
|
func (p *ParseAPI) Result() *TokenHandlerResult {
|
|
result := p.result
|
|
if p.result == nil {
|
|
callerPanic(1, "parsekit.ParseAPI.TokenHandlerResult(): TokenHandlerResult() called "+
|
|
"at {caller} without calling ParseAPI.Accept() on beforehand")
|
|
}
|
|
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 {
|
|
callerPanic(2, "parsekit.ParseAPI.Handle(): Handle() called with nil input at {caller}")
|
|
}
|
|
}
|
|
|
|
// 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.result.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.UnexpectedInput("end of file")
|
|
}
|
|
}
|
|
|
|
// UnexpectedInput is used to set an error that tells the user that some
|
|
// unexpected input was encountered.
|
|
//
|
|
// It automatically produces an error message for a couple of situations:
|
|
// 1) the input simply didn't match the expectation
|
|
// 2) the end of the input was reached
|
|
// 3) there was an error while reading the input.
|
|
func (p *ParseAPI) UnexpectedInput(expected string) {
|
|
p.panicWhenStoppedOrInError()
|
|
_, err := p.tokenAPI.NextRune()
|
|
switch {
|
|
case err == nil:
|
|
p.Error("unexpected input%s", fmtExpects(expected))
|
|
case err == io.EOF:
|
|
p.Error("unexpected end of file%s", fmtExpects(expected))
|
|
default:
|
|
p.Error("unexpected error '%s'%s", err, fmtExpects(expected))
|
|
}
|
|
}
|
|
|
|
func fmtExpects(expected string) string {
|
|
if expected == "" {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf(" (expected %s)", expected)
|
|
}
|