mirror of https://github.com/antonmedv/fx
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
387 lines
7.3 KiB
Go
387 lines
7.3 KiB
Go
package jsonx
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/antonmedv/fx/internal/utils"
|
|
)
|
|
|
|
type jsonParser struct {
|
|
data []byte
|
|
end int
|
|
lastChar byte
|
|
lineNumber uint
|
|
sourceTail *ring
|
|
depth uint8
|
|
skipFirstIdent bool
|
|
}
|
|
|
|
func Parse(data []byte) (head *Node, err error) {
|
|
p := &jsonParser{
|
|
data: data,
|
|
lineNumber: 1,
|
|
sourceTail: &ring{},
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = p.errorSnippet(fmt.Sprintf("%v", r))
|
|
}
|
|
}()
|
|
p.next()
|
|
var next *Node
|
|
for p.lastChar != 0 {
|
|
value := p.parseValue()
|
|
if head == nil {
|
|
head = value
|
|
next = head
|
|
} else {
|
|
value.Index = -1
|
|
next.adjacent(value)
|
|
next = value
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (p *jsonParser) next() {
|
|
if p.end < len(p.data) {
|
|
p.lastChar = p.data[p.end]
|
|
p.end++
|
|
} else {
|
|
p.lastChar = 0
|
|
}
|
|
if p.lastChar == '\n' {
|
|
p.lineNumber++
|
|
}
|
|
p.sourceTail.writeByte(p.lastChar)
|
|
}
|
|
|
|
func (p *jsonParser) parseValue() *Node {
|
|
p.skipWhitespace()
|
|
|
|
var l *Node
|
|
switch p.lastChar {
|
|
case '"':
|
|
l = p.parseString()
|
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-':
|
|
l = p.parseNumber()
|
|
case '{':
|
|
l = p.parseObject()
|
|
case '[':
|
|
l = p.parseArray()
|
|
case 't':
|
|
l = p.parseKeyword("true")
|
|
case 'f':
|
|
l = p.parseKeyword("false")
|
|
case 'n':
|
|
l = p.parseKeyword("null")
|
|
default:
|
|
panic(fmt.Sprintf("Unexpected character %q", p.lastChar))
|
|
}
|
|
|
|
p.skipWhitespace()
|
|
return l
|
|
}
|
|
|
|
func (p *jsonParser) parseString() *Node {
|
|
str := &Node{Depth: p.depth}
|
|
start := p.end - 1
|
|
p.next()
|
|
escaped := false
|
|
for {
|
|
if escaped {
|
|
switch p.lastChar {
|
|
case 'u':
|
|
var unicode string
|
|
for i := 0; i < 4; i++ {
|
|
p.next()
|
|
if !utils.IsHexDigit(p.lastChar) {
|
|
panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s%c'", unicode, p.lastChar))
|
|
}
|
|
unicode += string(p.lastChar)
|
|
}
|
|
_, err := strconv.ParseInt(unicode, 16, 32)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s'", unicode))
|
|
}
|
|
case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':
|
|
default:
|
|
panic(fmt.Sprintf("Invalid escape sequence '\\%c'", p.lastChar))
|
|
}
|
|
escaped = false
|
|
} else if p.lastChar == '\\' {
|
|
escaped = true
|
|
} else if p.lastChar == '"' {
|
|
break
|
|
} else if p.lastChar == 0 {
|
|
panic("Unexpected end of input in string")
|
|
} else if p.lastChar < 0x1F {
|
|
panic(fmt.Sprintf("Invalid character %q in string", p.lastChar))
|
|
}
|
|
p.next()
|
|
}
|
|
|
|
str.Value = p.data[start:p.end]
|
|
p.next()
|
|
return str
|
|
}
|
|
|
|
func (p *jsonParser) parseNumber() *Node {
|
|
num := &Node{Depth: p.depth}
|
|
start := p.end - 1
|
|
|
|
// Handle negative numbers
|
|
if p.lastChar == '-' {
|
|
p.next()
|
|
if !utils.IsDigit(p.lastChar) {
|
|
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
|
|
}
|
|
}
|
|
|
|
// Leading zero
|
|
if p.lastChar == '0' {
|
|
p.next()
|
|
} else {
|
|
for utils.IsDigit(p.lastChar) {
|
|
p.next()
|
|
}
|
|
}
|
|
|
|
// Decimal portion
|
|
if p.lastChar == '.' {
|
|
p.next()
|
|
if !utils.IsDigit(p.lastChar) {
|
|
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
|
|
}
|
|
for utils.IsDigit(p.lastChar) {
|
|
p.next()
|
|
}
|
|
}
|
|
|
|
// Exponent
|
|
if p.lastChar == 'e' || p.lastChar == 'E' {
|
|
p.next()
|
|
if p.lastChar == '+' || p.lastChar == '-' {
|
|
p.next()
|
|
}
|
|
if !utils.IsDigit(p.lastChar) {
|
|
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
|
|
}
|
|
for utils.IsDigit(p.lastChar) {
|
|
p.next()
|
|
}
|
|
}
|
|
|
|
num.Value = p.data[start : p.end-1]
|
|
return num
|
|
}
|
|
|
|
func (p *jsonParser) parseObject() *Node {
|
|
object := &Node{Depth: p.depth}
|
|
object.Value = []byte{'{'}
|
|
|
|
p.next()
|
|
p.skipWhitespace()
|
|
|
|
// Empty object
|
|
if p.lastChar == '}' {
|
|
object.Value = append(object.Value, '}')
|
|
p.next()
|
|
return object
|
|
}
|
|
|
|
for {
|
|
// Expecting a key which should be a string
|
|
if p.lastChar != '"' {
|
|
panic(fmt.Sprintf("Expected object key to be a string, got %q", p.lastChar))
|
|
}
|
|
|
|
p.depth++
|
|
key := p.parseString()
|
|
key.Key, key.Value = key.Value, nil
|
|
object.Size += 1
|
|
key.directParent = object
|
|
|
|
p.skipWhitespace()
|
|
|
|
// Expecting colon after key
|
|
if p.lastChar != ':' {
|
|
panic(fmt.Sprintf("Expected colon after object key, got %q", p.lastChar))
|
|
}
|
|
|
|
p.next()
|
|
|
|
p.skipFirstIdent = true
|
|
value := p.parseValue()
|
|
p.depth--
|
|
|
|
key.Value = value.Value
|
|
key.Size = value.Size
|
|
key.Next = value.Next
|
|
if key.Next != nil {
|
|
key.Next.Prev = key
|
|
}
|
|
key.End = value.End
|
|
value.indirectParent = key
|
|
object.append(key)
|
|
|
|
p.skipWhitespace()
|
|
|
|
if p.lastChar == ',' {
|
|
object.End.Comma = true
|
|
p.next()
|
|
p.skipWhitespace()
|
|
if p.lastChar == '}' {
|
|
object.End.Comma = false
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if p.lastChar == '}' {
|
|
closeBracket := &Node{Depth: p.depth}
|
|
closeBracket.Value = []byte{'}'}
|
|
closeBracket.directParent = object
|
|
closeBracket.Index = -1
|
|
object.append(closeBracket)
|
|
p.next()
|
|
return object
|
|
}
|
|
|
|
panic(fmt.Sprintf("Unexpected character %q in object", p.lastChar))
|
|
}
|
|
}
|
|
|
|
func (p *jsonParser) parseArray() *Node {
|
|
arr := &Node{Depth: p.depth}
|
|
arr.Value = []byte{'['}
|
|
|
|
p.next()
|
|
p.skipWhitespace()
|
|
|
|
if p.lastChar == ']' {
|
|
arr.Value = append(arr.Value, ']')
|
|
p.next()
|
|
return arr
|
|
}
|
|
|
|
for i := 0; ; i++ {
|
|
p.depth++
|
|
value := p.parseValue()
|
|
value.directParent = arr
|
|
arr.Size += 1
|
|
value.Index = i
|
|
p.depth--
|
|
|
|
arr.append(value)
|
|
p.skipWhitespace()
|
|
|
|
if p.lastChar == ',' {
|
|
arr.End.Comma = true
|
|
p.next()
|
|
p.skipWhitespace()
|
|
if p.lastChar == ']' {
|
|
arr.End.Comma = false
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if p.lastChar == ']' {
|
|
closeBracket := &Node{Depth: p.depth}
|
|
closeBracket.Value = []byte{']'}
|
|
closeBracket.directParent = arr
|
|
closeBracket.Index = -1
|
|
arr.append(closeBracket)
|
|
p.next()
|
|
return arr
|
|
}
|
|
|
|
panic(fmt.Sprintf("Invalid character %q in array", p.lastChar))
|
|
}
|
|
}
|
|
|
|
func (p *jsonParser) parseKeyword(name string) *Node {
|
|
for i := 1; i < len(name); i++ {
|
|
p.next()
|
|
if p.lastChar != name[i] {
|
|
panic(fmt.Sprintf("Unexpected character %q in keyword", p.lastChar))
|
|
}
|
|
}
|
|
p.next()
|
|
|
|
nextCharIsSpecial := isWhitespace(p.lastChar) || p.lastChar == ',' || p.lastChar == '}' || p.lastChar == ']' || p.lastChar == 0
|
|
if nextCharIsSpecial {
|
|
keyword := &Node{Depth: p.depth}
|
|
keyword.Value = []byte(name)
|
|
return keyword
|
|
}
|
|
|
|
panic(fmt.Sprintf("Unexpected character %q in keyword", p.lastChar))
|
|
}
|
|
|
|
func isWhitespace(ch byte) bool {
|
|
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
|
|
}
|
|
|
|
func (p *jsonParser) skipWhitespace() {
|
|
for {
|
|
switch p.lastChar {
|
|
case ' ', '\t', '\n', '\r':
|
|
p.next()
|
|
case '/':
|
|
p.skipComment()
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *jsonParser) skipComment() {
|
|
p.next()
|
|
switch p.lastChar {
|
|
case '/':
|
|
for p.lastChar != '\n' && p.lastChar != 0 {
|
|
p.next()
|
|
}
|
|
case '*':
|
|
for {
|
|
p.next()
|
|
if p.lastChar == '*' {
|
|
p.next()
|
|
if p.lastChar == '/' {
|
|
p.next()
|
|
return
|
|
}
|
|
}
|
|
if p.lastChar == 0 {
|
|
panic("Unexpected end of input in comment")
|
|
}
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("Invalid comment: '/%c'", p.lastChar))
|
|
}
|
|
}
|
|
|
|
func (p *jsonParser) errorSnippet(message string) error {
|
|
lines := strings.Split(p.sourceTail.string(), "\n")
|
|
lastLineLen := utf8.RuneCountInString(lines[len(lines)-1])
|
|
pointer := strings.Repeat(".", lastLineLen-1) + "^"
|
|
lines = append(lines, pointer)
|
|
|
|
paddedLines := make([]string, len(lines))
|
|
for i, line := range lines {
|
|
paddedLines[i] = " " + line
|
|
}
|
|
|
|
return fmt.Errorf(
|
|
"%s on line %d.\n\n%s\n\n",
|
|
message,
|
|
p.lineNumber,
|
|
strings.Join(paddedLines, "\n"),
|
|
)
|
|
}
|