-
Notifications
You must be signed in to change notification settings - Fork 6
/
parser.go
328 lines (264 loc) · 7.92 KB
/
parser.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
package formatting
import (
"fmt"
"strings"
"github.com/mcstatus-io/mcutil/v3/formatting/colors"
"github.com/mcstatus-io/mcutil/v3/formatting/decorators"
)
// Result is a parsed structure of any Minecraft string encoded with color codes or format codes
type Result struct {
Tree []Item `json:"-"`
Raw string `json:"raw"`
Clean string `json:"clean"`
HTML string `json:"html"`
}
// Parse parses the formatting of any string or Chat object
func Parse(input interface{}) (*Result, error) {
tree, err := parseAny(input, nil)
if err != nil {
return nil, err
}
return &Result{
Tree: tree,
Raw: toRaw(tree),
Clean: toClean(tree),
HTML: toHTML(tree),
}, nil
}
func parseAny(input interface{}, parent map[string]interface{}) ([]Item, error) {
result := make([]Item, 0)
switch value := input.(type) {
case map[string]interface{}:
{
// Merge the properties from the parent component into this one.
current := mergeChatProperties(parent, value)
// Only add a new item if there is text in this component, since it is useless
// to push a new item that has no displayable text.
if text, ok := current["text"].(string); ok {
children, err := parseString(text, current)
if err != nil {
return nil, err
}
result = append(result, children...)
}
// Iterate over the child components and pass the current component to
// them so they can inherit our properties.
if extra, ok := current["extra"].([]interface{}); ok {
for _, child := range extra {
extraChildren, err := parseAny(child, current)
if err != nil {
return nil, err
}
result = append(result, extraChildren...)
}
}
break
}
case string:
{
children, err := parseString(value, parent)
if err != nil {
return nil, err
}
// It is possible that a child value of an "extra" property on a chat component may be a string and
// not another chat component, so the parents' properties must be inherited on this string as well.
// See: https://github.com/mcstatus-io/mcutil/issues/6
result = append(result, children...)
break
}
default:
return nil, fmt.Errorf("format: unexpected input type: %+v", input)
}
// Remove component items with an empty text, as they do not do anything to the result other
// than just wasting space.
for i := 0; i < len(result); i++ {
if len(result[i].Text) > 0 {
continue
}
result = append(result[0:i], result[i+1:]...)
i--
}
return result, nil
}
func parseString(text string, props map[string]interface{}) ([]Item, error) {
if strings.Contains(text, "\u00A7") {
// This string uses the old text-based approach to formatting the resulting string. This
// system is incompatible with inheriting any chat component properties, and therefore
// has its own system of formatting. The text must be 'scanned' over instead. Using a
// color formatting code resets all formatting and uses the color code for the proceeding
// text, while using a decorator formatting code (bold, italics, etc.) will apply that
// property without resetting the proceeding text. Using '§r' will reset the text, as
// well as a new line ('\n').
var (
tree []Item = make([]Item, 0)
item Item = Item{
Text: "",
Decorators: make([]decorators.Decorator, 0),
}
r *strings.Reader = strings.NewReader(text)
)
// Iterate over the string as long as there still is text to be processed.
for r.Len() > 0 {
// Read the next character from the reader.
char, n, err := r.ReadRune()
if err != nil {
return nil, err
}
if n < 1 {
break
}
// If the character read is a new-line, push the current item onto the
// list of items, and set the current one to a new line item with no
// properties.
if char == '\n' {
tree = append(tree, item)
item = Item{
Text: "\n",
Decorators: make([]decorators.Decorator, 0),
}
continue
}
// If the current character is also not a formatting code ('§'), add it
// to the current item text without doing anything else.
if char != '\u00A7' {
item.Text += string(char)
continue
}
// Since we now know that formatting is being used, read the next character
// to check what formatting to apply.
code, n, err := r.ReadRune()
if err != nil {
return nil, err
}
if n < 1 {
break
}
switch code {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g':
{
// Parse the color code
color, ok := colors.Parse(code)
if !ok {
break
}
// If the current item has no text or decorators, there is no point
// in doing anything else except just setting the current item to
// the new color.
if len(item.Text) == 0 && len(item.Decorators) == 0 {
item.Color = &color
break
}
// Push the current item into the list of items if there is
// any text on it, before attempting to apply the color to
// the remaining text.
if len(item.Text) > 0 {
tree = append(tree, item)
}
// Reset the current item to use the new color with no extra
// decorators.
item = Item{
Text: "",
Color: &color,
Decorators: make([]decorators.Decorator, 0),
}
break
}
case 'k', 'l', 'm', 'n', 'o':
{
// Parse the formatting code
decorator, ok := decorators.Parse(code)
if !ok {
break
}
// If the current item has no text, there is no point in doing anything
// else except just pushing the decorator to the item.
if len(item.Text) == 0 {
item.Decorators = append(item.Decorators, decorator)
break
}
// Push the current item into the list of items before
// creating another item that uses this decorator.
tree = append(tree, item)
// Reset the current item to use a clone of the current
// item that has the new decorator.
item = Item{
Text: "",
Color: item.Color,
Decorators: append(item.Decorators, decorator),
}
break
}
case 'r':
{
// Do nothing if there is already no text, color, or decorators.
if len(item.Text) == 0 && len(item.Decorators) == 0 && item.Color == nil {
break
}
// Append the current item onto the list of items.
tree = append(tree, item)
// Reset the current item to have no text, decorators, or color.
item = Item{
Text: "",
Decorators: make([]decorators.Decorator, 0),
}
break
}
}
}
return append(tree, item), nil
}
var (
color *colors.Color = nil
decoratorsList []decorators.Decorator = make([]decorators.Decorator, 0)
)
// Color
if rawColor, ok := props["color"]; ok {
parsed, ok := colors.Parse(rawColor)
if ok {
color = &parsed
}
}
// Decorators
for k, v := range decorators.PropertyMap {
if value, ok := props[k]; ok && parseBool(value) {
decoratorsList = append(decoratorsList, v)
}
}
return []Item{
{
Text: text,
Color: color,
Decorators: decoratorsList,
},
}, nil
}
func parseBool(value interface{}) bool {
// Although bool is the most common form of a boolean value used in the chat
// component type, the standard is not properly documented and several types
// are used out in the wild. We must support them all, even if I have never
// seen integers used, does not hurt to add them /shrug
switch v := value.(type) {
case bool:
return v
case string:
return strings.ToLower(v) == "true"
case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
return v == 1
default:
return false
}
}
func mergeChatProperties(parent, child map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range parent {
// The "extra" property cannot be inherited, or this will cause an infinite loop.
if k == "extra" {
continue
}
result[k] = v
}
for k, v := range child {
result[k] = v
}
return result
}