Initial commit
This commit is contained in:
commit
68cde88125
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.idea
|
||||
*.iml
|
114
combinator.go
Normal file
114
combinator.go
Normal file
@ -0,0 +1,114 @@
|
||||
package parsec
|
||||
|
||||
func Nil(p Pointer) (Node, Pointer) {
|
||||
return nil, p
|
||||
}
|
||||
|
||||
func Never(p Pointer) (Node, Pointer) {
|
||||
return Error{p.pos, "Never matches"}, p
|
||||
}
|
||||
|
||||
func And(parsers ...Parserish) Parser {
|
||||
if len(parsers) == 0 {
|
||||
return Nil
|
||||
}
|
||||
|
||||
ps := ParsifyAll(parsers...)
|
||||
|
||||
return func(p Pointer) (Node, Pointer) {
|
||||
var nodes = make([]Node, 0, len(ps))
|
||||
var node Node
|
||||
newP := p
|
||||
for _, parser := range ps {
|
||||
node, newP = parser(newP)
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
if IsError(node) {
|
||||
return node, p
|
||||
}
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
return NewSequence(p.pos, nodes...), newP
|
||||
}
|
||||
}
|
||||
|
||||
func Any(parsers ...Parserish) Parser {
|
||||
if len(parsers) == 0 {
|
||||
return Nil
|
||||
}
|
||||
|
||||
ps := ParsifyAll(parsers...)
|
||||
|
||||
return func(p Pointer) (Node, Pointer) {
|
||||
errors := []Error{}
|
||||
for _, parser := range ps {
|
||||
node, newP := parser(p)
|
||||
if err, isErr := node.(Error); isErr {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
return node, newP
|
||||
}
|
||||
|
||||
longestError := errors[0]
|
||||
for _, e := range errors[1:] {
|
||||
if e.pos > longestError.pos {
|
||||
longestError = e
|
||||
}
|
||||
}
|
||||
|
||||
return longestError, p
|
||||
}
|
||||
}
|
||||
|
||||
func Kleene(opScan Parserish, sepScan ...Parserish) Parser {
|
||||
return manyImpl(0, opScan, Never, sepScan...)
|
||||
}
|
||||
|
||||
func KleeneUntil(opScan Parserish, untilScan Parserish, sepScan ...Parserish) Parser {
|
||||
return manyImpl(0, opScan, untilScan, sepScan...)
|
||||
}
|
||||
|
||||
func Many(opScan Parserish, sepScan ...Parserish) Parser {
|
||||
return manyImpl(1, opScan, Never, sepScan...)
|
||||
}
|
||||
|
||||
func ManyUntil(opScan Parserish, untilScan Parserish, sepScan ...Parserish) Parser {
|
||||
return manyImpl(1, opScan, untilScan, sepScan...)
|
||||
}
|
||||
|
||||
func manyImpl(min int, op Parserish, until Parserish, sep ...Parserish) Parser {
|
||||
opParser := Parsify(op)
|
||||
untilParser := Parsify(until)
|
||||
sepParser := Nil
|
||||
if len(sep) > 0 {
|
||||
sepParser = Parsify(sep[0])
|
||||
}
|
||||
|
||||
return func(p Pointer) (Node, Pointer) {
|
||||
var node Node
|
||||
nodes := make([]Node, 0)
|
||||
newP := p
|
||||
for {
|
||||
if node, _ := untilParser(newP); !IsError(node) {
|
||||
if len(nodes) < min {
|
||||
return NewError(newP.pos, "Unexpected input"), p
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if node, newP = opParser(newP); IsError(node) {
|
||||
if len(nodes) < min {
|
||||
return node, p
|
||||
}
|
||||
break
|
||||
}
|
||||
nodes = append(nodes, node)
|
||||
if node, newP = sepParser(newP); IsError(node) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return NewSequence(p.pos, nodes...), newP
|
||||
}
|
||||
}
|
215
combinator_test.go
Normal file
215
combinator_test.go
Normal file
@ -0,0 +1,215 @@
|
||||
package parsec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNil(t *testing.T) {
|
||||
p := Pointer{"hello world", 0}
|
||||
|
||||
node, p2 := Nil(p)
|
||||
require.Equal(t, nil, node)
|
||||
require.Equal(t, p, p2)
|
||||
}
|
||||
|
||||
func TestAnd(t *testing.T) {
|
||||
p := Pointer{"hello world", 0}
|
||||
|
||||
t.Run("matches sequence", func(t *testing.T) {
|
||||
node, p2 := And("hello", WS, "world")(p)
|
||||
require.Equal(t, NewSequence(0, NewToken(0, "hello"), NewToken(6, "world")), node)
|
||||
require.Equal(t, 0, p2.Remaining())
|
||||
})
|
||||
|
||||
t.Run("returns errors", func(t *testing.T) {
|
||||
e, p3 := And("hello", WS, "there")(p)
|
||||
require.Equal(t, NewError(6, "Expected there"), e)
|
||||
require.Equal(t, 0, p3.pos)
|
||||
})
|
||||
|
||||
t.Run("No parsers", func(t *testing.T) {
|
||||
assertNilParser(t, And())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAny(t *testing.T) {
|
||||
p := Pointer{"hello world!", 0}
|
||||
|
||||
t.Run("Matches any", func(t *testing.T) {
|
||||
node, p2 := Any("hello", "world")(p)
|
||||
require.Equal(t, NewToken(0, "hello"), node)
|
||||
require.Equal(t, 5, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("Returns longest error", func(t *testing.T) {
|
||||
err, p2 := Any(
|
||||
Exact("nope"),
|
||||
And(Exact("hello"), WS, Exact("world"), Exact(".")),
|
||||
And(Exact("hello"), WS, Exact("brother")),
|
||||
)(p)
|
||||
require.Equal(t, NewError(11, "Expected ."), err)
|
||||
require.Equal(t, 0, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("Accepts nil matches", func(t *testing.T) {
|
||||
node, p2 := Any(Exact("ffffff"), WS)(p)
|
||||
require.Equal(t, nil, node)
|
||||
require.Equal(t, 0, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("No parsers", func(t *testing.T) {
|
||||
assertNilParser(t, Any())
|
||||
})
|
||||
}
|
||||
|
||||
func TestKleene(t *testing.T) {
|
||||
p := Pointer{"a,b,c,d,e,", 0}
|
||||
|
||||
t.Run("Matches sequence with sep", func(t *testing.T) {
|
||||
node, p2 := Kleene(CharRun("abcdefg"), Exact(","))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(2, "b"),
|
||||
NewToken(4, "c"),
|
||||
NewToken(6, "d"),
|
||||
NewToken(8, "e"),
|
||||
), node)
|
||||
require.Equal(t, 10, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("Matches sequence without sep", func(t *testing.T) {
|
||||
node, p2 := Kleene(Any(CharRun("abcdefg"), Exact(",")))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(1, ","),
|
||||
NewToken(2, "b"),
|
||||
NewToken(3, ","),
|
||||
NewToken(4, "c"),
|
||||
NewToken(5, ","),
|
||||
NewToken(6, "d"),
|
||||
NewToken(7, ","),
|
||||
NewToken(8, "e"),
|
||||
NewToken(9, ","),
|
||||
), node)
|
||||
require.Equal(t, 10, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("Stops on error", func(t *testing.T) {
|
||||
node, p2 := Kleene(CharRun("abc"), Exact(","))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(2, "b"),
|
||||
NewToken(4, "c"),
|
||||
), node)
|
||||
require.Equal(t, 6, p2.pos)
|
||||
require.Equal(t, "d,e,", p2.Get())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMany(t *testing.T) {
|
||||
p := Pointer{"a,b,c,d,e,", 0}
|
||||
|
||||
t.Run("Matches sequence with sep", func(t *testing.T) {
|
||||
node, p2 := Many(CharRun("abcdefg"), Exact(","))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(2, "b"),
|
||||
NewToken(4, "c"),
|
||||
NewToken(6, "d"),
|
||||
NewToken(8, "e"),
|
||||
), node)
|
||||
require.Equal(t, 10, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("Matches sequence without sep", func(t *testing.T) {
|
||||
node, p2 := Many(Any(CharRun("abcdefg"), Exact(",")))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(1, ","),
|
||||
NewToken(2, "b"),
|
||||
NewToken(3, ","),
|
||||
NewToken(4, "c"),
|
||||
NewToken(5, ","),
|
||||
NewToken(6, "d"),
|
||||
NewToken(7, ","),
|
||||
NewToken(8, "e"),
|
||||
NewToken(9, ","),
|
||||
), node)
|
||||
require.Equal(t, 10, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("Stops on error", func(t *testing.T) {
|
||||
node, p2 := Many(CharRun("abc"), Exact(","))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(2, "b"),
|
||||
NewToken(4, "c"),
|
||||
), node)
|
||||
require.Equal(t, 6, p2.pos)
|
||||
require.Equal(t, "d,e,", p2.Get())
|
||||
})
|
||||
|
||||
t.Run("Returns error if nothing matches", func(t *testing.T) {
|
||||
node, p2 := Many(CharRun("def"), Exact(","))(p)
|
||||
require.Equal(t, NewError(0, "Expected some of def"), node)
|
||||
require.Equal(t, 0, p2.pos)
|
||||
require.Equal(t, "a,b,c,d,e,", p2.Get())
|
||||
})
|
||||
}
|
||||
|
||||
func TestKleeneUntil(t *testing.T) {
|
||||
p := Pointer{"a,b,c,d,e,fg", 0}
|
||||
|
||||
t.Run("Matches sequence with sep", func(t *testing.T) {
|
||||
node, p2 := KleeneUntil(CharRun("abcde"), CharRun("d"), Exact(","))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(2, "b"),
|
||||
NewToken(4, "c"),
|
||||
), node)
|
||||
require.Equal(t, 6, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("Breaks if separator does not match", func(t *testing.T) {
|
||||
node, p2 := KleeneUntil(Char("abcdefg"), Char("y"), Exact(","))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(2, "b"),
|
||||
NewToken(4, "c"),
|
||||
NewToken(6, "d"),
|
||||
NewToken(8, "e"),
|
||||
NewToken(10, "f"),
|
||||
), node)
|
||||
require.Equal(t, 11, p2.pos)
|
||||
})
|
||||
}
|
||||
|
||||
func TestManyUntil(t *testing.T) {
|
||||
p := Pointer{"a,b,c,d,e,", 0}
|
||||
|
||||
t.Run("Matches sequence until", func(t *testing.T) {
|
||||
node, p2 := ManyUntil(CharRun("abcdefg"), Char("d"), Exact(","))(p)
|
||||
require.Equal(t, NewSequence(0,
|
||||
NewToken(0, "a"),
|
||||
NewToken(2, "b"),
|
||||
NewToken(4, "c"),
|
||||
), node)
|
||||
require.Equal(t, 6, p2.pos)
|
||||
})
|
||||
|
||||
t.Run("Returns error until matches early", func(t *testing.T) {
|
||||
node, p2 := ManyUntil(CharRun("abc"), Exact("a"), Exact(","))(p)
|
||||
require.Equal(t, NewError(0, "Unexpected input"), node)
|
||||
require.Equal(t, 0, p2.pos)
|
||||
require.Equal(t, "a,b,c,d,e,", p2.Get())
|
||||
})
|
||||
}
|
||||
|
||||
func assertNilParser(t *testing.T, parser Parser) {
|
||||
p := Pointer{"fff", 0}
|
||||
node, p2 := parser(p)
|
||||
require.Equal(t, nil, node)
|
||||
require.Equal(t, p, p2)
|
||||
}
|
35
examples/html.go
Normal file
35
examples/html.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
. "github.com/vektah/goparsify"
|
||||
)
|
||||
|
||||
func html(p Pointer) (Node, Pointer) {
|
||||
opentag := Exact("<")
|
||||
closetag := Exact(">")
|
||||
equal := Exact("=")
|
||||
slash := Exact("/")
|
||||
identifier := And(Char(Range("a-z")), CharRun(Range("a-zA-Z0-9")))
|
||||
text := CharRunUntil("<>")
|
||||
|
||||
var tag Parser
|
||||
|
||||
element := Any(text, &tag)
|
||||
elements := Kleene(element)
|
||||
//attr := And(identifier, equal, String())
|
||||
attr := And(identifier, equal, Exact(`"test"`))
|
||||
attrws := And(attr, WS)
|
||||
attrs := Kleene(attrws)
|
||||
tstart := And(opentag, identifier, attrs, closetag)
|
||||
tend := And(opentag, slash, identifier, closetag)
|
||||
tag = And(tstart, elements, tend)
|
||||
|
||||
return element(p)
|
||||
}
|
||||
|
||||
func main() {
|
||||
node, _ := html(Input("<h1>hello world</h1>"))
|
||||
fmt.Printf("%#v\n", node)
|
||||
}
|
43
nodes.go
Normal file
43
nodes.go
Normal file
@ -0,0 +1,43 @@
|
||||
package parsec
|
||||
|
||||
type Node interface {
|
||||
Pos() int
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
pos int
|
||||
Value string
|
||||
}
|
||||
|
||||
func (e Token) Pos() int { return e.pos }
|
||||
|
||||
func NewToken(pos int, value string) Token {
|
||||
return Token{pos, value}
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
pos int
|
||||
Error string
|
||||
}
|
||||
|
||||
func (e Error) Pos() int { return e.pos }
|
||||
|
||||
func NewError(pos int, message string) Error {
|
||||
return Error{pos, message}
|
||||
}
|
||||
|
||||
func IsError(n Node) bool {
|
||||
_, isErr := n.(Error)
|
||||
return isErr
|
||||
}
|
||||
|
||||
type Sequence struct {
|
||||
pos int
|
||||
Nodes []Node
|
||||
}
|
||||
|
||||
func (e Sequence) Pos() int { return e.pos }
|
||||
|
||||
func NewSequence(pos int, n ...Node) Sequence {
|
||||
return Sequence{pos, n}
|
||||
}
|
122
parser.go
Normal file
122
parser.go
Normal file
@ -0,0 +1,122 @@
|
||||
package parsec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Parser func(Pointer) (Node, Pointer)
|
||||
|
||||
// Parserish types are any type that can be turned into a Parser by Parsify
|
||||
// These currently include *Parser and string literals.
|
||||
//
|
||||
// This makes recursive grammars cleaner and allows string literals to be used directly in most contexts.
|
||||
// eg, matching balanced paren:
|
||||
// ```go
|
||||
// var group Parser
|
||||
// group = And("(", Maybe(&group), ")")
|
||||
// ```
|
||||
// vs
|
||||
// ```go
|
||||
// var group ParserPtr{}
|
||||
// group.P = And(Exact("("), Maybe(group.Parse), Exact(")"))
|
||||
// ```
|
||||
type Parserish interface{}
|
||||
|
||||
func Parsify(p Parserish) Parser {
|
||||
switch p := p.(type) {
|
||||
case func(Pointer) (Node, Pointer):
|
||||
return Parser(p)
|
||||
case Parser:
|
||||
return p
|
||||
case *Parser:
|
||||
// Todo: Maybe capture this stack and on nil show it? Is there a good error library to do this?
|
||||
return func(ptr Pointer) (Node, Pointer) {
|
||||
return (*p)(ptr)
|
||||
}
|
||||
case string:
|
||||
return Exact(p)
|
||||
default:
|
||||
panic(fmt.Errorf("cant turn a `%T` into a parser", p))
|
||||
}
|
||||
}
|
||||
|
||||
func ParsifyAll(parsers ...Parserish) []Parser {
|
||||
ret := make([]Parser, len(parsers))
|
||||
for i, parser := range parsers {
|
||||
ret[i] = Parsify(parser)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func Exact(match string) Parser {
|
||||
return func(p Pointer) (Node, Pointer) {
|
||||
if !p.HasPrefix(match) {
|
||||
return NewError(p.pos, "Expected "+match), p
|
||||
}
|
||||
|
||||
return NewToken(p.pos, match), p.Advance(len(match))
|
||||
}
|
||||
}
|
||||
|
||||
func Char(match string) Parser {
|
||||
return func(p Pointer) (Node, Pointer) {
|
||||
r, p2 := p.Accept(match)
|
||||
if r == "" {
|
||||
return NewError(p.pos, "Expected one of "+string(match)), p
|
||||
}
|
||||
|
||||
return NewToken(p.pos, string(r)), p2
|
||||
}
|
||||
}
|
||||
|
||||
func CharRun(match string) Parser {
|
||||
return func(p Pointer) (Node, Pointer) {
|
||||
s, p2 := p.AcceptRun(match)
|
||||
if s == "" {
|
||||
return NewError(p.pos, "Expected some of "+match), p
|
||||
}
|
||||
|
||||
return NewToken(p.pos, s), p2
|
||||
}
|
||||
}
|
||||
|
||||
func CharRunUntil(match string) Parser {
|
||||
return func(p Pointer) (Node, Pointer) {
|
||||
s, p2 := p.AcceptUntil(match)
|
||||
if s == "" {
|
||||
return NewError(p.pos, "Expected some of "+match), p
|
||||
}
|
||||
|
||||
return NewToken(p.pos, s), p2
|
||||
}
|
||||
}
|
||||
|
||||
func Range(r string) string {
|
||||
runes := []rune(r)
|
||||
if len(runes)%3 != 0 {
|
||||
panic("ranges should be in the form a-z0-9")
|
||||
}
|
||||
|
||||
match := ""
|
||||
|
||||
for i := 0; i < len(runes); i += 3 {
|
||||
start := runes[i]
|
||||
end := runes[i+2]
|
||||
if start > end {
|
||||
tmp := start
|
||||
start = end
|
||||
end = tmp
|
||||
}
|
||||
for c := start; c <= end; c++ {
|
||||
match += string(c)
|
||||
}
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
func WS(p Pointer) (Node, Pointer) {
|
||||
_, p2 := p.AcceptRun("\t\n\v\f\r \x85\xA0")
|
||||
|
||||
return nil, p2
|
||||
}
|
107
parser_test.go
Normal file
107
parser_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
package parsec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParsify(t *testing.T) {
|
||||
p := Pointer{"ffooo", 0}
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
node, _ := Parsify("ff")(p)
|
||||
require.Equal(t, NewToken(0, "ff"), node)
|
||||
})
|
||||
|
||||
t.Run("parsers", func(t *testing.T) {
|
||||
node, _ := Parsify(CharRun("f"))(p)
|
||||
require.Equal(t, NewToken(0, "ff"), node)
|
||||
})
|
||||
|
||||
t.Run("*parsers", func(t *testing.T) {
|
||||
var parser Parser
|
||||
parserfied := Parsify(&parser)
|
||||
parser = CharRun("f")
|
||||
|
||||
node, _ := parserfied(p)
|
||||
require.Equal(t, NewToken(0, "ff"), node)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
p := Pointer{"fooo", 0}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
node, p2 := Exact("fo")(p)
|
||||
require.Equal(t, NewToken(0, "fo"), node)
|
||||
require.Equal(t, p.Advance(2), p2)
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
node, p2 := Exact("bar")(p)
|
||||
require.Equal(t, NewError(0, "Expected bar"), node)
|
||||
require.Equal(t, 0, p2.pos)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChar(t *testing.T) {
|
||||
p := Pointer{"foobar", 0}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
node, p2 := Char("fo")(p)
|
||||
require.Equal(t, NewToken(0, "f"), node)
|
||||
require.Equal(t, p.Advance(1), p2)
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
node, p2 := Char("bar")(p)
|
||||
require.Equal(t, NewError(0, "Expected one of bar"), node)
|
||||
require.Equal(t, 0, p2.pos)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCharRun(t *testing.T) {
|
||||
p := Pointer{"foobar", 0}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
node, p2 := CharRun("fo")(p)
|
||||
require.Equal(t, NewToken(0, "foo"), node)
|
||||
require.Equal(t, p.Advance(3), p2)
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
node, p2 := CharRun("bar")(p)
|
||||
require.Equal(t, NewError(0, "Expected some of bar"), node)
|
||||
require.Equal(t, 0, p2.pos)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCharUntil(t *testing.T) {
|
||||
p := Pointer{"foobar", 0}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
node, p2 := CharRunUntil("z")(p)
|
||||
require.Equal(t, NewToken(0, "foobar"), node)
|
||||
require.Equal(t, p.Advance(6), p2)
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
node, p2 := CharRunUntil("f")(p)
|
||||
require.Equal(t, NewError(0, "Expected some of f"), node)
|
||||
require.Equal(t, 0, p2.pos)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWS(t *testing.T) {
|
||||
p := Pointer{" fooo", 0}
|
||||
|
||||
node, p2 := WS(p)
|
||||
require.Equal(t, nil, node)
|
||||
require.Equal(t, p.Advance(2), p2)
|
||||
}
|
||||
|
||||
func TestRange(t *testing.T) {
|
||||
require.Equal(t, "abcdefg", Range("a-g"))
|
||||
require.Equal(t, "01234abcd", Range("0-4a-d"))
|
||||
}
|
81
pointer.go
Normal file
81
pointer.go
Normal file
@ -0,0 +1,81 @@
|
||||
package parsec
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
EOF rune = -1
|
||||
)
|
||||
|
||||
func Input(s string) Pointer {
|
||||
return Pointer{s, 0}
|
||||
}
|
||||
|
||||
type Pointer struct {
|
||||
input string
|
||||
pos int
|
||||
}
|
||||
|
||||
func (p Pointer) Advance(i int) Pointer {
|
||||
return Pointer{p.input, p.pos + i}
|
||||
}
|
||||
|
||||
func (p Pointer) Get() string {
|
||||
return p.input[p.pos:]
|
||||
}
|
||||
|
||||
func (p Pointer) Remaining() int {
|
||||
remaining := len(p.input) - p.pos
|
||||
if remaining < 0 {
|
||||
return 0
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
func (p Pointer) Next() (rune, Pointer) {
|
||||
if int(p.pos) >= len(p.input) {
|
||||
return EOF, p
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(p.input[p.pos:])
|
||||
return r, p.Advance(w)
|
||||
}
|
||||
|
||||
func (p Pointer) HasPrefix(s string) bool {
|
||||
return strings.HasPrefix(p.input[p.pos:], s)
|
||||
}
|
||||
|
||||
func (p Pointer) Accept(valid string) (string, Pointer) {
|
||||
c, newP := p.Next()
|
||||
if strings.ContainsRune(valid, c) {
|
||||
return string(c), newP
|
||||
}
|
||||
return "", p
|
||||
}
|
||||
|
||||
func (p Pointer) AcceptRun(valid string) (string, Pointer) {
|
||||
matched := 0
|
||||
for p.pos+matched < len(p.input) {
|
||||
r, w := utf8.DecodeRuneInString(p.input[p.pos+matched:])
|
||||
if !strings.ContainsRune(valid, r) {
|
||||
break
|
||||
}
|
||||
matched += w
|
||||
}
|
||||
|
||||
return p.input[p.pos : p.pos+matched], p.Advance(matched)
|
||||
}
|
||||
|
||||
func (p Pointer) AcceptUntil(invalid string) (string, Pointer) {
|
||||
matched := 0
|
||||
for p.pos+matched < len(p.input) {
|
||||
r, w := utf8.DecodeRuneInString(p.input[p.pos+matched:])
|
||||
if strings.ContainsRune(invalid, r) {
|
||||
break
|
||||
}
|
||||
matched += w
|
||||
}
|
||||
|
||||
return p.input[p.pos : p.pos+matched], p.Advance(matched)
|
||||
}
|
86
pointer_test.go
Normal file
86
pointer_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
package parsec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPointer(t *testing.T) {
|
||||
p := Pointer{"fooo", 0}
|
||||
|
||||
t.Run("Advances", func(t *testing.T) {
|
||||
p2 := p.Advance(2)
|
||||
require.Equal(t, Pointer{"fooo", 2}, p2)
|
||||
require.Equal(t, Pointer{"fooo", 0}, p)
|
||||
require.Equal(t, Pointer{"fooo", 3}, p2.Advance(1))
|
||||
})
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
require.Equal(t, "fooo", p.Get())
|
||||
require.Equal(t, "ooo", p.Advance(1).Get())
|
||||
})
|
||||
|
||||
t.Run("Remaining", func(t *testing.T) {
|
||||
require.Equal(t, 4, p.Remaining())
|
||||
require.Equal(t, 0, p.Advance(4).Remaining())
|
||||
require.Equal(t, 0, p.Advance(10).Remaining())
|
||||
})
|
||||
|
||||
t.Run("Next takes one character", func(t *testing.T) {
|
||||
s, p2 := p.Next()
|
||||
require.Equal(t, p.Advance(1), p2)
|
||||
require.Equal(t, 'f', s)
|
||||
})
|
||||
|
||||
t.Run("Next handles EOF", func(t *testing.T) {
|
||||
s, p2 := p.Advance(5).Next()
|
||||
require.Equal(t, p.Advance(5), p2)
|
||||
require.Equal(t, EOF, s)
|
||||
})
|
||||
|
||||
t.Run("HasPrefix", func(t *testing.T) {
|
||||
require.True(t, p.HasPrefix("fo"))
|
||||
require.False(t, p.HasPrefix("ooo"))
|
||||
require.True(t, p.Advance(1).HasPrefix("ooo"))
|
||||
require.False(t, p.Advance(1).HasPrefix("oooo"))
|
||||
})
|
||||
|
||||
t.Run("Accept", func(t *testing.T) {
|
||||
s, p2 := p.Accept("abcdef")
|
||||
require.Equal(t, "f", s)
|
||||
require.Equal(t, p.Advance(1), p2)
|
||||
|
||||
s, p2 = p.Accept("ooooo")
|
||||
require.Equal(t, "", s)
|
||||
require.Equal(t, p.Advance(0), p2)
|
||||
|
||||
s, p2 = p.Advance(4).Accept("ooooo")
|
||||
require.Equal(t, "", s)
|
||||
require.Equal(t, p.Advance(4), p2)
|
||||
})
|
||||
|
||||
t.Run("AcceptRun", func(t *testing.T) {
|
||||
s, p2 := p.AcceptRun("f")
|
||||
require.Equal(t, "f", s)
|
||||
require.Equal(t, p.Advance(1), p2)
|
||||
|
||||
s, p3 := p.AcceptRun("fo")
|
||||
require.Equal(t, "fooo", s)
|
||||
require.Equal(t, p.Advance(4), p3)
|
||||
|
||||
s, p4 := p3.AcceptRun("fo")
|
||||
require.Equal(t, "", s)
|
||||
require.Equal(t, p.Advance(4), p4)
|
||||
})
|
||||
|
||||
t.Run("AcceptUntil", func(t *testing.T) {
|
||||
s, p2 := p.AcceptUntil("o")
|
||||
require.Equal(t, "f", s)
|
||||
require.Equal(t, p.Advance(1), p2)
|
||||
|
||||
s, p3 := p2.AcceptRun("o")
|
||||
require.Equal(t, "ooo", s)
|
||||
require.Equal(t, p.Advance(4), p3)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user