-
Notifications
You must be signed in to change notification settings - Fork 2
/
reader.go
155 lines (130 loc) · 4.08 KB
/
reader.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package keyvalues
import (
"bufio"
"encoding/csv"
"io"
"strings"
)
const tokenEnterScope = "{"
const tokenExitScope = "}"
const tokenEscape = "\""
const tokenDiscardCutset = "\t \r\n"
const tokenSeparator = " "
const tokenTab = "\t"
const tokenComment = "//"
const tokenRootNodeKey = "$root"
// Reader is used for parsing a KeyValue format stream
// There are various KeyValue based formats (vmt, vmf, gameinfo.txt etc.)
// This should be able to parse all of them.
type Reader struct {
file io.Reader
}
// NewReader Return a new Vmf Reader
func NewReader(file io.Reader) Reader {
reader := Reader{}
reader.file = file
return reader
}
// Read buffer file into our defined structures
// Returns a fully mapped Vmf structure
// Every root KeyValue is contained in a predefined root node, due to spec lacking clarity
// about the number of valid root nodes. This assumes there can be more than 1
func (reader *Reader) Read() (keyvalue KeyValue, err error) {
bufReader := bufio.NewReader(reader.file)
rootNode := KeyValue{
key: tokenRootNodeKey,
valueType: ValueArray,
parent: nil,
}
readScope(bufReader, &rootNode)
if rootNode.HasChildren() && len(rootNode.value) == 1 {
root := rootNode.value[0].(*KeyValue)
return *root, nil
}
return rootNode, err
}
// readScope Reads a single scope
// Constructs a KeyValue node tree for a single scope
// Recursively parses all child scopes too
// Param: scope is the current scope to write to
func readScope(reader *bufio.Reader, scope *KeyValue) *KeyValue {
for {
line, err := reader.ReadString('\n')
if err == io.EOF {
break
}
// Remove any comments
line = strings.Split(line, tokenComment)[0]
// trim padding
line = strings.Trim(line, tokenDiscardCutset)
// Simplify parsing the line
line = strings.Replace(line, tokenTab, tokenSeparator, -1)
if len(line) == 0 {
continue
}
// New scope
if strings.Contains(line, tokenEnterScope) && !isCharacterEscaped(line, tokenEnterScope) {
// Scope is opened when the key is read
// There may be situations where there is no key, so we must account for that
subScope := scope.value[len(scope.value)-1].(*KeyValue)
scope.value = append(scope.value[:len(scope.value)-1], readScope(reader, subScope))
continue
}
// Exit scope
if strings.Contains(line, tokenExitScope) {
break
}
// Read the scope, but parse it as CSV to remove the quotes and split in the correct place
// Without parsing it as a CSV, it will split on the first space, not the first unquoted space
r := csv.NewReader(strings.NewReader(line))
r.Comma = rune(tokenSeparator[0])
prop, err := r.Read()
// Only the key is defined here
// This *SHOULD* mean key has children
if len(prop) == 1 {
//Create new scope
kv := &KeyValue{
key: trim(prop[0]),
valueType: ValueArray,
parent: scope,
}
scope.value = append(scope.value, kv)
continue
}
// Read keyvalue & append to current scope
results := parseKV(line)
for idx := range results {
results[idx].parent = scope
scope.value = append(scope.value, results[idx])
}
}
return scope
}
// parseKV reads a single line that should contain a KeyValue pair
func parseKV(line string) (res []*KeyValue) {
prop := strings.Split(line, tokenSeparator)
// value also defined on this line
vals := strings.Split(trim(strings.Replace(line, prop[0], "", -1)), "\r")
res = append(res, &KeyValue{
key: trim(prop[0]),
valueType: getType(trim(vals[0])),
value: append(make([]interface{}, 0), trim(vals[0])),
})
// Hack to catch \r carriage returns
if len(vals) == 2 {
prop := strings.Split(trim(vals[1]), tokenSeparator)
val2 := trim(strings.Replace(vals[1], prop[0], "", -1))
res = append(res, &KeyValue{
key: strings.Replace(trim(prop[0]), "\"", "", -1),
valueType: getType(val2),
value: append(make([]interface{}, 0), val2),
})
}
return res
}
func isCharacterEscaped(value string, char string) bool {
return strings.LastIndex(value, tokenEscape) >= strings.LastIndex(value, char)
}
func trim(value string) string {
return strings.Trim(strings.TrimSpace(value), tokenEscape)
}