rui_orig/data.go

536 lines
12 KiB
Go

package rui
import (
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"unicode"
)
// DataValueType defines the type of DataNode
type DataValueType int
// Constants which are used to describe a value type, see [DataValue]
const (
// TextDataValue - the DataValue value is a string.
// The String() method of DataValue interface is used to access the value.
TextDataValue DataValueType = 0
// ObjectDataValue - the DataValue value is a object.
// The Object() method of DataValue interface is used to access the object.
ObjectDataValue DataValueType = 1
// ArrayDataNode - the DataValue value is an array of DataValue.
// The Array() method of DataValue interface is used to access the array.
ArrayDataValue DataValueType = 2
)
// DataValue interface of a data node value
type DataValue interface {
fmt.Stringer
writeToBuffer(buffer *strings.Builder, indent string)
// IsObject returns "true" if data value is an object
Type() DataValueType
// Object returns data value as a data object if Type() == ObjectDataValue, and nil otherwise.
Object() DataObject
// Array returns data value as a data array if Type() == ArrayDataValue, and nil otherwise.
Array() DataArray
}
/******************************************************************************/
type dataStringValue struct {
text string
}
// NewTextData creates an DataValue object of TextDataValue type.
//
// The argument is converted to a string using the following functions:
// - int: strconv.FormatInt(value, 10)
// - uint: strconv.FormatUint(value, 10)
// - bool: fmt.Sprintf("%t", value)
// - float: fmt.Sprintf("%g", value)
func NewTextData[T string | int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 | bool | float32 | float64](value T) DataValue {
data := new(dataStringValue)
switch value := any(value).(type) {
case string:
data.text = value
case int:
data.text = strconv.FormatInt(int64(value), 10)
case int8:
data.text = strconv.FormatInt(int64(value), 10)
case int16:
data.text = strconv.FormatInt(int64(value), 10)
case int32:
data.text = strconv.FormatInt(int64(value), 10)
case int64:
data.text = strconv.FormatInt(value, 10)
case uint:
data.text = strconv.FormatUint(uint64(value), 10)
case uint8:
data.text = strconv.FormatUint(uint64(value), 10)
case uint16:
data.text = strconv.FormatUint(uint64(value), 10)
case uint32:
data.text = strconv.FormatUint(uint64(value), 10)
case uint64:
data.text = strconv.FormatUint(uint64(value), 10)
case bool:
data.text = fmt.Sprintf("%t", value)
case float32:
data.text = fmt.Sprintf("%g", float64(value))
case float64:
data.text = fmt.Sprintf("%g", value)
}
return data
}
func (value *dataStringValue) String() string {
return value.text
}
func (value *dataStringValue) writeToBuffer(buffer *strings.Builder, indent string) {
writeRuiText(buffer, value.text, false)
}
func (value *dataStringValue) Type() DataValueType {
return TextDataValue
}
func (value *dataStringValue) Object() DataObject {
return nil
}
func (value *dataStringValue) Array() DataArray {
return nil
}
/******************************************************************************/
type dataParser struct {
data []rune
size int
pos int
line int
lineStart int
}
func (parser *dataParser) skipSpaces(skipNewLine bool) {
for parser.pos < parser.size {
switch parser.data[parser.pos] {
case '\n':
if !skipNewLine {
return
}
parser.line++
parser.lineStart = parser.pos + 1
case '/':
if parser.pos+1 < parser.size {
switch parser.data[parser.pos+1] {
case '/':
parser.pos += 2
for parser.pos < parser.size && parser.data[parser.pos] != '\n' {
parser.pos++
}
parser.pos--
case '*':
parser.pos += 3
for {
if parser.pos >= parser.size {
ErrorLog("Unexpected end of file")
return
}
if parser.data[parser.pos-1] == '*' && parser.data[parser.pos] == '/' {
break
}
if parser.data[parser.pos-1] == '\n' {
parser.line++
parser.lineStart = parser.pos
}
parser.pos++
}
default:
return
}
}
case ' ', '\t':
// do nothing
default:
if !unicode.IsSpace(parser.data[parser.pos]) {
return
}
}
parser.pos++
}
}
func (parser *dataParser) skipSpacesAndCommas() {
parser.skipSpaces(true)
for parser.data[parser.pos] == ',' && parser.pos < parser.size {
parser.pos++
parser.skipSpaces(true)
}
}
func (parser *dataParser) parseTag() (string, error) {
parser.skipSpaces(true)
startPos := parser.pos
switch parser.data[parser.pos] {
case '`':
parser.pos++
startPos++
for parser.data[parser.pos] != '`' {
parser.pos++
if parser.pos >= parser.size {
return string(parser.data[startPos:parser.size]), errors.New("unexpected end of text")
}
}
str := string(parser.data[startPos:parser.pos])
parser.pos++
return str, nil
case '\'', '"':
stopSymbol := parser.data[parser.pos]
parser.pos++
startPos++
slash := false
for stopSymbol != parser.data[parser.pos] {
if parser.data[parser.pos] == '\\' {
parser.pos += 2
slash = true
} else {
parser.pos++
}
if parser.pos >= parser.size {
return string(parser.data[startPos:parser.size]), errors.New("unexpected end of text")
}
}
if !slash {
str := string(parser.data[startPos:parser.pos])
parser.pos++
parser.skipSpaces(false)
return str, nil
}
buffer := make([]rune, parser.pos-startPos+1)
n1 := 0
n2 := startPos
invalidEscape := func() (string, error) {
str := string(parser.data[startPos:parser.pos])
parser.pos++
return str, fmt.Errorf(`invalid escape sequence in "%s" (position %d)`, str, n2-2-startPos)
}
for n2 < parser.pos {
if parser.data[n2] != '\\' {
buffer[n1] = parser.data[n2]
n2++
} else {
n2 += 2
switch parser.data[n2-1] {
case 'n':
buffer[n1] = '\n'
case 'r':
buffer[n1] = '\r'
case 't':
buffer[n1] = '\t'
case '"':
buffer[n1] = '"'
case '\'':
buffer[n1] = '\''
case '\\':
buffer[n1] = '\\'
case 'x', 'X':
if n2+2 > parser.pos {
return invalidEscape()
}
x := 0
for range 2 {
ch := parser.data[n2]
if ch >= '0' && ch <= '9' {
x = x*16 + int(ch-'0')
} else if ch >= 'a' && ch <= 'f' {
x = x*16 + int(ch-'a'+10)
} else if ch >= 'A' && ch <= 'F' {
x = x*16 + int(ch-'A'+10)
} else {
return invalidEscape()
}
n2++
}
buffer[n1] = rune(x)
case 'u', 'U':
if n2+4 > parser.pos {
return invalidEscape()
}
x := 0
for range 4 {
ch := parser.data[n2]
if ch >= '0' && ch <= '9' {
x = x*16 + int(ch-'0')
} else if ch >= 'a' && ch <= 'f' {
x = x*16 + int(ch-'a'+10)
} else if ch >= 'A' && ch <= 'F' {
x = x*16 + int(ch-'A'+10)
} else {
return invalidEscape()
}
n2++
}
buffer[n1] = rune(x)
default:
str := string(parser.data[startPos:parser.pos])
return str, fmt.Errorf(`invalid escape sequence in "%s" (position %d)`, str, n2-2-startPos)
}
}
n1++
}
parser.pos++
parser.skipSpaces(false)
return string(buffer[0:n1]), nil
}
for parser.pos < parser.size && !parser.stopSymbol(parser.data[parser.pos]) {
parser.pos++
}
endPos := parser.pos
parser.skipSpaces(false)
if startPos == endPos {
//ErrorLog("empty tag")
return "", nil
}
return string(parser.data[startPos:endPos]), nil
}
func (parser *dataParser) stopSymbol(symbol rune) bool {
return unicode.IsSpace(symbol) ||
slices.Contains([]rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'}, symbol)
}
func (parser *dataParser) parseNode() (string, DataValue, error) {
tag, err := parser.parseTag()
if err != nil {
return "", nil, err
}
parser.skipSpaces(true)
if parser.data[parser.pos] != '=' {
return "", nil, fmt.Errorf("expected '=' after a tag name (line: %d, position: %d)", parser.line, parser.pos-parser.lineStart)
}
parser.pos++
parser.skipSpaces(true)
switch parser.data[parser.pos] {
case '[':
array, err := parser.parseArray()
if err != nil {
return "", nil, err
}
return tag, NewDataArray(array), nil
case '{':
obj, err := parser.parseObject("_")
if err != nil {
return "", nil, err
}
return tag, obj, nil
case '}', ']', '=':
return "", nil, fmt.Errorf(`expected '[', '{' or a tag name after '=' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart)
default:
text, err := parser.parseTag()
if err != nil {
return "", nil, err
}
if parser.data[parser.pos] == '{' {
obj, err := parser.parseObject(text)
if err != nil {
return "", nil, err
}
return tag, obj, nil
}
return tag, NewTextData(text), nil
}
}
func (parser *dataParser) parseObject(tag string) (DataObject, error) {
if parser.data[parser.pos] != '{' {
return nil, fmt.Errorf(`expected '{' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart)
}
parser.pos++
obj := NewDataObject(tag)
for parser.pos < parser.size {
parser.skipSpaces(true)
if parser.data[parser.pos] == '}' {
parser.pos++
parser.skipSpaces(false)
return obj, nil
}
property, value, err := parser.parseNode()
if err != nil {
return nil, err
}
obj.SetProperty(property, value)
switch parser.data[parser.pos] {
case '}':
parser.pos++
parser.skipSpaces(true)
return obj, nil
case ',':
parser.pos++
case '\n':
// do nothing
default:
return nil, fmt.Errorf(`expected '}', '\n' or ',' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart)
}
parser.skipSpacesAndCommas()
}
return nil, errors.New("unexpected end of text")
}
func (parser *dataParser) parseArray() ([]DataValue, error) {
parser.pos++
parser.skipSpaces(true)
array := []DataValue{}
parser.skipSpacesAndCommas()
for parser.pos < parser.size {
if parser.data[parser.pos] != ']' {
if parser.data[parser.pos] == '[' {
subarray, err := parser.parseArray()
if err != nil {
return nil, err
}
array = append(array, NewDataArray(subarray))
} else {
tag, err := parser.parseTag()
if err != nil {
return nil, err
}
if parser.data[parser.pos] == '{' {
obj, err := parser.parseObject(tag)
if err != nil {
return nil, err
}
array = append(array, obj)
} else {
array = append(array, NewTextData(tag))
}
}
}
switch parser.data[parser.pos] {
case ']':
parser.pos++
parser.skipSpaces(true)
return array, nil
case '\n':
// do nothing
case ',':
parser.pos++
default:
return nil, fmt.Errorf(`expected ']' or ',' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart)
}
parser.skipSpacesAndCommas()
}
return nil, errors.New("unexpected end of text")
}
// ParseDataText parses a text.
// If successful, it returns a DataObject and nil as the second element;
// otherwise, it returns nil and an error.
func ParseDataText(text string) (DataObject, error) {
if strings.ContainsAny(text, "\r") {
text = strings.ReplaceAll(text, "\r\n", "\n")
text = strings.ReplaceAll(text, "\r", "\n")
}
parser := dataParser{
data: append([]rune(text), rune(0)),
pos: 0,
line: 1,
lineStart: 0,
}
parser.size = len(parser.data) - 1
tag, err := parser.parseTag()
if err != nil {
return nil, err
}
return parser.parseObject(tag)
}
// ReadDataObject reads a text file from the specified path, parses it.
// If successful, it returns a DataObject and nil as the second element;
// otherwise, it returns nil and an error.
func ReadDataObject(path string) (DataObject, error) {
data, err := os.ReadFile(path)
if err != nil {
debugLog(err.Error())
return nil, err
}
return ParseDataText(string(data))
}