|
1 package toml |
|
2 |
|
3 import ( |
|
4 "bytes" |
|
5 "fmt" |
|
6 "io" |
|
7 "math" |
|
8 "reflect" |
|
9 "sort" |
|
10 "strconv" |
|
11 "strings" |
|
12 "time" |
|
13 ) |
|
14 |
|
15 // Encodes a string to a TOML-compliant multi-line string value |
|
16 // This function is a clone of the existing encodeTomlString function, except that whitespace characters |
|
17 // are preserved. Quotation marks and backslashes are also not escaped. |
|
18 func encodeMultilineTomlString(value string) string { |
|
19 var b bytes.Buffer |
|
20 |
|
21 for _, rr := range value { |
|
22 switch rr { |
|
23 case '\b': |
|
24 b.WriteString(`\b`) |
|
25 case '\t': |
|
26 b.WriteString("\t") |
|
27 case '\n': |
|
28 b.WriteString("\n") |
|
29 case '\f': |
|
30 b.WriteString(`\f`) |
|
31 case '\r': |
|
32 b.WriteString("\r") |
|
33 case '"': |
|
34 b.WriteString(`"`) |
|
35 case '\\': |
|
36 b.WriteString(`\`) |
|
37 default: |
|
38 intRr := uint16(rr) |
|
39 if intRr < 0x001F { |
|
40 b.WriteString(fmt.Sprintf("\\u%0.4X", intRr)) |
|
41 } else { |
|
42 b.WriteRune(rr) |
|
43 } |
|
44 } |
|
45 } |
|
46 return b.String() |
|
47 } |
|
48 |
|
49 // Encodes a string to a TOML-compliant string value |
|
50 func encodeTomlString(value string) string { |
|
51 var b bytes.Buffer |
|
52 |
|
53 for _, rr := range value { |
|
54 switch rr { |
|
55 case '\b': |
|
56 b.WriteString(`\b`) |
|
57 case '\t': |
|
58 b.WriteString(`\t`) |
|
59 case '\n': |
|
60 b.WriteString(`\n`) |
|
61 case '\f': |
|
62 b.WriteString(`\f`) |
|
63 case '\r': |
|
64 b.WriteString(`\r`) |
|
65 case '"': |
|
66 b.WriteString(`\"`) |
|
67 case '\\': |
|
68 b.WriteString(`\\`) |
|
69 default: |
|
70 intRr := uint16(rr) |
|
71 if intRr < 0x001F { |
|
72 b.WriteString(fmt.Sprintf("\\u%0.4X", intRr)) |
|
73 } else { |
|
74 b.WriteRune(rr) |
|
75 } |
|
76 } |
|
77 } |
|
78 return b.String() |
|
79 } |
|
80 |
|
81 func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElementPerLine bool) (string, error) { |
|
82 // this interface check is added to dereference the change made in the writeTo function. |
|
83 // That change was made to allow this function to see formatting options. |
|
84 tv, ok := v.(*tomlValue) |
|
85 if ok { |
|
86 v = tv.value |
|
87 } else { |
|
88 tv = &tomlValue{} |
|
89 } |
|
90 |
|
91 switch value := v.(type) { |
|
92 case uint64: |
|
93 return strconv.FormatUint(value, 10), nil |
|
94 case int64: |
|
95 return strconv.FormatInt(value, 10), nil |
|
96 case float64: |
|
97 // Ensure a round float does contain a decimal point. Otherwise feeding |
|
98 // the output back to the parser would convert to an integer. |
|
99 if math.Trunc(value) == value { |
|
100 return strings.ToLower(strconv.FormatFloat(value, 'f', 1, 32)), nil |
|
101 } |
|
102 return strings.ToLower(strconv.FormatFloat(value, 'f', -1, 32)), nil |
|
103 case string: |
|
104 if tv.multiline { |
|
105 return "\"\"\"\n" + encodeMultilineTomlString(value) + "\"\"\"", nil |
|
106 } |
|
107 return "\"" + encodeTomlString(value) + "\"", nil |
|
108 case []byte: |
|
109 b, _ := v.([]byte) |
|
110 return tomlValueStringRepresentation(string(b), indent, arraysOneElementPerLine) |
|
111 case bool: |
|
112 if value { |
|
113 return "true", nil |
|
114 } |
|
115 return "false", nil |
|
116 case time.Time: |
|
117 return value.Format(time.RFC3339), nil |
|
118 case nil: |
|
119 return "", nil |
|
120 } |
|
121 |
|
122 rv := reflect.ValueOf(v) |
|
123 |
|
124 if rv.Kind() == reflect.Slice { |
|
125 var values []string |
|
126 for i := 0; i < rv.Len(); i++ { |
|
127 item := rv.Index(i).Interface() |
|
128 itemRepr, err := tomlValueStringRepresentation(item, indent, arraysOneElementPerLine) |
|
129 if err != nil { |
|
130 return "", err |
|
131 } |
|
132 values = append(values, itemRepr) |
|
133 } |
|
134 if arraysOneElementPerLine && len(values) > 1 { |
|
135 stringBuffer := bytes.Buffer{} |
|
136 valueIndent := indent + ` ` // TODO: move that to a shared encoder state |
|
137 |
|
138 stringBuffer.WriteString("[\n") |
|
139 |
|
140 for _, value := range values { |
|
141 stringBuffer.WriteString(valueIndent) |
|
142 stringBuffer.WriteString(value) |
|
143 stringBuffer.WriteString(`,`) |
|
144 stringBuffer.WriteString("\n") |
|
145 } |
|
146 |
|
147 stringBuffer.WriteString(indent + "]") |
|
148 |
|
149 return stringBuffer.String(), nil |
|
150 } |
|
151 return "[" + strings.Join(values, ",") + "]", nil |
|
152 } |
|
153 return "", fmt.Errorf("unsupported value type %T: %v", v, v) |
|
154 } |
|
155 |
|
156 func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) { |
|
157 simpleValuesKeys := make([]string, 0) |
|
158 complexValuesKeys := make([]string, 0) |
|
159 |
|
160 for k := range t.values { |
|
161 v := t.values[k] |
|
162 switch v.(type) { |
|
163 case *Tree, []*Tree: |
|
164 complexValuesKeys = append(complexValuesKeys, k) |
|
165 default: |
|
166 simpleValuesKeys = append(simpleValuesKeys, k) |
|
167 } |
|
168 } |
|
169 |
|
170 sort.Strings(simpleValuesKeys) |
|
171 sort.Strings(complexValuesKeys) |
|
172 |
|
173 for _, k := range simpleValuesKeys { |
|
174 v, ok := t.values[k].(*tomlValue) |
|
175 if !ok { |
|
176 return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k]) |
|
177 } |
|
178 |
|
179 repr, err := tomlValueStringRepresentation(v, indent, arraysOneElementPerLine) |
|
180 if err != nil { |
|
181 return bytesCount, err |
|
182 } |
|
183 |
|
184 if v.comment != "" { |
|
185 comment := strings.Replace(v.comment, "\n", "\n"+indent+"#", -1) |
|
186 start := "# " |
|
187 if strings.HasPrefix(comment, "#") { |
|
188 start = "" |
|
189 } |
|
190 writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment, "\n") |
|
191 bytesCount += int64(writtenBytesCountComment) |
|
192 if errc != nil { |
|
193 return bytesCount, errc |
|
194 } |
|
195 } |
|
196 |
|
197 var commented string |
|
198 if v.commented { |
|
199 commented = "# " |
|
200 } |
|
201 writtenBytesCount, err := writeStrings(w, indent, commented, k, " = ", repr, "\n") |
|
202 bytesCount += int64(writtenBytesCount) |
|
203 if err != nil { |
|
204 return bytesCount, err |
|
205 } |
|
206 } |
|
207 |
|
208 for _, k := range complexValuesKeys { |
|
209 v := t.values[k] |
|
210 |
|
211 combinedKey := k |
|
212 if keyspace != "" { |
|
213 combinedKey = keyspace + "." + combinedKey |
|
214 } |
|
215 var commented string |
|
216 if t.commented { |
|
217 commented = "# " |
|
218 } |
|
219 |
|
220 switch node := v.(type) { |
|
221 // node has to be of those two types given how keys are sorted above |
|
222 case *Tree: |
|
223 tv, ok := t.values[k].(*Tree) |
|
224 if !ok { |
|
225 return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k]) |
|
226 } |
|
227 if tv.comment != "" { |
|
228 comment := strings.Replace(tv.comment, "\n", "\n"+indent+"#", -1) |
|
229 start := "# " |
|
230 if strings.HasPrefix(comment, "#") { |
|
231 start = "" |
|
232 } |
|
233 writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment) |
|
234 bytesCount += int64(writtenBytesCountComment) |
|
235 if errc != nil { |
|
236 return bytesCount, errc |
|
237 } |
|
238 } |
|
239 writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n") |
|
240 bytesCount += int64(writtenBytesCount) |
|
241 if err != nil { |
|
242 return bytesCount, err |
|
243 } |
|
244 bytesCount, err = node.writeTo(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine) |
|
245 if err != nil { |
|
246 return bytesCount, err |
|
247 } |
|
248 case []*Tree: |
|
249 for _, subTree := range node { |
|
250 writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n") |
|
251 bytesCount += int64(writtenBytesCount) |
|
252 if err != nil { |
|
253 return bytesCount, err |
|
254 } |
|
255 |
|
256 bytesCount, err = subTree.writeTo(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine) |
|
257 if err != nil { |
|
258 return bytesCount, err |
|
259 } |
|
260 } |
|
261 } |
|
262 } |
|
263 |
|
264 return bytesCount, nil |
|
265 } |
|
266 |
|
267 func writeStrings(w io.Writer, s ...string) (int, error) { |
|
268 var n int |
|
269 for i := range s { |
|
270 b, err := io.WriteString(w, s[i]) |
|
271 n += b |
|
272 if err != nil { |
|
273 return n, err |
|
274 } |
|
275 } |
|
276 return n, nil |
|
277 } |
|
278 |
|
279 // WriteTo encode the Tree as Toml and writes it to the writer w. |
|
280 // Returns the number of bytes written in case of success, or an error if anything happened. |
|
281 func (t *Tree) WriteTo(w io.Writer) (int64, error) { |
|
282 return t.writeTo(w, "", "", 0, false) |
|
283 } |
|
284 |
|
285 // ToTomlString generates a human-readable representation of the current tree. |
|
286 // Output spans multiple lines, and is suitable for ingest by a TOML parser. |
|
287 // If the conversion cannot be performed, ToString returns a non-nil error. |
|
288 func (t *Tree) ToTomlString() (string, error) { |
|
289 var buf bytes.Buffer |
|
290 _, err := t.WriteTo(&buf) |
|
291 if err != nil { |
|
292 return "", err |
|
293 } |
|
294 return buf.String(), nil |
|
295 } |
|
296 |
|
297 // String generates a human-readable representation of the current tree. |
|
298 // Alias of ToString. Present to implement the fmt.Stringer interface. |
|
299 func (t *Tree) String() string { |
|
300 result, _ := t.ToTomlString() |
|
301 return result |
|
302 } |
|
303 |
|
304 // ToMap recursively generates a representation of the tree using Go built-in structures. |
|
305 // The following types are used: |
|
306 // |
|
307 // * bool |
|
308 // * float64 |
|
309 // * int64 |
|
310 // * string |
|
311 // * uint64 |
|
312 // * time.Time |
|
313 // * map[string]interface{} (where interface{} is any of this list) |
|
314 // * []interface{} (where interface{} is any of this list) |
|
315 func (t *Tree) ToMap() map[string]interface{} { |
|
316 result := map[string]interface{}{} |
|
317 |
|
318 for k, v := range t.values { |
|
319 switch node := v.(type) { |
|
320 case []*Tree: |
|
321 var array []interface{} |
|
322 for _, item := range node { |
|
323 array = append(array, item.ToMap()) |
|
324 } |
|
325 result[k] = array |
|
326 case *Tree: |
|
327 result[k] = node.ToMap() |
|
328 case *tomlValue: |
|
329 result[k] = node.value |
|
330 } |
|
331 } |
|
332 return result |
|
333 } |