-
Notifications
You must be signed in to change notification settings - Fork 0
/
parser.go
254 lines (227 loc) · 7.13 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
package main
import (
"bytes"
"database/sql"
"errors"
"html"
"log"
"strings"
)
var (
// Schools are abbreviated in xml file, we want full text
// in our db
schools = map[string]string{
"A": "Abjuration",
"C": "Conjuration",
"D": "Divination",
"EN": "Enchantment",
"EV": "Evocation",
"I": "Illusion",
"N": "Necromancy",
"T": "Transmutation",
}
)
const (
// PHBid is the Player's Handbook id in our db
PHBid = 1
// EEid is the Elemental Evil id in our db
EEid = 2
// SCAGid is the Sword Coast Adventurer's guide id in our db
SCAGid = 3
)
// Spell represents our database version of a spell
type Spell struct {
ID int `db:"id"`
Name string `db:"name"`
Level string `db:"level"`
School string `db:"school"`
CastTime string `db:"cast_time"`
Duration string `db:"duration"`
Range string `db:"range"`
Verbal bool `db:"comp_verbal"`
Somatic bool `db:"comp_somatic"`
Material bool `db:"comp_material"`
MaterialDesc sql.NullString `db:"material_desc"`
Concentration bool `db:"concentration"`
Ritual bool `db:"ritual"`
Description string `db:"description"`
SourceID int `db:"source_id"`
}
// Class represents our database Class table
type Class struct {
ID int `db:"id"`
Name string `db:"name"`
BaseClass sql.NullInt64 `db:"base_class_id"`
SourceID int `db:"source_id"`
}
// XMLSpell represents a <spell> element from our xml file
//
// Struct tags in Go (the stuff between the `'s) are used commonly
// by encoding packages. Here we're telling the xml package how we
// want it to parse into our struct. Each time the xml parser tries to parse an
// element into this struct, it looks for struct tags here that correspond to
// the names of its child elements. When it finds them, it puts the text
// contents of those elements into the corresponding struct fields.
type XMLSpell struct {
Name string `xml:"name"`
Level string `xml:"level"`
School string `xml:"school"`
Ritual string `xml:"ritual"`
Time string `xml:"time"`
Range string `xml:"range"`
Components string `xml:"components"`
Duration string `xml:"duration"`
Classes string `xml:"classes"`
Texts []string `xml:"text"`
}
// Compendium represents our top level <compendium> element
type Compendium struct {
XMLSpells []XMLSpell `xml:"spell"`
}
// Components is needed because the xml file has everything on one darn line
// e.g. "V, S, M (blah blah)"
// And we care about data atomicity.
type components struct {
Verb bool
Som bool
Mat bool
Matdesc sql.NullString
}
// ToDbSpell parses the data from `x` into a new Spell object
// which it returns, along with an error. In the event of an error,
// a zero-valued Spell is returned.
func (x *XMLSpell) ToDbSpell() (Spell, error) {
// vars we need to do a little work for
// to convert
var school, desc string
var concentration, ritual bool
var comps components
sourceID := PHBid //default to phb
if strings.Contains(x.Name, "(EE)") {
sourceID = EEid
}
if strings.Contains(x.Name, "(SCAG)") {
sourceID = SCAGid
}
// We want the long version, not the abbreviation
if s, ok := schools[x.School]; ok {
school = s
} else {
return Spell{}, errors.New("Not in schools map")
}
var b bytes.Buffer
// The texts slice holds a slice of strings representing the spell
// description from the xml file. <text/> elements are used in the file
// to create newlines, here we replace them with good old \n's
for _, text := range x.Texts {
if text == "" {
b.Write([]byte("\n\n"))
}
if text != "" {
b.Write([]byte(html.EscapeString(text)))
}
// This is dirty, but the file doesn't have a field
// for concentration, only way to find it is to see
// if the description mentions it.
if strings.Contains(text, "concentration") {
concentration = true
}
}
desc = b.String()
comps = x.parseComponents()
// In the file, ritual will be either "" or "YES"
ritual = strings.Compare(x.Ritual, "YES") == 0
d := Spell{
Name: trimSourceFromName(x.Name),
Level: x.Level,
School: school,
CastTime: x.Time,
Duration: x.Duration,
Range: x.Range,
Verbal: comps.Verb,
Somatic: comps.Som,
Material: comps.Mat,
MaterialDesc: comps.Matdesc,
Concentration: concentration,
Ritual: ritual,
Description: desc,
SourceID: sourceID,
}
return d, nil
}
// ParseClasses converts the XMLSpell's string of comma separated
// classes into a slice of Class objects fully initialized with
// ID and BaseClass values, ready to be inserted into our db.
func (x *XMLSpell) ParseClasses() ([]Class, bool) {
cs := []Class{}
split := strings.Split(x.Classes, ", ")
for _, s := range split {
// here Classes is a map found in classes.go
// not in this file because it's long and ugly
if c, ok := Classes[s]; ok {
cs = append(cs, c)
} else {
return []Class{}, false
}
}
return cs, true
}
// parseComponents parses the information in the xml file's Components
// string into a Components struct literal
func (x *XMLSpell) parseComponents() components {
var verb, som, mat bool
var matdesc sql.NullString
// really taking advantage of the fact that spell descriptions are all
// lower case
verb = strings.Contains(x.Components, "V")
som = strings.Contains(x.Components, "S")
mat = strings.Contains(x.Components, "M")
// ('s only ocurr in our domain as the beginning of the material description
// Index returns -1 if not present
i := strings.Index(x.Components, "(")
if i > -1 {
// extract "inside parens" from "blah blah (inside parens)"
desc := x.Components[i+1 : len(x.Components)-1]
// Descriptions are all lower case, make them look prettier
// by capitalizing the first letter
cdesc, ok := capitalizeAtIndex(desc, 0)
if !ok {
log.Printf("Error capitalizing %v at index %d\n", desc, 0)
}
matdesc = toNullString(cdesc)
}
return components{
Verb: verb,
Som: som,
Mat: mat,
Matdesc: matdesc,
}
}
////////////////////////////////////////////////////////////////////////////////
//
// Utils
//
////////////////////////////////////////////////////////////////////////////////
func trimSourceFromName(name string) string {
s := strings.NewReplacer(" (EE)", "", " (SCAG)", "")
return s.Replace(name)
}
// toNullString converts a regular string to a sql.NullString
func toNullString(s string) sql.NullString {
return sql.NullString{String: s, Valid: s != ""}
}
// capitalizeAtIndex capitalizes a single char from a string at specified index
// If an error is encountered (normally index being out of range),
// ok will be set to false and the original string returned unaltered
func capitalizeAtIndex(s string, i int) (string, bool) {
if i < 0 || i > len(s)-1 {
return s, false
}
// TODO: Fix this ugly inefficient crap
out := []rune(s)
badstr := string(out[i])
goodstr := strings.ToUpper(badstr)
goodrune := []rune(goodstr)
out[i] = goodrune[0]
return string(out), true
}