Compare commits

..

12 Commits

Author SHA1 Message Date
Thomas Pelletier 7f50e4c339 Merge pull request #51 from pelletier/pelletier/fix-crlf-support
Fix support for CRLF line ending
2016-02-20 13:20:03 +01:00
Thomas Pelletier a402e618c3 sudo is not needed by travis anymore 2016-02-19 14:17:07 +01:00
Thomas Pelletier 2df083520a Fix support for CRLF line ending 2016-02-19 14:12:13 +01:00
Thomas Pelletier 8176e30b38 Fix printf formatting 2016-01-31 17:07:37 +01:00
Thomas Pelletier 14c964fc02 Merge pull request #49 from pelletier/generic-input
Generic input
2016-01-31 16:57:17 +01:00
Thomas Pelletier f963bc320f Generic input
Fixes #47
2016-01-31 16:54:40 +01:00
Thomas Pelletier 0488b850c6 Have Travis run 1.5.3 2016-01-14 11:33:30 +01:00
Thomas Pelletier 346e676fa2 2015 -> 2016 2016-01-05 10:06:54 +01:00
Thomas Pelletier 6d743bb19f Improve error checking on number parsing 2015-12-01 14:38:33 +01:00
Thomas Pelletier fa1c2ab68c Error when parsing an empty key 2015-12-01 14:02:02 +01:00
Thomas Pelletier a6c6ad1f5f Don't crash when assigning group array to array 2015-12-01 13:56:31 +01:00
Thomas Pelletier ab7a652912 Fix TOML URL in doc.go 2015-12-01 09:53:09 +01:00
17 changed files with 377 additions and 205 deletions
+1 -3
View File
@@ -1,11 +1,9 @@
language: go language: go
script: "./test.sh" script: "./test.sh"
sudo: false
go: go:
- 1.2.2
- 1.3.3 - 1.3.3
- 1.4.2 - 1.4.2
- 1.5.1 - 1.5.3
- tip - tip
before_install: before_install:
- go get github.com/axw/gocov/gocov - go get github.com/axw/gocov/gocov
+1 -1
View File
@@ -98,7 +98,7 @@ You can run both of them using `./test.sh`.
## License ## License
Copyright (c) 2013 - 2015 Thomas Pelletier, Eric Anderton Copyright (c) 2013 - 2016 Thomas Pelletier, Eric Anderton
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in
+1 -1
View File
@@ -1,7 +1,7 @@
// Package toml is a TOML markup language parser. // Package toml is a TOML markup language parser.
// //
// This version supports the specification as described in // This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/toml-v0.2.0.md // https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md
// //
// TOML Parsing // TOML Parsing
// //
+3 -3
View File
@@ -69,13 +69,13 @@ func Example_comprehensiveExample() {
fmt.Println("User is ", user, ". Password is ", password) fmt.Println("User is ", user, ". Password is ", password)
// show where elements are in the file // show where elements are in the file
fmt.Println("User position: %v", configTree.GetPosition("user")) fmt.Printf("User position: %v\n", configTree.GetPosition("user"))
fmt.Println("Password position: %v", configTree.GetPosition("password")) fmt.Printf("Password position: %v\n", configTree.GetPosition("password"))
// use a query to gather elements without walking the tree // use a query to gather elements without walking the tree
results, _ := config.Query("$..[user,password]") results, _ := config.Query("$..[user,password]")
for ii, item := range results.Values() { for ii, item := range results.Values() {
fmt.Println("Query result %d: %v", ii, item) fmt.Printf("Query result %d: %v\n", ii, item)
} }
} }
} }
+29
View File
@@ -0,0 +1,29 @@
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
+3
View File
@@ -70,6 +70,9 @@ func parseKey(key string) ([]string, error) {
if buffer.Len() > 0 { if buffer.Len() > 0 {
groups = append(groups, buffer.String()) groups = append(groups, buffer.String())
} }
if len(groups) == 0 {
return nil, fmt.Errorf("empty key")
}
return groups, nil return groups, nil
} }
+5
View File
@@ -42,3 +42,8 @@ func TestDottedKeyBasic(t *testing.T) {
func TestBaseKeyPound(t *testing.T) { func TestBaseKeyPound(t *testing.T) {
testError(t, "hello#world", "invalid bare character: #") testError(t, "hello#world", "invalid bare character: #")
} }
func TestEmptyKey(t *testing.T) {
testError(t, "", "empty key")
testError(t, " ", "empty key")
}
+189 -167
View File
@@ -7,10 +7,11 @@ package toml
import ( import (
"fmt" "fmt"
"github.com/pelletier/go-buffruneio"
"io"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8"
) )
var dateRegexp *regexp.Regexp var dateRegexp *regexp.Regexp
@@ -20,47 +21,56 @@ type tomlLexStateFn func() tomlLexStateFn
// Define lexer // Define lexer
type tomlLexer struct { type tomlLexer struct {
input string input *buffruneio.Reader // Textual source
start int buffer []rune // Runes composing the current token
pos int tokens chan token
width int depth int
tokens chan token line int
depth int col int
line int endbufferLine int
col int endbufferCol int
} }
func (l *tomlLexer) run() { // Basic read operations on input
for state := l.lexVoid; state != nil; {
state = state() func (l *tomlLexer) read() rune {
r, err := l.input.ReadRune()
if err != nil {
panic(err)
} }
close(l.tokens) if r == '\n' {
l.endbufferLine++
l.endbufferCol = 1
} else {
l.endbufferCol++
}
return r
} }
func (l *tomlLexer) nextStart() { func (l *tomlLexer) next() rune {
// iterate by runes (utf8 characters) r := l.read()
// search for newlines and advance line/col counts
for i := l.start; i < l.pos; { if r != eof {
r, width := utf8.DecodeRuneInString(l.input[i:]) l.buffer = append(l.buffer, r)
if r == '\n' {
l.line++
l.col = 1
} else {
l.col++
}
i += width
} }
// advance start position to next token return r
l.start = l.pos
} }
func (l *tomlLexer) emit(t tokenType) { func (l *tomlLexer) ignore() {
l.tokens <- token{ l.buffer = make([]rune, 0)
Position: Position{l.line, l.col}, l.line = l.endbufferLine
typ: t, l.col = l.endbufferCol
val: l.input[l.start:l.pos], }
func (l *tomlLexer) skip() {
l.next()
l.ignore()
}
func (l *tomlLexer) fastForward(n int) {
for i := 0; i < n; i++ {
l.next()
} }
l.nextStart()
} }
func (l *tomlLexer) emitWithValue(t tokenType, value string) { func (l *tomlLexer) emitWithValue(t tokenType, value string) {
@@ -69,27 +79,37 @@ func (l *tomlLexer) emitWithValue(t tokenType, value string) {
typ: t, typ: t,
val: value, val: value,
} }
l.nextStart() l.ignore()
} }
func (l *tomlLexer) next() rune { func (l *tomlLexer) emit(t tokenType) {
if l.pos >= len(l.input) { l.emitWithValue(t, string(l.buffer))
l.width = 0 }
return eof
func (l *tomlLexer) peek() rune {
r, err := l.input.ReadRune()
if err != nil {
panic(err)
} }
var r rune l.input.UnreadRune()
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
l.pos += l.width
return r return r
} }
func (l *tomlLexer) ignore() { func (l *tomlLexer) follow(next string) bool {
l.nextStart() for _, expectedRune := range next {
r, err := l.input.ReadRune()
defer l.input.UnreadRune()
if err != nil {
panic(err)
}
if expectedRune != r {
return false
}
}
return true
} }
func (l *tomlLexer) backup() { // Error management
l.pos -= l.width
}
func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn { func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
l.tokens <- token{ l.tokens <- token{
@@ -100,23 +120,7 @@ func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
return nil return nil
} }
func (l *tomlLexer) peek() rune { // State functions
r := l.next()
l.backup()
return r
}
func (l *tomlLexer) accept(valid string) bool {
if strings.IndexRune(valid, l.next()) >= 0 {
return true
}
l.backup()
return false
}
func (l *tomlLexer) follow(next string) bool {
return strings.HasPrefix(l.input[l.pos:], next)
}
func (l *tomlLexer) lexVoid() tomlLexStateFn { func (l *tomlLexer) lexVoid() tomlLexStateFn {
for { for {
@@ -128,10 +132,15 @@ func (l *tomlLexer) lexVoid() tomlLexStateFn {
return l.lexComment return l.lexComment
case '=': case '=':
return l.lexEqual return l.lexEqual
case '\r':
fallthrough
case '\n':
l.skip()
continue
} }
if isSpace(next) { if isSpace(next) {
l.ignore() l.skip()
} }
if l.depth > 0 { if l.depth > 0 {
@@ -142,7 +151,8 @@ func (l *tomlLexer) lexVoid() tomlLexStateFn {
return l.lexKey return l.lexKey
} }
if l.next() == eof { if next == eof {
l.next()
break break
} }
} }
@@ -177,13 +187,16 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
return l.lexLiteralString return l.lexLiteralString
case ',': case ',':
return l.lexComma return l.lexComma
case '\r':
fallthrough
case '\n': case '\n':
l.ignore() l.skip()
l.pos++
if l.depth == 0 { if l.depth == 0 {
return l.lexVoid return l.lexVoid
} }
return l.lexRvalue return l.lexRvalue
case '_':
return l.errorf("cannot start number with underscore")
} }
if l.follow("true") { if l.follow("true") {
@@ -194,14 +207,20 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
return l.lexFalse return l.lexFalse
} }
if isAlphanumeric(next) { if isSpace(next) {
return l.lexKey l.skip()
continue
} }
dateMatch := dateRegexp.FindString(l.input[l.pos:]) if next == eof {
l.next()
break
}
possibleDate := string(l.input.Peek(35))
dateMatch := dateRegexp.FindString(possibleDate)
if dateMatch != "" { if dateMatch != "" {
l.ignore() l.fastForward(len(dateMatch))
l.pos += len(dateMatch)
return l.lexDate return l.lexDate
} }
@@ -209,13 +228,10 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
return l.lexNumber return l.lexNumber
} }
if isSpace(next) { if isAlphanumeric(next) {
l.ignore() return l.lexKey
} }
if l.next() == eof {
break
}
} }
l.emit(tokenEOF) l.emit(tokenEOF)
@@ -223,15 +239,13 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
} }
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
l.ignore() l.next()
l.pos++
l.emit(tokenLeftCurlyBrace) l.emit(tokenLeftCurlyBrace)
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
l.ignore() l.next()
l.pos++
l.emit(tokenRightCurlyBrace) l.emit(tokenRightCurlyBrace)
return l.lexRvalue return l.lexRvalue
} }
@@ -242,37 +256,32 @@ func (l *tomlLexer) lexDate() tomlLexStateFn {
} }
func (l *tomlLexer) lexTrue() tomlLexStateFn { func (l *tomlLexer) lexTrue() tomlLexStateFn {
l.ignore() l.fastForward(4)
l.pos += 4
l.emit(tokenTrue) l.emit(tokenTrue)
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexFalse() tomlLexStateFn { func (l *tomlLexer) lexFalse() tomlLexStateFn {
l.ignore() l.fastForward(5)
l.pos += 5
l.emit(tokenFalse) l.emit(tokenFalse)
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexEqual() tomlLexStateFn { func (l *tomlLexer) lexEqual() tomlLexStateFn {
l.ignore() l.next()
l.accept("=")
l.emit(tokenEqual) l.emit(tokenEqual)
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexComma() tomlLexStateFn { func (l *tomlLexer) lexComma() tomlLexStateFn {
l.ignore() l.next()
l.accept(",")
l.emit(tokenComma) l.emit(tokenComma)
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexKey() tomlLexStateFn { func (l *tomlLexer) lexKey() tomlLexStateFn {
l.ignore()
inQuotes := false inQuotes := false
for r := l.next(); isKeyChar(r) || r == '\n'; r = l.next() { for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() {
if r == '"' { if r == '"' {
inQuotes = !inQuotes inQuotes = !inQuotes
} else if r == '\n' { } else if r == '\n' {
@@ -282,46 +291,46 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
} else if !isValidBareChar(r) && !inQuotes { } else if !isValidBareChar(r) && !inQuotes {
return l.errorf("keys cannot contain %c character", r) return l.errorf("keys cannot contain %c character", r)
} }
l.next()
} }
l.backup()
l.emit(tokenKey) l.emit(tokenKey)
return l.lexVoid return l.lexVoid
} }
func (l *tomlLexer) lexComment() tomlLexStateFn { func (l *tomlLexer) lexComment() tomlLexStateFn {
for { for next := l.peek(); next != '\n' && next != eof; next = l.peek() {
next := l.next() if (next == '\r' && l.follow("\r\n")) {
if next == '\n' || next == eof {
break break
} }
l.next()
} }
l.ignore() l.ignore()
return l.lexVoid return l.lexVoid
} }
func (l *tomlLexer) lexLeftBracket() tomlLexStateFn { func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
l.ignore() l.next()
l.pos++
l.emit(tokenLeftBracket) l.emit(tokenLeftBracket)
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexLiteralString() tomlLexStateFn { func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
l.pos++ l.skip()
l.ignore()
growingString := "" growingString := ""
// handle special case for triple-quote // handle special case for triple-quote
terminator := "'" terminator := "'"
if l.follow("''") { if l.follow("''") {
l.pos += 2 l.skip()
l.ignore() l.skip()
terminator = "'''" terminator = "'''"
// special case: discard leading newline // special case: discard leading newline
if l.peek() == '\n' { if l.follow("\r\n") {
l.pos++ l.skip()
l.ignore() l.skip()
} else if l.peek() == '\n' {
l.skip()
} }
} }
@@ -329,50 +338,51 @@ func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
for { for {
if l.follow(terminator) { if l.follow(terminator) {
l.emitWithValue(tokenString, growingString) l.emitWithValue(tokenString, growingString)
l.pos += len(terminator) l.fastForward(len(terminator))
l.ignore() l.ignore()
return l.lexRvalue return l.lexRvalue
} }
growingString += string(l.peek()) next := l.peek()
if next == eof {
if l.next() == eof {
break break
} }
growingString += string(l.next())
} }
return l.errorf("unclosed string") return l.errorf("unclosed string")
} }
func (l *tomlLexer) lexString() tomlLexStateFn { func (l *tomlLexer) lexString() tomlLexStateFn {
l.pos++ l.skip()
l.ignore()
growingString := "" growingString := ""
// handle special case for triple-quote // handle special case for triple-quote
terminator := "\"" terminator := "\""
if l.follow("\"\"") { if l.follow("\"\"") {
l.pos += 2 l.skip()
l.ignore() l.skip()
terminator = "\"\"\"" terminator = "\"\"\""
// special case: discard leading newline // special case: discard leading newline
if l.peek() == '\n' { if l.follow("\r\n") {
l.pos++ l.skip()
l.ignore() l.skip()
} else if l.peek() == '\n' {
l.skip()
} }
} }
for { for {
if l.follow(terminator) { if l.follow(terminator) {
l.emitWithValue(tokenString, growingString) l.emitWithValue(tokenString, growingString)
l.pos += len(terminator) l.fastForward(len(terminator))
l.ignore() l.ignore()
return l.lexRvalue return l.lexRvalue
} }
if l.follow("\\") { if l.follow("\\") {
l.pos++ l.next()
switch l.peek() { switch l.peek() {
case '\r': case '\r':
fallthrough fallthrough
@@ -382,56 +392,60 @@ func (l *tomlLexer) lexString() tomlLexStateFn {
fallthrough fallthrough
case ' ': case ' ':
// skip all whitespace chars following backslash // skip all whitespace chars following backslash
l.pos++
for strings.ContainsRune("\r\n\t ", l.peek()) { for strings.ContainsRune("\r\n\t ", l.peek()) {
l.pos++ l.next()
} }
l.pos--
case '"': case '"':
growingString += "\"" growingString += "\""
l.next()
case 'n': case 'n':
growingString += "\n" growingString += "\n"
l.next()
case 'b': case 'b':
growingString += "\b" growingString += "\b"
l.next()
case 'f': case 'f':
growingString += "\f" growingString += "\f"
l.next()
case '/': case '/':
growingString += "/" growingString += "/"
l.next()
case 't': case 't':
growingString += "\t" growingString += "\t"
l.next()
case 'r': case 'r':
growingString += "\r" growingString += "\r"
l.next()
case '\\': case '\\':
growingString += "\\" growingString += "\\"
l.next()
case 'u': case 'u':
l.pos++ l.next()
code := "" code := ""
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
c := l.peek() c := l.peek()
l.pos++
if !isHexDigit(c) { if !isHexDigit(c) {
return l.errorf("unfinished unicode escape") return l.errorf("unfinished unicode escape")
} }
l.next()
code = code + string(c) code = code + string(c)
} }
l.pos--
intcode, err := strconv.ParseInt(code, 16, 32) intcode, err := strconv.ParseInt(code, 16, 32)
if err != nil { if err != nil {
return l.errorf("invalid unicode escape: \\u" + code) return l.errorf("invalid unicode escape: \\u" + code)
} }
growingString += string(rune(intcode)) growingString += string(rune(intcode))
case 'U': case 'U':
l.pos++ l.next()
code := "" code := ""
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
c := l.peek() c := l.peek()
l.pos++
if !isHexDigit(c) { if !isHexDigit(c) {
return l.errorf("unfinished unicode escape") return l.errorf("unfinished unicode escape")
} }
l.next()
code = code + string(c) code = code + string(c)
} }
l.pos--
intcode, err := strconv.ParseInt(code, 16, 64) intcode, err := strconv.ParseInt(code, 16, 64)
if err != nil { if err != nil {
return l.errorf("invalid unicode escape: \\U" + code) return l.errorf("invalid unicode escape: \\U" + code)
@@ -445,10 +459,11 @@ func (l *tomlLexer) lexString() tomlLexStateFn {
if 0x00 <= r && r <= 0x1F { if 0x00 <= r && r <= 0x1F {
return l.errorf("unescaped control character %U", r) return l.errorf("unescaped control character %U", r)
} }
l.next()
growingString += string(r) growingString += string(r)
} }
if l.next() == eof { if l.peek() == eof {
break break
} }
} }
@@ -457,12 +472,11 @@ func (l *tomlLexer) lexString() tomlLexStateFn {
} }
func (l *tomlLexer) lexKeyGroup() tomlLexStateFn { func (l *tomlLexer) lexKeyGroup() tomlLexStateFn {
l.ignore() l.next()
l.pos++
if l.peek() == '[' { if l.peek() == '[' {
// token '[[' signifies an array of anonymous key groups // token '[[' signifies an array of anonymous key groups
l.pos++ l.next()
l.emit(tokenDoubleLeftBracket) l.emit(tokenDoubleLeftBracket)
return l.lexInsideKeyGroupArray return l.lexInsideKeyGroupArray
} }
@@ -472,87 +486,85 @@ func (l *tomlLexer) lexKeyGroup() tomlLexStateFn {
} }
func (l *tomlLexer) lexInsideKeyGroupArray() tomlLexStateFn { func (l *tomlLexer) lexInsideKeyGroupArray() tomlLexStateFn {
for { for r := l.peek(); r != eof; r = l.peek() {
if l.peek() == ']' { switch r {
if l.pos > l.start { case ']':
if len(l.buffer) > 0 {
l.emit(tokenKeyGroupArray) l.emit(tokenKeyGroupArray)
} }
l.ignore() l.next()
l.pos++
if l.peek() != ']' { if l.peek() != ']' {
break // error break
} }
l.pos++ l.next()
l.emit(tokenDoubleRightBracket) l.emit(tokenDoubleRightBracket)
return l.lexVoid return l.lexVoid
} else if l.peek() == '[' { case '[':
return l.errorf("group name cannot contain ']'") return l.errorf("group name cannot contain ']'")
} default:
l.next()
if l.next() == eof {
break
} }
} }
return l.errorf("unclosed key group array") return l.errorf("unclosed key group array")
} }
func (l *tomlLexer) lexInsideKeyGroup() tomlLexStateFn { func (l *tomlLexer) lexInsideKeyGroup() tomlLexStateFn {
for { for r := l.peek(); r != eof; r = l.peek() {
if l.peek() == ']' { switch r {
if l.pos > l.start { case ']':
if len(l.buffer) > 0 {
l.emit(tokenKeyGroup) l.emit(tokenKeyGroup)
} }
l.ignore() l.next()
l.pos++
l.emit(tokenRightBracket) l.emit(tokenRightBracket)
return l.lexVoid return l.lexVoid
} else if l.peek() == '[' { case '[':
return l.errorf("group name cannot contain ']'") return l.errorf("group name cannot contain ']'")
} default:
l.next()
if l.next() == eof {
break
} }
} }
return l.errorf("unclosed key group") return l.errorf("unclosed key group")
} }
func (l *tomlLexer) lexRightBracket() tomlLexStateFn { func (l *tomlLexer) lexRightBracket() tomlLexStateFn {
l.ignore() l.next()
l.pos++
l.emit(tokenRightBracket) l.emit(tokenRightBracket)
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexNumber() tomlLexStateFn { func (l *tomlLexer) lexNumber() tomlLexStateFn {
l.ignore() r := l.peek()
if !l.accept("+") { if r == '+' || r == '-' {
l.accept("-") l.next()
} }
pointSeen := false pointSeen := false
expSeen := false expSeen := false
digitSeen := false digitSeen := false
for { for {
next := l.next() next := l.peek()
if next == '.' { if next == '.' {
if pointSeen { if pointSeen {
return l.errorf("cannot have two dots in one float") return l.errorf("cannot have two dots in one float")
} }
l.next()
if !isDigit(l.peek()) { if !isDigit(l.peek()) {
return l.errorf("float cannot end with a dot") return l.errorf("float cannot end with a dot")
} }
pointSeen = true pointSeen = true
} else if next == 'e' || next == 'E' { } else if next == 'e' || next == 'E' {
expSeen = true expSeen = true
if !l.accept("+") { l.next()
l.accept("-") r := l.peek()
if r == '+' || r == '-' {
l.next()
} }
} else if isDigit(next) { } else if isDigit(next) {
digitSeen = true digitSeen = true
l.next()
} else if next == '_' { } else if next == '_' {
l.pos++ l.next()
} else { } else {
l.backup()
break break
} }
if pointSeen && !digitSeen { if pointSeen && !digitSeen {
@@ -571,17 +583,27 @@ func (l *tomlLexer) lexNumber() tomlLexStateFn {
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) run() {
for state := l.lexVoid; state != nil; {
state = state()
}
close(l.tokens)
}
func init() { func init() {
dateRegexp = regexp.MustCompile("^\\d{1,4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})") dateRegexp = regexp.MustCompile("^\\d{1,4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})")
} }
// Entry point // Entry point
func lexToml(input string) chan token { func lexToml(input io.Reader) chan token {
bufferedInput := buffruneio.NewReader(input)
l := &tomlLexer{ l := &tomlLexer{
input: input, input: bufferedInput,
tokens: make(chan token), tokens: make(chan token),
line: 1, line: 1,
col: 1, col: 1,
endbufferLine: 1,
endbufferCol: 1,
} }
go l.run() go l.run()
return l.tokens return l.tokens
+28 -2
View File
@@ -1,15 +1,19 @@
package toml package toml
import "testing" import (
"strings"
"testing"
)
func testFlow(t *testing.T, input string, expectedFlow []token) { func testFlow(t *testing.T, input string, expectedFlow []token) {
ch := lexToml(input) ch := lexToml(strings.NewReader(input))
for _, expected := range expectedFlow { for _, expected := range expectedFlow {
token := <-ch token := <-ch
if token != expected { if token != expected {
t.Log("While testing: ", input) t.Log("While testing: ", input)
t.Log("compared (got)", token, "to (expected)", expected) t.Log("compared (got)", token, "to (expected)", expected)
t.Log("\tvalue:", token.val, "<->", expected.val) t.Log("\tvalue:", token.val, "<->", expected.val)
t.Log("\tvalue as bytes:", []byte(token.val), "<->", []byte(expected.val))
t.Log("\ttype:", token.typ.String(), "<->", expected.typ.String()) t.Log("\ttype:", token.typ.String(), "<->", expected.typ.String())
t.Log("\tline:", token.Line, "<->", expected.Line) t.Log("\tline:", token.Line, "<->", expected.Line)
t.Log("\tcolumn:", token.Col, "<->", expected.Col) t.Log("\tcolumn:", token.Col, "<->", expected.Col)
@@ -83,6 +87,19 @@ func TestMultipleKeyGroupsComment(t *testing.T) {
}) })
} }
func TestSimpleWindowsCRLF(t *testing.T) {
testFlow(t, "a=4\r\nb=2", []token{
token{Position{1, 1}, tokenKey, "a"},
token{Position{1, 2}, tokenEqual, "="},
token{Position{1, 3}, tokenInteger, "4"},
token{Position{2, 1}, tokenKey, "b"},
token{Position{2, 2}, tokenEqual, "="},
token{Position{2, 3}, tokenInteger, "2"},
token{Position{2, 4}, tokenEOF, ""},
})
}
func TestBasicKey(t *testing.T) { func TestBasicKey(t *testing.T) {
testFlow(t, "hello", []token{ testFlow(t, "hello", []token{
token{Position{1, 1}, tokenKey, "hello"}, token{Position{1, 1}, tokenKey, "hello"},
@@ -618,3 +635,12 @@ func TestKeyNewline(t *testing.T) {
token{Position{1, 1}, tokenError, "keys cannot contain new lines"}, token{Position{1, 1}, tokenError, "keys cannot contain new lines"},
}) })
} }
func TestInvalidFloat(t *testing.T) {
testFlow(t, "a=7e1_", []token{
token{Position{1, 1}, tokenKey, "a"},
token{Position{1, 2}, tokenEqual, "="},
token{Position{1, 3}, tokenFloat, "7e1_"},
token{Position{1, 7}, tokenEOF, ""},
})
}
+1 -1
View File
@@ -202,7 +202,7 @@ func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
fn, ok := (*ctx.filters)[f.Name] fn, ok := (*ctx.filters)[f.Name]
if !ok { if !ok {
panic(fmt.Sprintf("%s: query context does not have filter '%s'", panic(fmt.Sprintf("%s: query context does not have filter '%s'",
f.Pos, f.Name)) f.Pos.String(), f.Name))
} }
switch castNode := tomlValueCheck(node, ctx).(type) { switch castNode := tomlValueCheck(node, ctx).(type) {
case *TomlTree: case *TomlTree:
+24 -3
View File
@@ -5,6 +5,7 @@ package toml
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -107,7 +108,7 @@ func (p *tomlParser) parseGroupArray() tomlParserStateFn {
var array []*TomlTree var array []*TomlTree
if destTree == nil { if destTree == nil {
array = make([]*TomlTree, 0) array = make([]*TomlTree, 0)
} else if destTree.([]*TomlTree) != nil { } else if target, ok := destTree.([]*TomlTree); ok && target != nil {
array = destTree.([]*TomlTree) array = destTree.([]*TomlTree)
} else { } else {
p.raiseError(key, "key %s is already assigned and not of type group array", key) p.raiseError(key, "key %s is already assigned and not of type group array", key)
@@ -211,6 +212,16 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
return p.parseStart return p.parseStart
} }
var numberUnderscoreInvalidRegexp *regexp.Regexp
func cleanupNumberToken(value string) (string, error) {
if numberUnderscoreInvalidRegexp.MatchString(value) {
return "", fmt.Errorf("invalid use of _ in number")
}
cleanedVal := strings.Replace(value, "_", "", -1)
return cleanedVal, nil
}
func (p *tomlParser) parseRvalue() interface{} { func (p *tomlParser) parseRvalue() interface{} {
tok := p.getToken() tok := p.getToken()
if tok == nil || tok.typ == tokenEOF { if tok == nil || tok.typ == tokenEOF {
@@ -225,14 +236,20 @@ func (p *tomlParser) parseRvalue() interface{} {
case tokenFalse: case tokenFalse:
return false return false
case tokenInteger: case tokenInteger:
cleanedVal := strings.Replace(tok.val, "_", "", -1) cleanedVal, err := cleanupNumberToken(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err := strconv.ParseInt(cleanedVal, 10, 64) val, err := strconv.ParseInt(cleanedVal, 10, 64)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
return val return val
case tokenFloat: case tokenFloat:
cleanedVal := strings.Replace(tok.val, "_", "", -1) cleanedVal, err := cleanupNumberToken(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err := strconv.ParseFloat(cleanedVal, 64) val, err := strconv.ParseFloat(cleanedVal, 64)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
@@ -361,3 +378,7 @@ func parseToml(flow chan token) *TomlTree {
parser.run() parser.run()
return result return result
} }
func init() {
numberUnderscoreInvalidRegexp = regexp.MustCompile(`([^\d]_|_[^\d]|_$|^_)`)
}
+67 -2
View File
@@ -287,7 +287,7 @@ func TestArrayNestedStrings(t *testing.T) {
func TestMissingValue(t *testing.T) { func TestMissingValue(t *testing.T) {
_, err := Load("a = ") _, err := Load("a = ")
if err.Error() != "(1, 4): expecting a value" { if err.Error() != "(1, 5): expecting a value" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
@@ -441,7 +441,7 @@ func TestImplicitDeclarationBefore(t *testing.T) {
func TestFloatsWithoutLeadingZeros(t *testing.T) { func TestFloatsWithoutLeadingZeros(t *testing.T) {
_, err := Load("a = .42") _, err := Load("a = .42")
if err.Error() != "(1, 4): cannot start float with a dot" { if err.Error() != "(1, 5): cannot start float with a dot" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
@@ -494,6 +494,42 @@ func TestParseFile(t *testing.T) {
}) })
} }
func TestParseFileCRLF(t *testing.T) {
tree, err := LoadFile("example-crlf.toml")
assertTree(t, tree, err, map[string]interface{}{
"title": "TOML Example",
"owner": map[string]interface{}{
"name": "Tom Preston-Werner",
"organization": "GitHub",
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
"dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
},
"database": map[string]interface{}{
"server": "192.168.1.1",
"ports": []int64{8001, 8001, 8002},
"connection_max": 5000,
"enabled": true,
},
"servers": map[string]interface{}{
"alpha": map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
},
"beta": map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
},
},
"clients": map[string]interface{}{
"data": []interface{}{
[]string{"gamma", "delta"},
[]int64{1, 2},
},
},
})
}
func TestParseKeyGroupArray(t *testing.T) { func TestParseKeyGroupArray(t *testing.T) {
tree, err := Load("[[foo.bar]] a = 42\n[[foo.bar]] a = 69") tree, err := Load("[[foo.bar]] a = 42\n[[foo.bar]] a = 69")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -626,3 +662,32 @@ func TestDoubleEqual(t *testing.T) {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
func TestGroupArrayReassign(t *testing.T) {
_, err := Load("[hello]\n[[hello]]")
if err.Error() != "(2, 3): key \"hello\" is already assigned and not of type group array" {
t.Error("Bad error message:", err.Error())
}
}
func TestInvalidFloatParsing(t *testing.T) {
_, err := Load("a=1e_2")
if err.Error() != "(1, 3): invalid use of _ in number" {
t.Error("Bad error message:", err.Error())
}
_, err = Load("a=1e2_")
if err.Error() != "(1, 3): invalid use of _ in number" {
t.Error("Bad error message:", err.Error())
}
_, err = Load("a=1__2")
if err.Error() != "(1, 3): invalid use of _ in number" {
t.Error("Bad error message:", err.Error())
}
_, err = Load("a=_1_2")
if err.Error() != "(1, 3): cannot start number with underscore" {
t.Error("Bad error message:", err.Error())
}
}
+6 -8
View File
@@ -6,13 +6,11 @@ import (
"fmt" "fmt"
) )
/* // Position of a document element within a TOML document.
Position of a document element within a TOML document. //
// Line and Col are both 1-indexed positions for the element's line number and
Line and Col are both 1-indexed positions for the element's line number and // column number, respectively. Values of zero or less will cause Invalid(),
column number, respectively. Values of zero or less will cause Invalid(), // to return true.
to return true.
*/
type Position struct { type Position struct {
Line int // line within the document Line int // line within the document
Col int // column within the line Col int // column within the line
@@ -24,7 +22,7 @@ func (p *Position) String() string {
return fmt.Sprintf("(%d, %d)", p.Line, p.Col) return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
} }
// Returns whether or not the position is valid (i.e. with negative or // Invalid returns whether or not the position is valid (i.e. with negative or
// null values) // null values)
func (p *Position) Invalid() bool { func (p *Position) Invalid() bool {
return p.Line <= 0 || p.Col <= 0 return p.Line <= 0 || p.Col <= 0
-1
View File
@@ -138,7 +138,6 @@ func (p *queryParser) parseMatchExpr() queryParserStateFn {
return nil // allow EOF at this stage return nil // allow EOF at this stage
} }
return p.parseError(tok, "expected match expression") return p.parseError(tok, "expected match expression")
return nil
} }
func (p *queryParser) parseBracketExpr() queryParserStateFn { func (p *queryParser) parseBracketExpr() queryParserStateFn {
+3 -1
View File
@@ -19,6 +19,8 @@ function git_clone() {
popd popd
} }
go get github.com/pelletier/go-buffruneio
# get code for BurntSushi TOML validation # get code for BurntSushi TOML validation
# pinning all to 'HEAD' for version 0.3.x work (TODO: pin to commit hash when tests stabilize) # pinning all to 'HEAD' for version 0.3.x work (TODO: pin to commit hash when tests stabilize)
git_clone github.com/BurntSushi/toml master HEAD git_clone github.com/BurntSushi/toml master HEAD
@@ -66,7 +68,7 @@ else
echo "Invalid Test TOML for $test:" echo "Invalid Test TOML for $test:"
echo "====" echo "===="
cat "$invalid_test.toml" cat "$invalid_test.toml"
echo "Go-TOML Output for $test:" echo "Go-TOML Output for $test:"
echo "====" echo "===="
echo "go-toml Output:" echo "go-toml Output:"
+15 -11
View File
@@ -3,7 +3,8 @@ package toml
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"os"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@@ -360,8 +361,8 @@ func (t *TomlTree) ToString() string {
return t.toToml("", "") return t.toToml("", "")
} }
// Load creates a TomlTree from a string. // LoadReader creates a TomlTree from any io.Reader.
func Load(content string) (tree *TomlTree, err error) { func LoadReader(reader io.Reader) (tree *TomlTree, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok { if _, ok := r.(runtime.Error); ok {
@@ -370,18 +371,21 @@ func Load(content string) (tree *TomlTree, err error) {
err = errors.New(r.(string)) err = errors.New(r.(string))
} }
}() }()
tree = parseToml(lexToml(content)) tree = parseToml(lexToml(reader))
return return
} }
// Load creates a TomlTree from a string.
func Load(content string) (tree *TomlTree, err error) {
return LoadReader(strings.NewReader(content))
}
// LoadFile creates a TomlTree from a file. // LoadFile creates a TomlTree from a file.
func LoadFile(path string) (tree *TomlTree, err error) { func LoadFile(path string) (tree *TomlTree, err error) {
buff, ferr := ioutil.ReadFile(path) file, err := os.Open(path)
if ferr != nil { if err != nil {
err = ferr return nil, err
} else {
s := string(buff)
tree, err = Load(s)
} }
return defer file.Close()
return LoadReader(file)
} }
+1 -1
View File
@@ -65,7 +65,7 @@ func TestTomlQuery(t *testing.T) {
} }
if tt, ok := values[0].(*TomlTree); !ok { if tt, ok := values[0].(*TomlTree); !ok {
t.Errorf("Expected type of TomlTree: %T Tv", values[0], values[0]) t.Errorf("Expected type of TomlTree: %T", values[0])
} else if tt.Get("a") != int64(1) { } else if tt.Get("a") != int64(1) {
t.Errorf("Expected 'a' with a value 1: %v", tt.Get("a")) t.Errorf("Expected 'a' with a value 1: %v", tt.Get("a"))
} else if tt.Get("b") != int64(2) { } else if tt.Get("b") != int64(2) {